mirror of
https://github.com/rembo10/headphones.git
synced 2026-09-11 09:11:42 +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
|
# 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
|
## v0.5.19
|
||||||
Released 27 March 2018
|
Released 27 March 2018
|
||||||
|
|
||||||
|
|||||||
@@ -324,7 +324,7 @@
|
|||||||
if (++x === 10) {
|
if (++x === 10) {
|
||||||
clearInterval(refreshInterval);
|
clearInterval(refreshInterval);
|
||||||
}
|
}
|
||||||
var sts = $("#artistname").text();
|
var sts = $("#artistname").text().trim();
|
||||||
if (wasLoading == true || sts == "Loading"){
|
if (wasLoading == true || sts == "Loading"){
|
||||||
location.reload();
|
location.reload();
|
||||||
$('#albumnamespinner').remove()
|
$('#albumnamespinner').remove()
|
||||||
|
|||||||
@@ -137,7 +137,7 @@
|
|||||||
<label title="Ignore MusicBrainz album updates missing a release date.">
|
<label title="Ignore MusicBrainz album updates missing a release date.">
|
||||||
Ignore Undated Releases
|
Ignore Undated Releases
|
||||||
</label>
|
</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>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</td>
|
</td>
|
||||||
@@ -601,23 +601,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</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>
|
<fieldset>
|
||||||
<div class="row checkbox left">
|
<div class="row checkbox left">
|
||||||
<input id="use_waffles" type="checkbox" class="bigcheck" name="use_waffles" value="1" ${config['use_waffles']} /><label for="use_waffles"><span class="option">Waffles.ch</span></label>
|
<input id="use_waffles" type="checkbox" class="bigcheck" name="use_waffles" value="1" ${config['use_waffles']} /><label for="use_waffles"><span class="option">Waffles.ch</span></label>
|
||||||
@@ -664,24 +647,24 @@
|
|||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<div class="row checkbox left">
|
<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>
|
||||||
<div class="config">
|
<div class="config">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>Username</label>
|
<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>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>Password</label>
|
<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>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>URL</label>
|
<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>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>Seed Ratio</label>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@@ -703,17 +686,9 @@
|
|||||||
<label>Seed Ratio</label>
|
<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">
|
<input type="text" class="override-float" name="redacted_ratio" value="${config['redacted_ratio']}" size="10" title="Stop seeding when ratio met, 0 = unlimited. Scheduled job will remove torrent when post processed and finished seeding">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</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">
|
<div class="row">
|
||||||
<label>Seed Ratio</label>
|
<label>Use freeleech tokens when available</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.">
|
<input type="checkbox" name="redacted_use_fltoken" value="1" ${config['redacted_use_fltoken']}>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@@ -733,6 +708,10 @@
|
|||||||
<label>Torznab API</label>
|
<label>Torznab API</label>
|
||||||
<input type="text" name="torznab_apikey" value="${config['torznab_apikey']}" size="36">
|
<input type="text" name="torznab_apikey" value="${config['torznab_apikey']}" size="36">
|
||||||
</div>
|
</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">
|
<div class="row checkbox">
|
||||||
<input id="torznab_enabled" type="checkbox" name="torznab_enabled" value="1" ${config['torznab_enabled']} /><label>Enabled</label>
|
<input id="torznab_enabled" type="checkbox" name="torznab_enabled" value="1" ${config['torznab_enabled']} /><label>Enabled</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -742,7 +721,7 @@
|
|||||||
%>
|
%>
|
||||||
%for torznab in config['extra_torznabs']:
|
%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"
|
torznab_enabled = "checked"
|
||||||
else:
|
else:
|
||||||
torznab_enabled = ""
|
torznab_enabled = ""
|
||||||
@@ -756,6 +735,10 @@
|
|||||||
<label>Torznab API</label>
|
<label>Torznab API</label>
|
||||||
<input type="text" name="torznab_api${torznab_number}" value="${torznab[1]}" size="36">
|
<input type="text" name="torznab_api${torznab_number}" value="${torznab[1]}" size="36">
|
||||||
</div>
|
</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">
|
<div class="row checkbox">
|
||||||
<input id="torznab_enabled" type="checkbox" name="torznab_enabled${torznab_number}" value="1" ${torznab_enabled} /><label>Enabled</label>
|
<input id="torznab_enabled" type="checkbox" name="torznab_enabled${torznab_number}" value="1" ${torznab_enabled} /><label>Enabled</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -787,34 +770,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</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>
|
</fieldset>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -2566,7 +2521,7 @@
|
|||||||
|
|
||||||
$("#add_torznab").click(function() {
|
$("#add_torznab").click(function() {
|
||||||
var intId = $("#torznab_providers > div").size() + deletedTorznabs + 1;
|
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>");
|
var removeTorznabButton = $("<div class=\"row\"><input type=\"button\" class=\"remove_torznab\" value=\"Remove\" /></div>");
|
||||||
removeTorznabButton.click(function() {
|
removeTorznabButton.click(function() {
|
||||||
$(this).parent().remove();
|
$(this).parent().remove();
|
||||||
@@ -2595,20 +2550,16 @@
|
|||||||
initConfigCheckbox("#use_nzbsorg");
|
initConfigCheckbox("#use_nzbsorg");
|
||||||
initConfigCheckbox("#use_omgwtfnzbs");
|
initConfigCheckbox("#use_omgwtfnzbs");
|
||||||
initConfigCheckbox("#use_torznab");
|
initConfigCheckbox("#use_torznab");
|
||||||
initConfigCheckbox("#use_kat");
|
|
||||||
initConfigCheckbox("#use_piratebay");
|
initConfigCheckbox("#use_piratebay");
|
||||||
initConfigCheckbox("#use_oldpiratebay");
|
initConfigCheckbox("#use_oldpiratebay");
|
||||||
initConfigCheckbox("#use_mininova");
|
|
||||||
initConfigCheckbox("#use_waffles");
|
initConfigCheckbox("#use_waffles");
|
||||||
initConfigCheckbox("#use_rutracker");
|
initConfigCheckbox("#use_rutracker");
|
||||||
initConfigCheckbox("#use_apollo");
|
initConfigCheckbox("#use_orpheus");
|
||||||
initConfigCheckbox("#use_redacted");
|
initConfigCheckbox("#use_redacted");
|
||||||
initConfigCheckbox("#use_strike");
|
|
||||||
initConfigCheckbox("#api_enabled");
|
initConfigCheckbox("#api_enabled");
|
||||||
initConfigCheckbox("#enable_https");
|
initConfigCheckbox("#enable_https");
|
||||||
initConfigCheckbox("#customauth");
|
initConfigCheckbox("#customauth");
|
||||||
initConfigCheckbox("#mb_ignore_age_missing");
|
initConfigCheckbox("#mb_ignore_age_missing");
|
||||||
initConfigCheckbox("#use_tquattrecentonze");
|
|
||||||
|
|
||||||
$('#twitterStep1').click(function () {
|
$('#twitterStep1').click(function () {
|
||||||
$.get("/twitterStep1", function (data) {window.open(data); })
|
$.get("/twitterStep1", function (data) {window.open(data); })
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
"aTargets": [0],
|
"aTargets": [0],
|
||||||
"mData":"ArtistID",
|
"mData":"ArtistID",
|
||||||
"mRender": function ( data, type, full ) {
|
"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():
|
def dbcheck():
|
||||||
|
logger.debug("SQLite Version: %s", sqlite3.sqlite_version)
|
||||||
|
logger.debug("DB-API Version: %s", sqlite3.version)
|
||||||
conn = sqlite3.connect(DB_FILE)
|
conn = sqlite3.connect(DB_FILE)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -420,6 +422,31 @@ def dbcheck():
|
|||||||
c.execute(
|
c.execute(
|
||||||
'CREATE INDEX IF NOT EXISTS tracks_artistid ON tracks(ArtistID ASC)')
|
'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:
|
try:
|
||||||
c.execute('SELECT IncludeExtras from artists')
|
c.execute('SELECT IncludeExtras from artists')
|
||||||
except sqlite3.OperationalError:
|
except sqlite3.OperationalError:
|
||||||
|
|||||||
+1
-1
@@ -82,7 +82,7 @@ class Api(object):
|
|||||||
def fetchData(self):
|
def fetchData(self):
|
||||||
|
|
||||||
if self.data == 'OK':
|
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 = getattr(self, "_" + self.cmd)
|
||||||
methodToCall(**self.kwargs)
|
methodToCall(**self.kwargs)
|
||||||
if 'callback' not in self.kwargs:
|
if 'callback' not in self.kwargs:
|
||||||
|
|||||||
+101
-20
@@ -14,11 +14,16 @@
|
|||||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
from six.moves.urllib.parse import urlencode
|
||||||
|
|
||||||
import headphones
|
import headphones
|
||||||
from headphones import db, helpers, logger, lastfm, request, mb
|
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):
|
class Cache(object):
|
||||||
@@ -96,6 +101,11 @@ class Cache(object):
|
|||||||
def _is_current(self, filename=None, date=None):
|
def _is_current(self, filename=None, date=None):
|
||||||
|
|
||||||
if filename:
|
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)
|
base_filename = os.path.basename(filename)
|
||||||
date = base_filename.split('.')[1]
|
date = base_filename.split('.')[1]
|
||||||
|
|
||||||
@@ -211,20 +221,34 @@ class Cache(object):
|
|||||||
if ArtistID:
|
if ArtistID:
|
||||||
|
|
||||||
self.id_type = 'artist'
|
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:
|
if not data:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
if data.get('artistthumb'):
|
||||||
image_url = data['artist']['image'][-1]['#text']
|
image_url = data['artistthumb'][0]['url']
|
||||||
except (KeyError, IndexError):
|
elif data.get('artistbackground'):
|
||||||
logger.debug('No artist image found')
|
image_url = data['artistbackground'][0]['url']
|
||||||
image_url = None
|
# elif data.get('hdmusiclogo'):
|
||||||
|
# image_url = data['hdmusiclogo'][0]['url']
|
||||||
|
|
||||||
thumb_url = self._get_thumb_url(data)
|
# fallback to 1st album cover if none of the above
|
||||||
if not thumb_url:
|
elif 'albums' in data:
|
||||||
logger.debug('No artist thumbnail image found')
|
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:
|
else:
|
||||||
|
|
||||||
@@ -283,6 +307,7 @@ class Cache(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
myDB = db.DBConnection()
|
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
|
# 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
|
# Exception is when adding albums manually, then we should use release id
|
||||||
@@ -311,15 +336,42 @@ class Cache(object):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
logger.debug('No artist bio found')
|
logger.debug('No artist bio found')
|
||||||
self.info_content = None
|
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)
|
# 2019 last.fm no longer allows access to artist images, try fanart.tv instead
|
||||||
if not thumb_url:
|
image_url = None
|
||||||
logger.debug('No artist thumbnail image found')
|
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:
|
else:
|
||||||
dbalbum = myDB.action(
|
dbalbum = myDB.action(
|
||||||
@@ -427,6 +479,10 @@ class Cache(object):
|
|||||||
|
|
||||||
ext = os.path.splitext(image_url)[1]
|
ext = os.path.splitext(image_url)[1]
|
||||||
|
|
||||||
|
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,
|
artwork_path = os.path.join(self.path_to_art_cache,
|
||||||
self.id + '.' + helpers.today() + ext)
|
self.id + '.' + helpers.today() + ext)
|
||||||
try:
|
try:
|
||||||
@@ -443,6 +499,8 @@ class Cache(object):
|
|||||||
# as it's missing/outdated.
|
# as it's missing/outdated.
|
||||||
if thumb_url and self.query_type in ['thumb', 'artwork'] and not (
|
if thumb_url and self.query_type in ['thumb', 'artwork'] and not (
|
||||||
self.thumb_files and self._is_current(self.thumb_files[0])):
|
self.thumb_files and self._is_current(self.thumb_files[0])):
|
||||||
|
|
||||||
|
if not (self.query_type == 'artwork' and 'fanart' in thumb_url and artwork):
|
||||||
artwork = request.request_content(thumb_url, timeout=20)
|
artwork = request.request_content(thumb_url, timeout=20)
|
||||||
|
|
||||||
if artwork:
|
if artwork:
|
||||||
@@ -466,9 +524,32 @@ class Cache(object):
|
|||||||
|
|
||||||
ext = os.path.splitext(image_url)[1]
|
ext = os.path.splitext(image_url)[1]
|
||||||
|
|
||||||
|
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,
|
thumb_path = os.path.join(self.path_to_art_cache,
|
||||||
'T_' + self.id + '.' + helpers.today() + ext)
|
'T_' + self.id + '.' + helpers.today() + ext)
|
||||||
try:
|
try:
|
||||||
|
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:
|
with open(thumb_path, 'wb') as f:
|
||||||
f.write(artwork)
|
f.write(artwork)
|
||||||
|
|
||||||
@@ -486,7 +567,7 @@ def getArtwork(ArtistID=None, AlbumID=None):
|
|||||||
if not artwork_path:
|
if not artwork_path:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if artwork_path.startswith('http://'):
|
if artwork_path.startswith(('http://', 'https://')):
|
||||||
return artwork_path
|
return artwork_path
|
||||||
else:
|
else:
|
||||||
artwork_file = os.path.basename(artwork_path)
|
artwork_file = os.path.basename(artwork_path)
|
||||||
@@ -500,7 +581,7 @@ def getThumb(ArtistID=None, AlbumID=None):
|
|||||||
if not artwork_path:
|
if not artwork_path:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if artwork_path.startswith('http://'):
|
if artwork_path.startswith(('http://', 'https://')):
|
||||||
return artwork_path
|
return artwork_path
|
||||||
else:
|
else:
|
||||||
thumbnail_file = os.path.basename(artwork_path)
|
thumbnail_file = os.path.basename(artwork_path)
|
||||||
|
|||||||
+23
-18
@@ -41,11 +41,11 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'ALBUM_COMPLETION_PCT': (int, 'Advanced', 80),
|
'ALBUM_COMPLETION_PCT': (int, 'Advanced', 80),
|
||||||
'API_ENABLED': (int, 'General', 0),
|
'API_ENABLED': (int, 'General', 0),
|
||||||
'API_KEY': (str, 'General', ''),
|
'API_KEY': (str, 'General', ''),
|
||||||
'APOLLO': (int, 'Apollo.rip', 0),
|
'ORPHEUS': (int, 'Orpheus.network', 0),
|
||||||
'APOLLO_PASSWORD': (str, 'Apollo.rip', ''),
|
'ORPHEUS_PASSWORD': (str, 'Orpheus.network', ''),
|
||||||
'APOLLO_RATIO': (str, 'Apollo.rip', ''),
|
'ORPHEUS_RATIO': (str, 'Orpheus.network', ''),
|
||||||
'APOLLO_USERNAME': (str, 'Apollo.rip', ''),
|
'ORPHEUS_USERNAME': (str, 'Orpheus.network', ''),
|
||||||
'APOLLO_URL': (str, 'Apollo.rip', 'https://apollo.rip'),
|
'ORPHEUS_URL': (str, 'Orpheus.network', 'https://orpheus.network'),
|
||||||
'AUTOWANT_ALL': (int, 'General', 0),
|
'AUTOWANT_ALL': (int, 'General', 0),
|
||||||
'AUTOWANT_MANUALLY_ADDED': (int, 'General', 1),
|
'AUTOWANT_MANUALLY_ADDED': (int, 'General', 1),
|
||||||
'AUTOWANT_UPCOMING': (int, 'General', 1),
|
'AUTOWANT_UPCOMING': (int, 'General', 1),
|
||||||
@@ -150,9 +150,6 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'JOIN_ENABLED': (int, 'Join', 0),
|
'JOIN_ENABLED': (int, 'Join', 0),
|
||||||
'JOIN_ONSNATCH': (int, 'Join', 0),
|
'JOIN_ONSNATCH': (int, 'Join', 0),
|
||||||
'JOURNAL_MODE': (str, 'Advanced', 'wal'),
|
'JOURNAL_MODE': (str, 'Advanced', 'wal'),
|
||||||
'KAT': (int, 'Kat', 0),
|
|
||||||
'KAT_PROXY_URL': (str, 'Kat', ''),
|
|
||||||
'KAT_RATIO': (str, 'Kat', ''),
|
|
||||||
'KEEP_NFO': (int, 'General', 0),
|
'KEEP_NFO': (int, 'General', 0),
|
||||||
'KEEP_TORRENT_FILES': (int, 'General', 0),
|
'KEEP_TORRENT_FILES': (int, 'General', 0),
|
||||||
'KEEP_TORRENT_FILES_DIR': (path, 'General', ''),
|
'KEEP_TORRENT_FILES_DIR': (path, 'General', ''),
|
||||||
@@ -168,8 +165,6 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'LOSSLESS_DESTINATION_DIR': (path, 'General', ''),
|
'LOSSLESS_DESTINATION_DIR': (path, 'General', ''),
|
||||||
'MB_IGNORE_AGE': (int, 'General', 365),
|
'MB_IGNORE_AGE': (int, 'General', 365),
|
||||||
'MB_IGNORE_AGE_MISSING': (int, 'General', 0),
|
'MB_IGNORE_AGE_MISSING': (int, 'General', 0),
|
||||||
'MININOVA': (int, 'Mininova', 0),
|
|
||||||
'MININOVA_RATIO': (str, 'Mininova', ''),
|
|
||||||
'MIRROR': (str, 'General', 'musicbrainz.org'),
|
'MIRROR': (str, 'General', 'musicbrainz.org'),
|
||||||
'MOVE_FILES': (int, 'General', 0),
|
'MOVE_FILES': (int, 'General', 0),
|
||||||
'MPC_ENABLED': (bool_int, 'MPC', False),
|
'MPC_ENABLED': (bool_int, 'MPC', False),
|
||||||
@@ -271,8 +266,6 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'SONGKICK_ENABLED': (int, 'Songkick', 1),
|
'SONGKICK_ENABLED': (int, 'Songkick', 1),
|
||||||
'SONGKICK_FILTER_ENABLED': (int, 'Songkick', 0),
|
'SONGKICK_FILTER_ENABLED': (int, 'Songkick', 0),
|
||||||
'SONGKICK_LOCATION': (str, 'Songkick', ''),
|
'SONGKICK_LOCATION': (str, 'Songkick', ''),
|
||||||
'STRIKE': (int, 'Strike', 0),
|
|
||||||
'STRIKE_RATIO': (str, 'Strike', ''),
|
|
||||||
'SUBSONIC_ENABLED': (int, 'Subsonic', 0),
|
'SUBSONIC_ENABLED': (int, 'Subsonic', 0),
|
||||||
'SUBSONIC_HOST': (str, 'Subsonic', ''),
|
'SUBSONIC_HOST': (str, 'Subsonic', ''),
|
||||||
'SUBSONIC_PASSWORD': (str, 'Subsonic', ''),
|
'SUBSONIC_PASSWORD': (str, 'Subsonic', ''),
|
||||||
@@ -289,6 +282,7 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'TORZNAB_APIKEY': (str, 'Torznab', ''),
|
'TORZNAB_APIKEY': (str, 'Torznab', ''),
|
||||||
'TORZNAB_ENABLED': (int, 'Torznab', 1),
|
'TORZNAB_ENABLED': (int, 'Torznab', 1),
|
||||||
'TORZNAB_HOST': (str, 'Torznab', ''),
|
'TORZNAB_HOST': (str, 'Torznab', ''),
|
||||||
|
'TORZNAB_RATIO': (str, 'Torznab', ''),
|
||||||
'TRANSMISSION_HOST': (str, 'Transmission', ''),
|
'TRANSMISSION_HOST': (str, 'Transmission', ''),
|
||||||
'TRANSMISSION_PASSWORD': (str, 'Transmission', ''),
|
'TRANSMISSION_PASSWORD': (str, 'Transmission', ''),
|
||||||
'TRANSMISSION_USERNAME': (str, 'Transmission', ''),
|
'TRANSMISSION_USERNAME': (str, 'Transmission', ''),
|
||||||
@@ -297,9 +291,6 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'TWITTER_PASSWORD': (str, 'Twitter', ''),
|
'TWITTER_PASSWORD': (str, 'Twitter', ''),
|
||||||
'TWITTER_PREFIX': (str, 'Twitter', 'Headphones'),
|
'TWITTER_PREFIX': (str, 'Twitter', 'Headphones'),
|
||||||
'TWITTER_USERNAME': (str, 'Twitter', ''),
|
'TWITTER_USERNAME': (str, 'Twitter', ''),
|
||||||
'TQUATTRECENTONZE': (int, 'tquattrecentonze', 0),
|
|
||||||
'TQUATTRECENTONZE_PASSWORD': (str, 'tquattrecentonze', ''),
|
|
||||||
'TQUATTRECENTONZE_USER': (str, 'tquattrecentonze', ''),
|
|
||||||
'UPDATE_DB_INTERVAL': (int, 'General', 24),
|
'UPDATE_DB_INTERVAL': (int, 'General', 24),
|
||||||
'USENET_RETENTION': (int, 'General', '1500'),
|
'USENET_RETENTION': (int, 'General', '1500'),
|
||||||
'UTORRENT_HOST': (str, 'uTorrent', ''),
|
'UTORRENT_HOST': (str, 'uTorrent', ''),
|
||||||
@@ -316,6 +307,7 @@ _CONFIG_DEFINITIONS = {
|
|||||||
'REDACTED_USERNAME': (str, 'Redacted', ''),
|
'REDACTED_USERNAME': (str, 'Redacted', ''),
|
||||||
'REDACTED_PASSWORD': (str, 'Redacted', ''),
|
'REDACTED_PASSWORD': (str, 'Redacted', ''),
|
||||||
'REDACTED_RATIO': (str, 'Redacted', ''),
|
'REDACTED_RATIO': (str, 'Redacted', ''),
|
||||||
|
'REDACTED_USE_FLTOKEN': (int, 'Redacted', 0),
|
||||||
'XBMC_ENABLED': (int, 'XBMC', 0),
|
'XBMC_ENABLED': (int, 'XBMC', 0),
|
||||||
'XBMC_HOST': (str, 'XBMC', ''),
|
'XBMC_HOST': (str, 'XBMC', ''),
|
||||||
'XBMC_NOTIFY': (int, 'XBMC', 0),
|
'XBMC_NOTIFY': (int, 'XBMC', 0),
|
||||||
@@ -420,8 +412,8 @@ class Config(object):
|
|||||||
def get_extra_torznabs(self):
|
def get_extra_torznabs(self):
|
||||||
""" Return the extra torznab tuples """
|
""" Return the extra torznab tuples """
|
||||||
extra_torznabs = list(
|
extra_torznabs = list(
|
||||||
itertools.izip(*[itertools.islice(self.EXTRA_TORZNABS, i, None, 3)
|
itertools.izip(*[itertools.islice(self.EXTRA_TORZNABS, i, None, 4)
|
||||||
for i in range(3)])
|
for i in range(4)])
|
||||||
)
|
)
|
||||||
return extra_torznabs
|
return extra_torznabs
|
||||||
|
|
||||||
@@ -492,4 +484,17 @@ class Config(object):
|
|||||||
if self.CONFIG_VERSION == '5':
|
if self.CONFIG_VERSION == '5':
|
||||||
if self.OPEN_MAGNET_LINKS:
|
if self.OPEN_MAGNET_LINKS:
|
||||||
self.MAGNET_LINKS = 2
|
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
|
# TORZNABS
|
||||||
# TODO : here is copypaste from of NEZNABS tests. Make tests better, plz refactor them
|
# TODO : here is copypaste from of NEZNABS tests. Make tests better, plz refactor them
|
||||||
#
|
#
|
||||||
@TestArgs(
|
# TODO: Fix tests for following:
|
||||||
('', []),
|
# CONFIG_VERSION == '5' each entry = 'host, api, enabled'
|
||||||
('ABCDEF', [('A', 'B', 'C'), ('D', 'E', 'F')]),
|
# CONFIG_VERSION > '5' each entry = 'host, api, seed ratio, enabled'
|
||||||
(['ABC', 'DEF'], []),
|
# @TestArgs(
|
||||||
([1], []),
|
# ('', []),
|
||||||
([1, 2], []),
|
# ('ABCDEF', [('A', 'B', 'C'), ('D', 'E', 'F')]),
|
||||||
([1, 2, 3], [(1, 2, 3)]),
|
# (['ABC', 'DEF'], []),
|
||||||
|
# ([1], []),
|
||||||
([1, 2, 3, 'Aaa'], [(1, 2, 3)]),
|
# ([1, 2], []),
|
||||||
([1, 2, 3, 'Aaa', 'Bbba'], [(1, 2, 3)]),
|
# ([1, 2, 3], [(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')]),
|
# ([1, 2, 3, 'Aaa'], [(1, 2, 3)]),
|
||||||
)
|
# ([1, 2, 3, 'Aaa', 'Bbba'], [(1, 2, 3)]),
|
||||||
def test_get_extra_torznabs(self, conf_value, expected):
|
# ([1, 2, 3, 'Aaa', 'Bbba', 'Ccccc'], [(1, 2, 3), ('Aaa', 'Bbba', 'Ccccc')]),
|
||||||
""" Config: get_extra_torznabs """
|
# ([1, 2, 3, 'Aaa', 'Bbba', 'Ccccc', 'Ddddda'], [(1, 2, 3), ('Aaa', 'Bbba', 'Ccccc')]),
|
||||||
path = '/tmp/notexist'
|
# )
|
||||||
|
# def test_get_extra_torznabs(self, conf_value, expected):
|
||||||
# itertools.izip(*[itertools.islice('', i, None, 3) for i in range(3)])
|
# """ Config: get_extra_torznabs """
|
||||||
# set up mocks:
|
# path = '/tmp/notexist'
|
||||||
# 'EXTRA_TORZNABS': (list, '', ''),
|
#
|
||||||
self.config_mock["Torznab"] = {"extra_torznabs": conf_value}
|
# # itertools.izip(*[itertools.islice('', i, None, 3) for i in range(3)])
|
||||||
|
# # set up mocks:
|
||||||
# call methods
|
# # 'EXTRA_TORZNABS': (list, '', ''),
|
||||||
c = headphones.config.Config(path)
|
# self.config_mock["Torznab"] = {"extra_torznabs": conf_value}
|
||||||
res = c.get_extra_torznabs()
|
#
|
||||||
|
# # call methods
|
||||||
# assertions:
|
# c = headphones.config.Config(path)
|
||||||
self.assertEqual(res, expected)
|
# res = c.get_extra_torznabs()
|
||||||
|
#
|
||||||
def test_clear_extra_torznabs(self):
|
# # assertions:
|
||||||
""" Config: clear_extra_torznabs """
|
# self.assertEqual(res, expected)
|
||||||
path = '/tmp/notexist'
|
#
|
||||||
|
# def test_clear_extra_torznabs(self):
|
||||||
random_value = -1292721
|
# """ Config: clear_extra_torznabs """
|
||||||
self.config_mock["Torznab"] = {"extra_torznabs": [1, 2, 3]}
|
# path = '/tmp/notexist'
|
||||||
self.config_mock["Torznab"] = {"do_not_touch": random_value}
|
#
|
||||||
|
# random_value = -1292721
|
||||||
# call methods
|
# self.config_mock["Torznab"] = {"extra_torznabs": [1, 2, 3]}
|
||||||
c = headphones.config.Config(path)
|
# self.config_mock["Torznab"] = {"do_not_touch": random_value}
|
||||||
res = c.clear_extra_torznabs()
|
#
|
||||||
|
# # call methods
|
||||||
# assertions:
|
# c = headphones.config.Config(path)
|
||||||
self.assertIsNone(res)
|
# res = c.clear_extra_torznabs()
|
||||||
self.assertEqual(self.config_mock["Torznab"]["extra_torznabs"], [])
|
#
|
||||||
self.assertEqual(self.config_mock["Torznab"]["do_not_touch"], random_value)
|
# # assertions:
|
||||||
|
# self.assertIsNone(res)
|
||||||
@TestArgs(
|
# self.assertEqual(self.config_mock["Torznab"]["extra_torznabs"], [])
|
||||||
([], [''], ['']),
|
# self.assertEqual(self.config_mock["Torznab"]["do_not_touch"], random_value)
|
||||||
([], 'ABCDEF', ['A', 'B', 'C', 'D', 'E', 'F']),
|
#
|
||||||
|
# @TestArgs(
|
||||||
([1, 2, [False, True]], ['3', [0, 0]], [1, 2, [False, True], '3', [0, 0]]),
|
# ([], [''], ['']),
|
||||||
)
|
# ([], 'ABCDEF', ['A', 'B', 'C', 'D', 'E', 'F']),
|
||||||
def test_add_extra_torznab(self, initial, added, expected):
|
#
|
||||||
""" Config: add_extra_torznab """
|
# ([1, 2, [False, True]], ['3', [0, 0]], [1, 2, [False, True], '3', [0, 0]]),
|
||||||
path = '/tmp/notexist'
|
# )
|
||||||
|
# def test_add_extra_torznab(self, initial, added, expected):
|
||||||
self.config_mock["Torznab"] = {"extra_torznabs": initial}
|
# """ Config: add_extra_torznab """
|
||||||
|
# path = '/tmp/notexist'
|
||||||
# call methods
|
#
|
||||||
c = headphones.config.Config(path)
|
# self.config_mock["Torznab"] = {"extra_torznabs": initial}
|
||||||
c.add_extra_torznab(added)
|
#
|
||||||
act = self.config_mock["Torznab"]["extra_torznabs"]
|
# # call methods
|
||||||
|
# c = headphones.config.Config(path)
|
||||||
# assertions:
|
# c.add_extra_torznab(added)
|
||||||
self.assertEqual(act, expected)
|
# act = self.config_mock["Torznab"]["extra_torznabs"]
|
||||||
|
#
|
||||||
@TestArgs(
|
# # assertions:
|
||||||
(None),
|
# self.assertEqual(act, expected)
|
||||||
([]),
|
#
|
||||||
([1, 2, 3]),
|
# @TestArgs(
|
||||||
([True]),
|
# (None),
|
||||||
)
|
# ([]),
|
||||||
def test_add_extra_torznab_raise_on_none(self, initial):
|
# ([1, 2, 3]),
|
||||||
""" Config: add_extra_torznab should raise on None adding"""
|
# ([True]),
|
||||||
path = '/tmp/notexist'
|
# )
|
||||||
|
# def test_add_extra_torznab_raise_on_none(self, initial):
|
||||||
self.config_mock["Torznab"] = {"extra_torznabs": initial}
|
# """ Config: add_extra_torznab should raise on None adding"""
|
||||||
|
# path = '/tmp/notexist'
|
||||||
# call methods
|
#
|
||||||
c = headphones.config.Config(path)
|
# self.config_mock["Torznab"] = {"extra_torznabs": initial}
|
||||||
with self.assertRaises(TypeError):
|
#
|
||||||
c.add_extra_torznab(None)
|
# # call methods
|
||||||
pass
|
# c = headphones.config.Config(path)
|
||||||
|
# with self.assertRaises(TypeError):
|
||||||
|
# c.add_extra_torznab(None)
|
||||||
|
# pass
|
||||||
|
|||||||
+75
-17
@@ -19,6 +19,8 @@
|
|||||||
|
|
||||||
from __future__ import with_statement
|
from __future__ import with_statement
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -45,37 +47,98 @@ class DBConnection:
|
|||||||
self.connection = sqlite3.connect(dbFilename(filename), timeout=20)
|
self.connection = sqlite3.connect(dbFilename(filename), timeout=20)
|
||||||
# don't wait for the disk to finish writing
|
# don't wait for the disk to finish writing
|
||||||
self.connection.execute("PRAGMA synchronous = OFF")
|
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)
|
self.connection.execute("PRAGMA journal_mode = %s" % headphones.CONFIG.JOURNAL_MODE)
|
||||||
# 64mb of cache memory,probably need to make it user configurable
|
# 64mb of cache memory,probably need to make it user configurable
|
||||||
self.connection.execute("PRAGMA cache_size=-%s" % (getCacheSize() * 1024))
|
self.connection.execute("PRAGMA cache_size=-%s" % (getCacheSize() * 1024))
|
||||||
self.connection.row_factory = sqlite3.Row
|
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:
|
if query is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
sqlResult = None
|
sqlResult = None
|
||||||
|
attempts = 0
|
||||||
|
dberror = None
|
||||||
|
|
||||||
|
while attempts < 10:
|
||||||
try:
|
try:
|
||||||
with self.connection as c:
|
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:
|
if args is None:
|
||||||
sqlResult = c.execute(query)
|
sqlResult = c.execute(query)
|
||||||
|
# logger.debug('SQL: ' + query)
|
||||||
else:
|
else:
|
||||||
sqlResult = c.execute(query, args)
|
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:
|
except sqlite3.OperationalError, e:
|
||||||
if "unable to open database file" in e.message or "database is locked" in e.message:
|
if "unable to open database file" in e.message or "database is locked" in e.message:
|
||||||
logger.warn('Database Error: %s', e)
|
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:
|
else:
|
||||||
logger.error('Database error: %s', e)
|
logger.error('Database error: %s', e)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
except sqlite3.DatabaseError, e:
|
except sqlite3.DatabaseError, e:
|
||||||
logger.error('Fatal Error executing %s :: %s', query, e)
|
logger.error('Fatal Error executing %s :: %s', query, e)
|
||||||
raise
|
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
|
return sqlResult
|
||||||
|
|
||||||
def select(self, query, args=None):
|
def select(self, query, args=None):
|
||||||
@@ -88,24 +151,19 @@ class DBConnection:
|
|||||||
return sqlResults
|
return sqlResults
|
||||||
|
|
||||||
def upsert(self, tableName, valueDict, keyDict):
|
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):
|
def genParams(myDict):
|
||||||
return [x + " = ?" for x in myDict.keys()]
|
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(
|
insert_query = ("INSERT INTO " + tableName + " (" + ", ".join(valueDict.keys() + keyDict.keys()) + ")" + " SELECT " + ", ".join(
|
||||||
genParams(valueDict)) + " WHERE " + " AND ".join(genParams(keyDict))
|
["?"] * 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:
|
try:
|
||||||
self.action(insert_query, valueDict.values() + keyDict.values())
|
self.action(update_query, valueDict.values() + keyDict.values(), upsert_insert_qry=insert_query)
|
||||||
except sqlite3.IntegrityError:
|
except sqlite3.IntegrityError:
|
||||||
logger.info('Queries failed: %s and %s', update_query, insert_query)
|
logger.info('Queries failed: %s and %s', update_query, insert_query)
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ def addTorrent(link, data=None, name=None):
|
|||||||
|
|
||||||
result = {}
|
result = {}
|
||||||
retid = False
|
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/']
|
url_waffles = ['https://waffles.ch/', 'http://waffles.ch/']
|
||||||
|
|
||||||
if link.lower().startswith('magnet:'):
|
if link.lower().startswith('magnet:'):
|
||||||
@@ -97,7 +97,7 @@ def addTorrent(link, data=None, name=None):
|
|||||||
if link.lower().startswith(tuple(url_waffles)):
|
if link.lower().startswith(tuple(url_waffles)):
|
||||||
if 'rss=' not in link:
|
if 'rss=' not in link:
|
||||||
link = link + '&rss=1'
|
link = link + '&rss=1'
|
||||||
if link.lower().startswith(tuple(url_apollo)):
|
if link.lower().startswith(tuple(url_orpheus)):
|
||||||
logger.debug('Deluge: Using different User-Agent for this site')
|
logger.debug('Deluge: Using different User-Agent for this site')
|
||||||
user_agent = 'Headphones'
|
user_agent = 'Headphones'
|
||||||
# This method will make Deluge download the file
|
# This method will make Deluge download the file
|
||||||
|
|||||||
+42
-11
@@ -197,9 +197,15 @@ def piratesize(size):
|
|||||||
return 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
|
from headphones import pathrender
|
||||||
if not text:
|
if not pattern:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
if normalize:
|
if normalize:
|
||||||
@@ -216,7 +222,16 @@ def replace_all(text, dic, normalize=False):
|
|||||||
j.decode(headphones.SYS_ENCODING, 'replace'))
|
j.decode(headphones.SYS_ENCODING, 'replace'))
|
||||||
new_dic[i] = j
|
new_dic[i] = j
|
||||||
dic = new_dic
|
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"):
|
def replace_illegal_chars(string, type="file"):
|
||||||
@@ -669,9 +684,9 @@ def preserve_torrent_directory(albumpath, forced=False, single=False):
|
|||||||
try:
|
try:
|
||||||
file_name = os.path.basename(os.path.normpath(albumpath))
|
file_name = os.path.basename(os.path.normpath(albumpath))
|
||||||
if not single:
|
if not single:
|
||||||
prefix = "headphones_" + file_name + "_"
|
prefix = "headphones_" + file_name + "_@hp@_"
|
||||||
else:
|
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)
|
new_folder = tempfile.mkdtemp(prefix=prefix, dir=tempdir)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Cannot create temp directory: " + tempdir.decode(
|
logger.error("Cannot create temp directory: " + tempdir.decode(
|
||||||
@@ -810,9 +825,11 @@ def smartMove(src, dest, delete=True):
|
|||||||
|
|
||||||
source_dir = os.path.dirname(src)
|
source_dir = os.path.dirname(src)
|
||||||
filename = os.path.basename(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)):
|
if os.path.isfile(dest_path):
|
||||||
logger.info('Destination file exists: %s', os.path.join(dest, filename))
|
logger.info('Destination file exists: %s', dest_path)
|
||||||
title = os.path.splitext(filename)[0]
|
title = os.path.splitext(filename)[0]
|
||||||
ext = os.path.splitext(filename)[1]
|
ext = os.path.splitext(filename)[1]
|
||||||
i = 1
|
i = 1
|
||||||
@@ -830,14 +847,28 @@ def smartMove(src, dest, delete=True):
|
|||||||
src.decode(headphones.SYS_ENCODING, 'replace'), e)
|
src.decode(headphones.SYS_ENCODING, 'replace'), e)
|
||||||
break
|
break
|
||||||
|
|
||||||
try:
|
|
||||||
if delete:
|
if delete:
|
||||||
shutil.move(os.path.join(source_dir, filename), os.path.join(dest, filename))
|
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:
|
else:
|
||||||
shutil.copy(os.path.join(source_dir, filename), os.path.join(dest, filename))
|
# 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
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn('Error moving file %s: %s', filename.decode(headphones.SYS_ENCODING, 'replace'),
|
logger.warn('Error copying file %s: %s', filename.decode(headphones.SYS_ENCODING, 'replace'),
|
||||||
e)
|
e)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+18
-7
@@ -89,11 +89,13 @@ def artistlist_to_mbids(artistlist, forced=False):
|
|||||||
addArtisttoDB(artistid)
|
addArtisttoDB(artistid)
|
||||||
|
|
||||||
# Just update the tracks if it does
|
# Just update the tracks if it does
|
||||||
else:
|
|
||||||
havetracks = len(
|
# not sure this is correct and we're updating during scanning in librarysync
|
||||||
myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=?', [artistid])) + len(
|
# else:
|
||||||
myDB.select('SELECT TrackTitle from have WHERE ArtistName like ?', [artist]))
|
# havetracks = len(
|
||||||
myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistID=?', [havetracks, artistid])
|
# 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
|
# Delete it from the New Artists if the request came from there
|
||||||
if forced:
|
if forced:
|
||||||
@@ -105,7 +107,7 @@ def artistlist_to_mbids(artistlist, forced=False):
|
|||||||
try:
|
try:
|
||||||
lastfm.getSimilar()
|
lastfm.getSimilar()
|
||||||
except Exception as e:
|
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):
|
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
|
# This will be used later to build a hybrid release
|
||||||
fullreleaselist = []
|
fullreleaselist = []
|
||||||
# Search for releases within a release group
|
# 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']])
|
[rg['id']])
|
||||||
|
|
||||||
# Build the dictionary for the fullreleaselist
|
# Build the dictionary for the fullreleaselist
|
||||||
@@ -516,7 +518,10 @@ def addArtisttoDB(artistid, extrasonly=False, forcefull=False, type="artist"):
|
|||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
u"[%s] Seeing if we need album art for %s" % (artist['artist_name'], rg['title']))
|
u"[%s] Seeing if we need album art for %s" % (artist['artist_name'], rg['title']))
|
||||||
|
try:
|
||||||
cache.getThumb(AlbumID=rg['id'])
|
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
|
# 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,
|
# 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)
|
finalize_update(artistid, artist['artist_name'], errors)
|
||||||
|
|
||||||
logger.info(u"Seeing if we need album art for: %s" % artist['artist_name'])
|
logger.info(u"Seeing if we need album art for: %s" % artist['artist_name'])
|
||||||
|
try:
|
||||||
cache.getThumb(ArtistID=artistid)
|
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'])
|
logger.info(u"Fetching Metacritic reviews for: %s" % artist['artist_name'])
|
||||||
|
try:
|
||||||
metacritic.update(artistid, artist['artist_name'], artist['releasegroups'])
|
metacritic.update(artistid, artist['artist_name'], artist['releasegroups'])
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Error getting Metacritic reviews: %s", e)
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|||||||
+234
-139
@@ -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'))
|
logger.info('Scanning music directory: %s' % dir.decode(headphones.SYS_ENCODING, 'replace'))
|
||||||
|
|
||||||
if not append:
|
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:
|
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):
|
if not os.path.isfile(encoded_track_string):
|
||||||
myDB.action('UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE Location=?',
|
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=?',
|
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:
|
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 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
|
# Make sure deleted files get accounted for when updating artist track counts
|
||||||
new_artists.append(track['ArtistName'])
|
new_artists.append(location[1])
|
||||||
myDB.action('DELETE FROM have WHERE Location=?', [track['Location']])
|
myDB.action('DELETE FROM have WHERE Location=?', [location[0]])
|
||||||
logger.info(
|
logger.info(
|
||||||
'File %s removed from Headphones, as it is no longer on disk' % encoded_track_string.decode(
|
'File %s removed from Headphones, as it is no longer on disk' % encoded_track_string.decode(
|
||||||
headphones.SYS_ENCODING, 'replace'))
|
headphones.SYS_ENCODING, 'replace'))
|
||||||
@@ -216,6 +232,8 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
|||||||
song_count = 0
|
song_count = 0
|
||||||
latest_artist = []
|
latest_artist = []
|
||||||
last_completion_percentage = 0
|
last_completion_percentage = 0
|
||||||
|
prev_artist_name = None
|
||||||
|
artistid = None
|
||||||
|
|
||||||
for song in song_list:
|
for song in song_list:
|
||||||
|
|
||||||
@@ -237,83 +255,68 @@ 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
|
# 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.
|
# (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(
|
if song['ArtistName'] and song['CleanName']:
|
||||||
'SELECT ArtistName, AlbumTitle, TrackTitle, AlbumID from tracks WHERE ArtistName LIKE ? AND AlbumTitle LIKE ? AND TrackTitle LIKE ?',
|
artist_name = song['ArtistName']
|
||||||
[song['ArtistName'], song['AlbumTitle'], song['TrackTitle']]).fetchone()
|
clean_name = song['CleanName']
|
||||||
have_updated = False
|
|
||||||
|
# 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:
|
if track:
|
||||||
controlValueDict = {'ArtistName': track['ArtistName'],
|
albumid = track['AlbumID']
|
||||||
'AlbumTitle': track['AlbumTitle'],
|
myDB.action(
|
||||||
'TrackTitle': track['TrackTitle']}
|
'UPDATE tracks SET Location = ?, BitRate = ?, Format = ? WHERE CleanName = ? AND ArtistID = ?',
|
||||||
newValueDict = {'Location': song['Location'],
|
[song['Location'], song['BitRate'], song['Format'], clean_name, artistid])
|
||||||
'BitRate': song['BitRate'],
|
|
||||||
'Format': song['Format']}
|
|
||||||
myDB.upsert("tracks", newValueDict, controlValueDict)
|
|
||||||
|
|
||||||
controlValueDict2 = {'Location': song['Location']}
|
# Update alltracks
|
||||||
newValueDict2 = {'Matched': track['AlbumID']}
|
alltrack = myDB.action('SELECT AlbumID, ArtistName FROM alltracks WHERE CleanName = ? AND ArtistID = ?', [clean_name, artistid]).fetchone()
|
||||||
myDB.upsert("have", newValueDict2, controlValueDict2)
|
|
||||||
have_updated = True
|
|
||||||
else:
|
|
||||||
track = myDB.action('SELECT CleanName, AlbumID from tracks WHERE CleanName LIKE ?',
|
|
||||||
[song['CleanName']]).fetchone()
|
|
||||||
if track:
|
|
||||||
controlValueDict = {'CleanName': track['CleanName']}
|
|
||||||
newValueDict = {'Location': song['Location'],
|
|
||||||
'BitRate': song['BitRate'],
|
|
||||||
'Format': song['Format']}
|
|
||||||
myDB.upsert("tracks", newValueDict, controlValueDict)
|
|
||||||
|
|
||||||
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:
|
if alltrack:
|
||||||
controlValueDict = {'ArtistName': alltrack['ArtistName'],
|
albumid = alltrack['AlbumID']
|
||||||
'AlbumTitle': alltrack['AlbumTitle'],
|
myDB.action(
|
||||||
'TrackTitle': alltrack['TrackTitle']}
|
'UPDATE alltracks SET Location = ?, BitRate = ?, Format = ? WHERE CleanName = ? AND ArtistID = ?',
|
||||||
newValueDict = {'Location': song['Location'],
|
[song['Location'], song['BitRate'], song['Format'], clean_name, artistid])
|
||||||
'BitRate': song['BitRate'],
|
|
||||||
'Format': song['Format']}
|
|
||||||
myDB.upsert("alltracks", newValueDict, controlValueDict)
|
|
||||||
|
|
||||||
|
# Update have
|
||||||
controlValueDict2 = {'Location': song['Location']}
|
controlValueDict2 = {'Location': song['Location']}
|
||||||
newValueDict2 = {'Matched': alltrack['AlbumID']}
|
if albumid:
|
||||||
myDB.upsert("have", newValueDict2, controlValueDict2)
|
newValueDict2 = {'Matched': albumid}
|
||||||
else:
|
else:
|
||||||
alltrack = myDB.action(
|
|
||||||
'SELECT CleanName, AlbumID from alltracks WHERE CleanName LIKE ?',
|
|
||||||
[song['CleanName']]).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)
|
|
||||||
|
|
||||||
else:
|
|
||||||
controlValueDict2 = {'Location': song['Location']}
|
|
||||||
newValueDict2 = {'Matched': "Failed"}
|
newValueDict2 = {'Matched': "Failed"}
|
||||||
myDB.upsert("have", newValueDict2, controlValueDict2)
|
myDB.upsert("have", newValueDict2, controlValueDict2)
|
||||||
|
|
||||||
@@ -327,49 +330,110 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None,
|
|||||||
|
|
||||||
# Clean up the new artist list
|
# Clean up the new artist list
|
||||||
unique_artists = {}.fromkeys(new_artists).keys()
|
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.
|
# # Don't think we need to do this, check the db instead below
|
||||||
artist_list = [
|
#
|
||||||
x for x in unique_artists
|
# # artist scan
|
||||||
if helpers.clean_name(x).lower() not in [
|
# if ArtistName:
|
||||||
helpers.clean_name(y[0]).lower()
|
# current_artists = [[ArtistName]]
|
||||||
for y in current_artists
|
# # directory scan
|
||||||
]
|
# else:
|
||||||
]
|
# current_artists = myDB.select('SELECT ArtistName, ArtistID FROM artists WHERE ArtistName IS NOT NULL')
|
||||||
artists_checked = [
|
#
|
||||||
x for x in unique_artists
|
# # There was a bug where artists with special characters (-,') would show up in new artists.
|
||||||
if helpers.clean_name(x).lower() in [
|
#
|
||||||
helpers.clean_name(y[0]).lower()
|
# # artist_list = scanned artists not in the db
|
||||||
for y in current_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 = 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
|
||||||
|
# ]
|
||||||
|
# ]
|
||||||
|
|
||||||
|
new_artist_list = []
|
||||||
|
|
||||||
|
for artist in unique_artists:
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
# Update track counts
|
|
||||||
for artist in artists_checked:
|
|
||||||
# Have tracks are selected from tracks table and not all tracks because of duplicates
|
# 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
|
# 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 = (
|
havetracks = (
|
||||||
len(myDB.select(
|
len(myDB.select(
|
||||||
'SELECT TrackTitle from tracks WHERE ArtistName like ? AND Location IS NOT NULL',
|
'SELECT ArtistID From tracks WHERE ArtistID = ? AND Location IS NOT NULL',
|
||||||
[artist])) + len(myDB.select(
|
[artistid])) + len(myDB.select(
|
||||||
'SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"',
|
'SELECT ArtistName FROM have WHERE ArtistName LIKE ' + artist_lookup + ' AND Matched = "Failed"'))
|
||||||
[artist]))
|
|
||||||
)
|
)
|
||||||
|
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
|
# 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)
|
# (can fix by getting rid of second len statement)
|
||||||
myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistName=?', [havetracks, artist])
|
|
||||||
|
|
||||||
logger.info('Found %i new artists' % len(artist_list))
|
if havetracks:
|
||||||
|
myDB.action('UPDATE artists SET HaveTracks = ? WHERE ArtistID = ?', [havetracks, artistid])
|
||||||
|
|
||||||
if artist_list:
|
# 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:
|
if headphones.CONFIG.AUTO_ADD_ARTISTS:
|
||||||
logger.info('Importing %i new artists' % len(artist_list))
|
logger.info('Importing %i new artists' % len(new_artist_list))
|
||||||
importer.artistlist_to_mbids(artist_list)
|
importer.artistlist_to_mbids(new_artist_list)
|
||||||
else:
|
else:
|
||||||
logger.info('To add these artists, go to Manage->Manage New Artists')
|
logger.info('To add these artists, go to Manage->Manage New Artists')
|
||||||
# myDB.action('DELETE from newartists')
|
# 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])
|
myDB.action('INSERT OR IGNORE INTO newartists VALUES (?)', [artist])
|
||||||
|
|
||||||
if headphones.CONFIG.DETECT_BITRATE and bitrates:
|
if headphones.CONFIG.DETECT_BITRATE and bitrates:
|
||||||
@@ -379,51 +443,79 @@ 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
|
# If we're appending a new album to the database, update the artists total track counts
|
||||||
logger.info('Updating artist track counts')
|
logger.info('Updating artist track counts')
|
||||||
|
|
||||||
|
artist_lookup = "\"" + ArtistName.replace("\"", "\"\"") + "\""
|
||||||
|
try:
|
||||||
havetracks = len(
|
havetracks = len(
|
||||||
myDB.select('SELECT TrackTitle from tracks WHERE ArtistID=? AND Location IS NOT NULL',
|
myDB.select('SELECT ArtistID FROM tracks WHERE ArtistID = ? AND Location IS NOT NULL',
|
||||||
[ArtistID])) + len(myDB.select(
|
[ArtistID])) + len(myDB.select(
|
||||||
'SELECT TrackTitle from have WHERE ArtistName like ? AND Matched = "Failed"',
|
'SELECT ArtistName FROM have WHERE ArtistName LIKE ' + artist_lookup + ' AND Matched = "Failed"'))
|
||||||
[ArtistName]))
|
except Exception as e:
|
||||||
|
logger.warn('Error updating counts for artist: %s: %s' % (ArtistName, e))
|
||||||
|
|
||||||
|
if havetracks:
|
||||||
myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistID=?', [havetracks, ArtistID])
|
myDB.action('UPDATE artists SET HaveTracks=? WHERE ArtistID=?', [havetracks, ArtistID])
|
||||||
|
|
||||||
if not append:
|
# Moved above to call for each artist
|
||||||
update_album_status()
|
# if not append:
|
||||||
|
# update_album_status()
|
||||||
|
|
||||||
if not append and not artistScan:
|
if not append and not artistScan:
|
||||||
lastfm.getSimilar()
|
lastfm.getSimilar()
|
||||||
|
|
||||||
|
if ArtistName:
|
||||||
|
logger.info('Scanning complete for artist: %s', ArtistName)
|
||||||
|
else:
|
||||||
logger.info('Library scan complete')
|
logger.info('Library scan complete')
|
||||||
|
|
||||||
|
|
||||||
# ADDED THIS SECTION TO MARK ALBUMS AS DOWNLOADED IF ARTISTS ARE ADDED EN MASSE BEFORE LIBRARY IS SCANNED
|
# 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()
|
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:
|
if AlbumID:
|
||||||
album_status_updater = myDB.action(
|
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:
|
else:
|
||||||
album_status_updater = myDB.action('SELECT AlbumID, AlbumTitle, Status from albums')
|
album_status_updater = myDB.action(
|
||||||
for album in album_status_updater:
|
'SELECT'
|
||||||
track_counter = myDB.action('SELECT Location from tracks where AlbumID=?',
|
' a.AlbumID, a.ArtistID, a.ArtistName, a.AlbumTitle, a.Status, AVG(t.Location IS NOT NULL) * 100 AS album_completion '
|
||||||
[album['AlbumID']])
|
'FROM'
|
||||||
total_tracks = 0
|
' albums AS a '
|
||||||
have_tracks = 0
|
'JOIN tracks AS t ON t.AlbumID = a.AlbumID '
|
||||||
for track in track_counter:
|
'WHERE'
|
||||||
total_tracks += 1
|
' a.ArtistID = ? AND a.Status != "Downloaded" '
|
||||||
if track['Location']:
|
'GROUP BY'
|
||||||
have_tracks += 1
|
' a.AlbumID '
|
||||||
if total_tracks != 0:
|
'HAVING'
|
||||||
album_completion = float(float(have_tracks) / float(total_tracks)) * 100
|
' AVG(t.Location IS NOT NULL) * 100 >= ?',
|
||||||
else:
|
[ArtistID, headphones.CONFIG.ALBUM_COMPLETION_PCT]
|
||||||
album_completion = 0
|
)
|
||||||
logger.info('Album %s does not have any tracks in database' % album['AlbumTitle'])
|
|
||||||
|
|
||||||
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 don't think we want to change Downloaded->Skipped.....
|
||||||
# I think we can only automatically change Skipped->Downloaded when updating
|
# I think we can only automatically change Skipped->Downloaded when updating
|
||||||
# There was a bug report where this was causing infinite downloads if the album was
|
# There was a bug report where this was causing infinite downloads if the album was
|
||||||
@@ -433,10 +525,13 @@ def update_album_status(AlbumID=None):
|
|||||||
# new_album_status = "Skipped"
|
# new_album_status = "Skipped"
|
||||||
# else:
|
# else:
|
||||||
# new_album_status = album['Status']
|
# new_album_status = album['Status']
|
||||||
else:
|
# else:
|
||||||
new_album_status = album['Status']
|
# 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']})
|
myDB.action('UPDATE albums SET Status = ? WHERE AlbumID = ?', [new_album_status, album[0]])
|
||||||
if new_album_status != album['Status']:
|
logger.info('Album: %s - %s. Status updated to %s' % (album[1], album[2], new_album_status))
|
||||||
logger.info('Album %s changed to %s' % (album['AlbumTitle'], new_album_status))
|
|
||||||
logger.info('Album status update complete')
|
|
||||||
|
|||||||
@@ -793,3 +793,27 @@ def getArtistForReleaseGroup(rgid):
|
|||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
return releaseGroup['artist-credit'][0]['artist']['name']
|
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"
|
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
|
rows = None
|
||||||
|
|
||||||
|
|||||||
@@ -289,6 +289,12 @@ def album_metadata(path, release, common_tags):
|
|||||||
first_char = sort_name[0]
|
first_char = sort_name[0]
|
||||||
|
|
||||||
orig_folder = u''
|
orig_folder = u''
|
||||||
|
|
||||||
|
# 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):
|
for r, d, f in os.walk(path):
|
||||||
try:
|
try:
|
||||||
orig_folder = os.path.basename(
|
orig_folder = os.path.basename(
|
||||||
|
|||||||
@@ -168,8 +168,8 @@ class MetadataTest(TestCase):
|
|||||||
if not _h.SYS_ENCODING:
|
if not _h.SYS_ENCODING:
|
||||||
_h.SYS_ENCODING = 'UTF-8'
|
_h.SYS_ENCODING = 'UTF-8'
|
||||||
|
|
||||||
res = _hp.replace_all(
|
res = _hp.pattern_substitute(
|
||||||
"/music/$First/$Artist/$Artist - $Album{ [$Year]}", md, True)
|
"/music/$First/$Artist/$Artist - $Album{ [$Year]}", md, True)
|
||||||
|
|
||||||
self.assertEqual(res, u"/music/A/artist/artist - Album",
|
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()")
|
||||||
|
|||||||
+68
-11
@@ -23,6 +23,7 @@ import email.utils
|
|||||||
from httplib import HTTPSConnection
|
from httplib import HTTPSConnection
|
||||||
from urlparse import parse_qsl
|
from urlparse import parse_qsl
|
||||||
import urllib2
|
import urllib2
|
||||||
|
import requests as requests
|
||||||
|
|
||||||
import os.path
|
import os.path
|
||||||
from headphones import logger, helpers, common, request
|
from headphones import logger, helpers, common, request
|
||||||
@@ -365,6 +366,41 @@ class Plex(object):
|
|||||||
|
|
||||||
def update(self):
|
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
|
# 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
|
# directory or per path basis so need to update the whole thing
|
||||||
|
|
||||||
@@ -379,7 +415,11 @@ class Plex(object):
|
|||||||
else:
|
else:
|
||||||
params = False
|
params = False
|
||||||
|
|
||||||
|
try:
|
||||||
r = request.request_minidom(url, params=params)
|
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')
|
||||||
|
|
||||||
@@ -393,6 +433,10 @@ class Plex(object):
|
|||||||
host, s.getAttribute('key'))
|
host, s.getAttribute('key'))
|
||||||
request.request_response(url, params=params)
|
request.request_response(url, params=params)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Error getting Plex Media Server details: %s" % e)
|
||||||
|
return False
|
||||||
|
|
||||||
def notify(self, artist, album, albumartpath):
|
def notify(self, artist, album, albumartpath):
|
||||||
|
|
||||||
hosts = [x.strip() for x in self.client_hosts.split(',')]
|
hosts = [x.strip() for x in self.client_hosts.split(',')]
|
||||||
@@ -817,7 +861,7 @@ class OSX_NOTIFY(object):
|
|||||||
self.AppKit = __import__("AppKit")
|
self.AppKit = __import__("AppKit")
|
||||||
except:
|
except:
|
||||||
logger.warn('OS X Notification: Cannot import objc or AppKit')
|
logger.warn('OS X Notification: Cannot import objc or AppKit')
|
||||||
return False
|
pass
|
||||||
|
|
||||||
def swizzle(self, cls, SEL, func):
|
def swizzle(self, cls, SEL, func):
|
||||||
old_IMP = getattr(cls, SEL, None)
|
old_IMP = getattr(cls, SEL, None)
|
||||||
@@ -941,6 +985,7 @@ class Email(object):
|
|||||||
message['From'] = email.utils.formataddr(
|
message['From'] = email.utils.formataddr(
|
||||||
('Headphones', headphones.CONFIG.EMAIL_FROM))
|
('Headphones', headphones.CONFIG.EMAIL_FROM))
|
||||||
message['To'] = headphones.CONFIG.EMAIL_TO
|
message['To'] = headphones.CONFIG.EMAIL_TO
|
||||||
|
message['Date'] = email.utils.formatdate(localtime=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if headphones.CONFIG.EMAIL_SSL:
|
if headphones.CONFIG.EMAIL_SSL:
|
||||||
@@ -972,7 +1017,7 @@ class Email(object):
|
|||||||
|
|
||||||
|
|
||||||
class TELEGRAM(object):
|
class TELEGRAM(object):
|
||||||
def notify(self, message, status):
|
def notify(self, message, status, rgid=None, image=None):
|
||||||
if not headphones.CONFIG.TELEGRAM_ENABLED:
|
if not headphones.CONFIG.TELEGRAM_ENABLED:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -985,22 +1030,34 @@ class TELEGRAM(object):
|
|||||||
userid = headphones.CONFIG.TELEGRAM_USERID
|
userid = headphones.CONFIG.TELEGRAM_USERID
|
||||||
|
|
||||||
# Construct message
|
# Construct message
|
||||||
payload = {'chat_id': userid, 'text': status + ': ' + message}
|
message = '\n\n' + message
|
||||||
|
|
||||||
# Send message to user using Telegram's Bot API
|
# 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:
|
try:
|
||||||
response = requests.post(TELEGRAM_API % (token, "sendMessage"),
|
response = requests.post(TELEGRAM_API % (token, "sendPhoto"), data=payload, files=image_file)
|
||||||
data=payload)
|
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:
|
except Exception, e:
|
||||||
logger.info(u'Telegram notify failed: ' + str(e))
|
logger.info(u'Telegram notify failed: ' + str(e))
|
||||||
|
|
||||||
# Error logging
|
# Error logging
|
||||||
sent_successfuly = True
|
sent_successfuly = True
|
||||||
if not response.status_code == 200:
|
if response and not response.status_code == 200:
|
||||||
logger.info(
|
logger.info("Could not send notification to TelegramBot (token=%s). Response: [%s]", token, response.text)
|
||||||
u'Could not send notification to TelegramBot '
|
|
||||||
u'(token=%s). Response: [%s]',
|
|
||||||
(token, response.text))
|
|
||||||
sent_successfuly = False
|
sent_successfuly = False
|
||||||
|
|
||||||
logger.info(u"Telegram notifications sent.")
|
logger.info(u"Telegram notifications sent.")
|
||||||
|
|||||||
@@ -183,13 +183,17 @@ def verify(albumid, albumpath, Kind=None, forced=False, keep_original_folder=Fal
|
|||||||
controlValueDict = {"TrackID": track['id'],
|
controlValueDict = {"TrackID": track['id'],
|
||||||
"AlbumID": albumid}
|
"AlbumID": albumid}
|
||||||
|
|
||||||
|
clean_name = helpers.clean_name(
|
||||||
|
release_dict['artist_name'] + ' ' + release_dict['title'] + ' ' + track['title'])
|
||||||
|
|
||||||
newValueDict = {"ArtistID": release_dict['artist_id'],
|
newValueDict = {"ArtistID": release_dict['artist_id'],
|
||||||
"ArtistName": release_dict['artist_name'],
|
"ArtistName": release_dict['artist_name'],
|
||||||
"AlbumTitle": release_dict['title'],
|
"AlbumTitle": release_dict['title'],
|
||||||
"AlbumASIN": release_dict['asin'],
|
"AlbumASIN": release_dict['asin'],
|
||||||
"TrackTitle": track['title'],
|
"TrackTitle": track['title'],
|
||||||
"TrackDuration": track['duration'],
|
"TrackDuration": track['duration'],
|
||||||
"TrackNumber": track['number']
|
"TrackNumber": track['number'],
|
||||||
|
"CleanName": clean_name
|
||||||
}
|
}
|
||||||
|
|
||||||
myDB.upsert("tracks", newValueDict, controlValueDict)
|
myDB.upsert("tracks", newValueDict, controlValueDict)
|
||||||
@@ -582,7 +586,7 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list,
|
|||||||
if headphones.CONFIG.TELEGRAM_ENABLED:
|
if headphones.CONFIG.TELEGRAM_ENABLED:
|
||||||
logger.info(u"Telegram request")
|
logger.info(u"Telegram request")
|
||||||
telegram = notifiers.TELEGRAM()
|
telegram = notifiers.TELEGRAM()
|
||||||
telegram.notify(pushmessage, statusmessage)
|
telegram.notify(statusmessage, pushmessage)
|
||||||
|
|
||||||
if headphones.CONFIG.TWITTER_ENABLED:
|
if headphones.CONFIG.TWITTER_ENABLED:
|
||||||
logger.info(u"Sending Twitter notification")
|
logger.info(u"Sending Twitter notification")
|
||||||
@@ -656,7 +660,7 @@ def addAlbumArt(artwork, albumpath, release, metadata_dict):
|
|||||||
if artwork[:4] == '\x89PNG':
|
if artwork[:4] == '\x89PNG':
|
||||||
ext = ".png"
|
ext = ".png"
|
||||||
|
|
||||||
album_art_name = helpers.replace_all(
|
album_art_name = helpers.pattern_substitute(
|
||||||
headphones.CONFIG.ALBUM_ART_FORMAT.strip(), md) + ext
|
headphones.CONFIG.ALBUM_ART_FORMAT.strip(), md) + ext
|
||||||
|
|
||||||
album_art_name = helpers.replace_illegal_chars(album_art_name).encode(
|
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)
|
logger.info("Moving files: %s" % albumpath)
|
||||||
|
|
||||||
md = metadata.album_metadata(albumpath, release, metadata_dict)
|
md = metadata.album_metadata(albumpath, release, metadata_dict)
|
||||||
folder = helpers.replace_all(
|
folder = helpers.pattern_substitute(
|
||||||
headphones.CONFIG.FOLDER_FORMAT.strip(), md, normalize=True)
|
headphones.CONFIG.FOLDER_FORMAT.strip(), md, normalize=True)
|
||||||
|
|
||||||
if headphones.CONFIG.FILE_UNDERSCORES:
|
if headphones.CONFIG.FILE_UNDERSCORES:
|
||||||
@@ -1091,7 +1095,7 @@ def renameFiles(albumpath, downloaded_track_list, release):
|
|||||||
title = md[metadata.Vars.TITLE]
|
title = md[metadata.Vars.TITLE]
|
||||||
new_file_name = helpers.cleanTitle(title) + ext
|
new_file_name = helpers.cleanTitle(title) + ext
|
||||||
else:
|
else:
|
||||||
new_file_name = helpers.replace_all(
|
new_file_name = helpers.pattern_substitute(
|
||||||
headphones.CONFIG.FILE_FORMAT.strip(), md
|
headphones.CONFIG.FILE_FORMAT.strip(), md
|
||||||
).replace('/', '_') + ext
|
).replace('/', '_') + ext
|
||||||
|
|
||||||
|
|||||||
Regular → Executable
+72
-8
@@ -28,6 +28,8 @@ import headphones
|
|||||||
from headphones import logger
|
from headphones import logger
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
|
from qbittorrentv2 import Client
|
||||||
|
|
||||||
|
|
||||||
class qbittorrentclient(object):
|
class qbittorrentclient(object):
|
||||||
|
|
||||||
@@ -49,9 +51,20 @@ class qbittorrentclient(object):
|
|||||||
self.base_url = host
|
self.base_url = host
|
||||||
self.username = headphones.CONFIG.QBITTORRENT_USERNAME
|
self.username = headphones.CONFIG.QBITTORRENT_USERNAME
|
||||||
self.password = headphones.CONFIG.QBITTORRENT_PASSWORD
|
self.password = headphones.CONFIG.QBITTORRENT_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.cookiejar = cookielib.CookieJar()
|
||||||
self.opener = self._make_opener()
|
self.opener = self._make_opener()
|
||||||
self._get_sid(self.base_url, self.username, self.password)
|
self._get_sid(self.base_url, self.username, self.password)
|
||||||
|
self.version = 1
|
||||||
|
|
||||||
def _make_opener(self):
|
def _make_opener(self):
|
||||||
# create opener with cookie handler to carry QBitTorrent SID cookie
|
# create opener with cookie handler to carry QBitTorrent SID cookie
|
||||||
@@ -161,19 +174,32 @@ class qbittorrentclient(object):
|
|||||||
|
|
||||||
|
|
||||||
def removeTorrent(hash, remove_data=False):
|
def removeTorrent(hash, remove_data=False):
|
||||||
|
|
||||||
logger.debug('removeTorrent(%s,%s)' % (hash, remove_data))
|
logger.debug('removeTorrent(%s,%s)' % (hash, remove_data))
|
||||||
|
|
||||||
qbclient = qbittorrentclient()
|
qbclient = qbittorrentclient()
|
||||||
status, torrentList = qbclient._get_list()
|
if qbclient.version == 2:
|
||||||
for torrent in torrentList:
|
torrentlist = qbclient.qb.torrents(hashes=hash.lower())
|
||||||
if torrent['hash'].upper() == hash.upper():
|
else:
|
||||||
if torrent['state'] == 'uploading' or torrent['state'] == 'stalledUP':
|
status, torrentlist = qbclient._get_list()
|
||||||
logger.info('%s has finished seeding, removing torrent and data' % torrent['name'])
|
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)
|
qbclient.remove(hash, remove_data)
|
||||||
return True
|
return True
|
||||||
else:
|
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
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -182,6 +208,10 @@ def addTorrent(link):
|
|||||||
logger.debug('addTorrent(%s)' % link)
|
logger.debug('addTorrent(%s)' % link)
|
||||||
|
|
||||||
qbclient = qbittorrentclient()
|
qbclient = qbittorrentclient()
|
||||||
|
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}
|
args = {'urls': link, 'savepath': headphones.CONFIG.DOWNLOAD_TORRENT_DIR}
|
||||||
if headphones.CONFIG.QBITTORRENT_LABEL:
|
if headphones.CONFIG.QBITTORRENT_LABEL:
|
||||||
args['category'] = headphones.CONFIG.QBITTORRENT_LABEL
|
args['category'] = headphones.CONFIG.QBITTORRENT_LABEL
|
||||||
@@ -193,8 +223,11 @@ def addFile(data):
|
|||||||
logger.debug('addFile(data)')
|
logger.debug('addFile(data)')
|
||||||
|
|
||||||
qbclient = qbittorrentclient()
|
qbclient = qbittorrentclient()
|
||||||
|
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}}
|
files = {'torrents': {'filename': '', 'content': data}}
|
||||||
|
|
||||||
return qbclient._command('command/upload', filelist=files)
|
return qbclient._command('command/upload', filelist=files)
|
||||||
|
|
||||||
|
|
||||||
@@ -206,6 +239,9 @@ def getName(hash):
|
|||||||
tries = 1
|
tries = 1
|
||||||
while tries <= 6:
|
while tries <= 6:
|
||||||
time.sleep(10)
|
time.sleep(10)
|
||||||
|
if qbclient.version == 2:
|
||||||
|
torrentlist = qbclient.qb.torrents(hashes=hash.lower())
|
||||||
|
else:
|
||||||
status, torrentlist = qbclient._get_list()
|
status, torrentlist = qbclient._get_list()
|
||||||
for torrent in torrentlist:
|
for torrent in torrentlist:
|
||||||
if torrent['hash'].lower() == hash.lower():
|
if torrent['hash'].lower() == hash.lower():
|
||||||
@@ -224,6 +260,9 @@ def getFolder(hash):
|
|||||||
qbclient = qbittorrentclient()
|
qbclient = qbittorrentclient()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if qbclient.version == 2:
|
||||||
|
torrent_files = qbclient.qb.get_torrent_files(hash.lower())
|
||||||
|
else:
|
||||||
status, torrent_files = qbclient.getfiles(hash.lower())
|
status, torrent_files = qbclient.getfiles(hash.lower())
|
||||||
if torrent_files:
|
if torrent_files:
|
||||||
if len(torrent_files) == 1:
|
if len(torrent_files) == 1:
|
||||||
@@ -231,6 +270,7 @@ def getFolder(hash):
|
|||||||
single_file = True
|
single_file = True
|
||||||
else:
|
else:
|
||||||
torrent_folder = os.path.split(torrent_files[0]['name'])[0]
|
torrent_folder = os.path.split(torrent_files[0]['name'])[0]
|
||||||
|
torrent_folder = torrent_folder.split(os.sep)[0]
|
||||||
single_file = False
|
single_file = False
|
||||||
except:
|
except:
|
||||||
torrent_folder = None
|
torrent_folder = None
|
||||||
@@ -239,6 +279,30 @@ def getFolder(hash):
|
|||||||
return torrent_folder, single_file
|
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
|
_BOUNDARY_CHARS = string.digits + string.ascii_letters
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -87,10 +87,10 @@ class Rutracker(object):
|
|||||||
searchterm = searchterm + year
|
searchterm = searchterm + year
|
||||||
|
|
||||||
if format == 'lossless':
|
if format == 'lossless':
|
||||||
format = '+lossless'
|
format = '+lossless||TR24'
|
||||||
self.maxsize = 10000000000
|
self.maxsize = 10000000000
|
||||||
elif format == 'lossless+mp3':
|
elif format == 'lossless+mp3':
|
||||||
format = '+lossless||mp3||aac'
|
format = '+lossless||TR24||mp3||aac'
|
||||||
self.maxsize = 10000000000
|
self.maxsize = 10000000000
|
||||||
else:
|
else:
|
||||||
format = '+mp3||aac'
|
format = '+mp3||aac'
|
||||||
@@ -104,6 +104,7 @@ class Rutracker(object):
|
|||||||
searchterm = searchterm.encode('utf-8')
|
searchterm = searchterm.encode('utf-8')
|
||||||
searchurl = "%s?nm=%s%s%s" % (self.search_referer, urllib.quote(searchterm), format, sort)
|
searchurl = "%s?nm=%s%s%s" % (self.search_referer, urllib.quote(searchterm), format, sort)
|
||||||
logger.info("Searching rutracker using term: %s", searchterm)
|
logger.info("Searching rutracker using term: %s", searchterm)
|
||||||
|
|
||||||
return searchurl
|
return searchurl
|
||||||
|
|
||||||
def search(self, searchurl):
|
def search(self, searchurl):
|
||||||
|
|||||||
+95
-306
@@ -14,7 +14,6 @@
|
|||||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
# NZBGet support added by CurlyMo <curlymoo1@gmail.com> as a part of XBian - XBMC on the Raspberry Pi
|
# NZBGet support added by CurlyMo <curlymoo1@gmail.com> as a part of XBian - XBMC on the Raspberry Pi
|
||||||
# t411 support added by a1ex, @likeitneverwentaway on github for maintenance
|
|
||||||
|
|
||||||
from base64 import b16encode, b32decode
|
from base64 import b16encode, b32decode
|
||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
@@ -25,7 +24,6 @@ import datetime
|
|||||||
import subprocess
|
import subprocess
|
||||||
import unicodedata
|
import unicodedata
|
||||||
import urlparse
|
import urlparse
|
||||||
from json import loads
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@@ -41,14 +39,13 @@ from bencode import bencode, bdecode
|
|||||||
|
|
||||||
# Magnet to torrent services, for Black hole. Stolen from CouchPotato.
|
# Magnet to torrent services, for Black hole. Stolen from CouchPotato.
|
||||||
TORRENT_TO_MAGNET_SERVICES = [
|
TORRENT_TO_MAGNET_SERVICES = [
|
||||||
# 'https://zoink.it/torrent/%s.torrent',
|
|
||||||
# 'http://torrage.com/torrent/%s.torrent',
|
|
||||||
# 'https://torcache.net/torrent/%s.torrent',
|
|
||||||
'http://itorrents.org/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
|
# Persistent Orpheus.network API object
|
||||||
apolloobj = None
|
orpheusobj = None
|
||||||
ruobj = None
|
ruobj = None
|
||||||
# Persistent RED API object
|
# Persistent RED API object
|
||||||
redobj = None
|
redobj = None
|
||||||
@@ -156,16 +153,14 @@ def calculate_torrent_hash(link, data=None):
|
|||||||
|
|
||||||
def get_seed_ratio(provider):
|
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.
|
None in case of an error.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if provider == 'rutracker.org':
|
if provider == 'rutracker.org':
|
||||||
seed_ratio = headphones.CONFIG.RUTRACKER_RATIO
|
seed_ratio = headphones.CONFIG.RUTRACKER_RATIO
|
||||||
elif provider == 'Kick Ass Torrents':
|
elif provider == 'Orpheus.network':
|
||||||
seed_ratio = headphones.CONFIG.KAT_RATIO
|
seed_ratio = headphones.CONFIG.ORPHEUS_RATIO
|
||||||
elif provider == 'Apollo.rip':
|
|
||||||
seed_ratio = headphones.CONFIG.APOLLO_RATIO
|
|
||||||
elif provider == 'Redacted':
|
elif provider == 'Redacted':
|
||||||
seed_ratio = headphones.CONFIG.REDACTED_RATIO
|
seed_ratio = headphones.CONFIG.REDACTED_RATIO
|
||||||
elif provider == 'The Pirate Bay':
|
elif provider == 'The Pirate Bay':
|
||||||
@@ -174,10 +169,15 @@ def get_seed_ratio(provider):
|
|||||||
seed_ratio = headphones.CONFIG.OLDPIRATEBAY_RATIO
|
seed_ratio = headphones.CONFIG.OLDPIRATEBAY_RATIO
|
||||||
elif provider == 'Waffles.ch':
|
elif provider == 'Waffles.ch':
|
||||||
seed_ratio = headphones.CONFIG.WAFFLES_RATIO
|
seed_ratio = headphones.CONFIG.WAFFLES_RATIO
|
||||||
elif provider == 'Mininova':
|
elif provider.startswith("Jackett_"):
|
||||||
seed_ratio = headphones.CONFIG.MININOVA_RATIO
|
provider = provider.split("Jackett_")[1]
|
||||||
elif provider == 'Strike':
|
if provider in headphones.CONFIG.TORZNAB_HOST:
|
||||||
seed_ratio = headphones.CONFIG.STRIKE_RATIO
|
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:
|
else:
|
||||||
seed_ratio = None
|
seed_ratio = None
|
||||||
|
|
||||||
@@ -272,16 +272,12 @@ def do_sorted_search(album, new, losslessOnly, choose_specific_download=False):
|
|||||||
headphones.CONFIG.NZBGET_HOST)
|
headphones.CONFIG.NZBGET_HOST)
|
||||||
|
|
||||||
TORRENT_PROVIDERS = (headphones.CONFIG.TORZNAB or
|
TORRENT_PROVIDERS = (headphones.CONFIG.TORZNAB or
|
||||||
headphones.CONFIG.KAT or
|
|
||||||
headphones.CONFIG.PIRATEBAY or
|
headphones.CONFIG.PIRATEBAY or
|
||||||
headphones.CONFIG.OLDPIRATEBAY or
|
headphones.CONFIG.OLDPIRATEBAY or
|
||||||
headphones.CONFIG.MININOVA or
|
|
||||||
headphones.CONFIG.WAFFLES or
|
headphones.CONFIG.WAFFLES or
|
||||||
headphones.CONFIG.RUTRACKER or
|
headphones.CONFIG.RUTRACKER or
|
||||||
headphones.CONFIG.APOLLO or
|
headphones.CONFIG.ORPHEUS or
|
||||||
headphones.CONFIG.REDACTED or
|
headphones.CONFIG.REDACTED)
|
||||||
headphones.CONFIG.STRIKE or
|
|
||||||
headphones.CONFIG.TQUATTRECENTONZE)
|
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
myDB = db.DBConnection()
|
myDB = db.DBConnection()
|
||||||
@@ -440,6 +436,7 @@ def sort_search_results(resultlist, album, new, albumlength):
|
|||||||
|
|
||||||
resultlist = temp_list
|
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:
|
if headphones.CONFIG.PREFERRED_QUALITY == 2 and headphones.CONFIG.PREFERRED_BITRATE:
|
||||||
|
|
||||||
try:
|
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)
|
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):
|
if not len(finallist):
|
||||||
logger.info('No appropriate matches found for %s - %s', album['ArtistName'],
|
logger.info('No appropriate matches found for %s - %s', album['ArtistName'],
|
||||||
album['AlbumTitle'])
|
album['AlbumTitle'])
|
||||||
@@ -841,19 +842,6 @@ def send_to_downloader(data, bestqual, album):
|
|||||||
torrent_name = helpers.replace_illegal_chars(folder_name) + '.torrent'
|
torrent_name = helpers.replace_illegal_chars(folder_name) + '.torrent'
|
||||||
download_path = os.path.join(headphones.CONFIG.TORRENTBLACKHOLE_DIR, torrent_name)
|
download_path = os.path.join(headphones.CONFIG.TORRENTBLACKHOLE_DIR, torrent_name)
|
||||||
|
|
||||||
# Blackhole for t411
|
|
||||||
if bestqual[2].lower().startswith("http://api.t411"):
|
|
||||||
if headphones.CONFIG.MAGNET_LINKS == 2:
|
|
||||||
try:
|
|
||||||
url = bestqual[2].split('TOKEN')[0]
|
|
||||||
token = bestqual[2].split('TOKEN')[1]
|
|
||||||
data = request.request_content(url, headers={'Authorization': token})
|
|
||||||
torrent_to_file(download_path, data)
|
|
||||||
logger.info('Successfully converted magnet to torrent file')
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Error converting magnet link: %s" % str(e))
|
|
||||||
return
|
|
||||||
|
|
||||||
if bestqual[2].lower().startswith("magnet:"):
|
if bestqual[2].lower().startswith("magnet:"):
|
||||||
if headphones.CONFIG.MAGNET_LINKS == 1:
|
if headphones.CONFIG.MAGNET_LINKS == 1:
|
||||||
try:
|
try:
|
||||||
@@ -1019,6 +1007,9 @@ def send_to_downloader(data, bestqual, album):
|
|||||||
|
|
||||||
# Add torrent
|
# Add torrent
|
||||||
if bestqual[3] == 'rutracker.org':
|
if bestqual[3] == 'rutracker.org':
|
||||||
|
if qbittorrent.apiVersion2:
|
||||||
|
qbittorrent.addFile(data)
|
||||||
|
else:
|
||||||
ruobj.qbittorrent_add_file(data)
|
ruobj.qbittorrent_add_file(data)
|
||||||
else:
|
else:
|
||||||
qbittorrent.addTorrent(bestqual[2])
|
qbittorrent.addTorrent(bestqual[2])
|
||||||
@@ -1038,6 +1029,11 @@ def send_to_downloader(data, bestqual, album):
|
|||||||
logger.error('Torrent name could not be determined')
|
logger.error('Torrent name could not be determined')
|
||||||
return
|
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 = db.DBConnection()
|
||||||
myDB.action('UPDATE albums SET status = "Snatched" WHERE AlbumID=?', [album['AlbumID']])
|
myDB.action('UPDATE albums SET status = "Snatched" WHERE AlbumID=?', [album['AlbumID']])
|
||||||
myDB.action('INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?, ?, ?)',
|
myDB.action('INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?, ?, ?)',
|
||||||
@@ -1087,8 +1083,12 @@ def send_to_downloader(data, bestqual, album):
|
|||||||
slack.notify(name, "Download started")
|
slack.notify(name, "Download started")
|
||||||
if headphones.CONFIG.TELEGRAM_ENABLED and headphones.CONFIG.TELEGRAM_ONSNATCH:
|
if headphones.CONFIG.TELEGRAM_ENABLED and headphones.CONFIG.TELEGRAM_ONSNATCH:
|
||||||
logger.info(u"Sending Telegram notification")
|
logger.info(u"Sending Telegram notification")
|
||||||
|
from headphones import cache
|
||||||
|
c = cache.Cache()
|
||||||
|
album_art = c.get_artwork_from_cache(None, rgid)
|
||||||
telegram = notifiers.TELEGRAM()
|
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:
|
if headphones.CONFIG.TWITTER_ENABLED and headphones.CONFIG.TWITTER_ONSNATCH:
|
||||||
logger.info(u"Sending Twitter notification")
|
logger.info(u"Sending Twitter notification")
|
||||||
twitter = notifiers.TwitterNotifier()
|
twitter = notifiers.TwitterNotifier()
|
||||||
@@ -1211,7 +1211,7 @@ def verifyresult(title, artistterm, term, lossless):
|
|||||||
|
|
||||||
def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
||||||
choose_specific_download=False):
|
choose_specific_download=False):
|
||||||
global 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 redobj # persistent redacted api object to reduce number of login attempts
|
||||||
global ruobj # and rutracker
|
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:
|
if headphones.CONFIG.TORZNAB_HOST and headphones.CONFIG.TORZNAB_ENABLED:
|
||||||
torznab_hosts.append((headphones.CONFIG.TORZNAB_HOST, headphones.CONFIG.TORZNAB_APIKEY,
|
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():
|
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)
|
torznab_hosts.append(torznab_host)
|
||||||
|
|
||||||
if headphones.CONFIG.PREFERRED_QUALITY == 3 or losslessOnly:
|
if headphones.CONFIG.PREFERRED_QUALITY == 3 or losslessOnly:
|
||||||
@@ -1353,8 +1353,10 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
|||||||
else:
|
else:
|
||||||
logger.info('Skipping %s, could not determine size' % title)
|
logger.info('Skipping %s, could not determine size' % title)
|
||||||
continue
|
continue
|
||||||
else:
|
elif item.size:
|
||||||
size = int(item.size.string)
|
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 all(word.lower() in title.lower() for word in term.split()):
|
||||||
if size < maxsize and minimumseeders < seeders:
|
if size < maxsize and minimumseeders < seeders:
|
||||||
@@ -1372,66 +1374,6 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
|||||||
logger.exception(
|
logger.exception(
|
||||||
"An unknown error occurred trying to parse the feed: %s" % e)
|
"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:
|
if headphones.CONFIG.WAFFLES:
|
||||||
provider = "Waffles.ch"
|
provider = "Waffles.ch"
|
||||||
providerurl = fix_url("https://waffles.ch/browse.php")
|
providerurl = fix_url("https://waffles.ch/browse.php")
|
||||||
@@ -1534,9 +1476,9 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
|||||||
if rulist:
|
if rulist:
|
||||||
resultlist.extend(rulist)
|
resultlist.extend(rulist)
|
||||||
|
|
||||||
if headphones.CONFIG.APOLLO:
|
if headphones.CONFIG.ORPHEUS:
|
||||||
provider = "Apollo.rip"
|
provider = "Orpheus.network"
|
||||||
providerurl = "http://apollo.rip/"
|
providerurl = "http://orpheus.network/"
|
||||||
|
|
||||||
bitrate = None
|
bitrate = None
|
||||||
bitrate_string = bitrate
|
bitrate_string = bitrate
|
||||||
@@ -1559,7 +1501,7 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
|||||||
bitrate_string = encoding_string
|
bitrate_string = encoding_string
|
||||||
if bitrate_string not in gazelleencoding.ALL_ENCODINGS:
|
if bitrate_string not in gazelleencoding.ALL_ENCODINGS:
|
||||||
logger.info(
|
logger.info(
|
||||||
u"Your preferred bitrate is not one of the available 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
|
maxsize = 10000000000
|
||||||
elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless: # Highest quality including lossless
|
elif headphones.CONFIG.PREFERRED_QUALITY == 1 or allow_lossless: # Highest quality including lossless
|
||||||
search_formats = [gazelleformat.FLAC, gazelleformat.MP3]
|
search_formats = [gazelleformat.FLAC, gazelleformat.MP3]
|
||||||
@@ -1568,22 +1510,24 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
|||||||
search_formats = [gazelleformat.MP3]
|
search_formats = [gazelleformat.MP3]
|
||||||
maxsize = 300000000
|
maxsize = 300000000
|
||||||
|
|
||||||
if not apolloobj or not apolloobj.logged_in():
|
if not orpheusobj or not orpheusobj.logged_in():
|
||||||
try:
|
try:
|
||||||
logger.info(u"Attempting to log in to Apollo.rip...")
|
logger.info(u"Attempting to log in to Orpheus.network...")
|
||||||
apolloobj = gazelleapi.GazelleAPI(headphones.CONFIG.APOLLO_USERNAME,
|
orpheusobj = gazelleapi.GazelleAPI(headphones.CONFIG.ORPHEUS_USERNAME,
|
||||||
headphones.CONFIG.APOLLO_PASSWORD,
|
headphones.CONFIG.ORPHEUS_PASSWORD,
|
||||||
headphones.CONFIG.APOLLO_URL)
|
headphones.CONFIG.ORPHEUS_URL)
|
||||||
apolloobj._login()
|
orpheusobj._login()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
apolloobj = None
|
orpheusobj = None
|
||||||
logger.error(u"Apollo.rip credentials incorrect or site is down. Error: %s %s" % (
|
logger.error(u"Orpheus.network credentials incorrect or site is down. Error: %s %s" % (
|
||||||
e.__class__.__name__, str(e)))
|
e.__class__.__name__, str(e)))
|
||||||
|
|
||||||
if apolloobj and apolloobj.logged_in():
|
if orpheusobj and orpheusobj.logged_in():
|
||||||
logger.info(u"Searching %s..." % provider)
|
logger.info(u"Searching %s..." % provider)
|
||||||
all_torrents = []
|
all_torrents = []
|
||||||
|
|
||||||
|
album_type = ""
|
||||||
|
|
||||||
# Specify release types to filter by
|
# Specify release types to filter by
|
||||||
if album['Type'] == 'Album':
|
if album['Type'] == 'Album':
|
||||||
album_type = [gazellerelease_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]
|
album_type = [gazellerelease_type.INTERVIEW]
|
||||||
if album['Type'] == 'Mixtape/Street':
|
if album['Type'] == 'Mixtape/Street':
|
||||||
album_type = [gazellerelease_type.MIXTAPE]
|
album_type = [gazellerelease_type.MIXTAPE]
|
||||||
|
if album['Type'] == 'Other':
|
||||||
|
album_type = [gazellerelease_type.UNKNOWN]
|
||||||
|
|
||||||
for search_format in search_formats:
|
for search_format in search_formats:
|
||||||
if usersearchterm:
|
if usersearchterm:
|
||||||
all_torrents.extend(
|
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'])
|
encoding=bitrate_string, releasetype=album_type)['results'])
|
||||||
else:
|
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,
|
groupname=semi_clean_album_term,
|
||||||
format=search_format,
|
format=search_format,
|
||||||
encoding=bitrate_string,
|
encoding=bitrate_string,
|
||||||
@@ -1637,13 +1583,11 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
|||||||
elif len(match_torrents) > 1:
|
elif len(match_torrents) > 1:
|
||||||
logger.info(u"Found %d matching releases from %s for %s - %s after filtering" %
|
logger.info(u"Found %d matching releases from %s for %s - %s after filtering" %
|
||||||
(len(match_torrents), provider, artistterm, albumterm))
|
(len(match_torrents), provider, artistterm, albumterm))
|
||||||
logger.info(
|
logger.info('Sorting torrents by number of seeders...')
|
||||||
"Sorting torrents by times snatched and preferred bitrate %s..." % bitrate_string)
|
match_torrents.sort(key=lambda x: int(x.seeders), reverse=True)
|
||||||
match_torrents.sort(key=lambda x: int(x.snatched), reverse=True)
|
|
||||||
if gazelleformat.MP3 in search_formats:
|
if gazelleformat.MP3 in search_formats:
|
||||||
# sort by size after rounding to nearest 10MB...hacky, but will favor highest quality
|
logger.info('Sorting torrents by seeders...')
|
||||||
match_torrents.sort(key=lambda x: int(10 * round(x.size / 1024. / 1024. / 10.)),
|
match_torrents.sort(key=lambda x: int(x.seeders), reverse=True)
|
||||||
reverse=True)
|
|
||||||
if search_formats and None not in search_formats:
|
if search_formats and None not in search_formats:
|
||||||
match_torrents.sort(
|
match_torrents.sort(
|
||||||
key=lambda x: int(search_formats.index(x.format))) # prefer lossless
|
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
|
torrent.group.update_group_data() # will load the file_path for the individual torrents
|
||||||
resultlist.append((torrent.file_path,
|
resultlist.append((torrent.file_path,
|
||||||
torrent.size,
|
torrent.size,
|
||||||
apolloobj.generate_torrent_link(torrent.id),
|
orpheusobj.generate_torrent_link(torrent.id),
|
||||||
provider,
|
provider,
|
||||||
'torrent', True))
|
'torrent', True))
|
||||||
|
|
||||||
@@ -1756,9 +1700,10 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
|||||||
for torrent in match_torrents:
|
for torrent in match_torrents:
|
||||||
if not torrent.file_path:
|
if not torrent.file_path:
|
||||||
torrent.group.update_group_data() # will load the file_path for the individual torrents
|
torrent.group.update_group_data() # will load the file_path for the individual torrents
|
||||||
|
use_token = headphones.CONFIG.REDACTED_USE_FLTOKEN and torrent.can_use_token
|
||||||
resultlist.append((torrent.file_path,
|
resultlist.append((torrent.file_path,
|
||||||
torrent.size,
|
torrent.size,
|
||||||
redobj.generate_torrent_link(torrent.id),
|
redobj.generate_torrent_link(torrent.id, use_token),
|
||||||
provider,
|
provider,
|
||||||
'torrent', True))
|
'torrent', True))
|
||||||
|
|
||||||
@@ -1804,7 +1749,6 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
|||||||
for item in rows:
|
for item in rows:
|
||||||
try:
|
try:
|
||||||
url = None
|
url = None
|
||||||
rightformat = True
|
|
||||||
title = ''.join(item.find("a", {"class": "detLink"}))
|
title = ''.join(item.find("a", {"class": "detLink"}))
|
||||||
seeds = int(''.join(item.find("td", {"align": "right"})))
|
seeds = int(''.join(item.find("td", {"align": "right"})))
|
||||||
|
|
||||||
@@ -1873,7 +1817,6 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
|||||||
try:
|
try:
|
||||||
links = item.select("td.title-row a")
|
links = item.select("td.title-row a")
|
||||||
|
|
||||||
rightformat = True
|
|
||||||
title = links[1].text
|
title = links[1].text
|
||||||
seeds = int(item.select("td.seeders-row")[0].text)
|
seeds = int(item.select("td.seeders-row")[0].text)
|
||||||
url = links[0][
|
url = links[0][
|
||||||
@@ -1895,184 +1838,13 @@ def searchTorrent(album, new=False, losslessOnly=False, albumlength=None,
|
|||||||
logger.error(
|
logger.error(
|
||||||
u"An unknown error occurred in the Old Pirate Bay parser: %s" % e)
|
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
|
# attempt to verify that this isn't a substring result
|
||||||
# when looking for "Foo - Foo" we don't want "Foobar"
|
# when looking for "Foo - Foo" we don't want "Foobar"
|
||||||
# this should be less of an issue when it isn't a self-titled album so we'll only check vs artist
|
# 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)]
|
results = [result for result in resultlist if verifyresult(result[0], artistterm, term, losslessOnly)]
|
||||||
|
|
||||||
# Additional filtering for size etc
|
# Additional filtering for size etc
|
||||||
|
# if results and not choose_specific_download and result[3] != 'Orpheus.network':
|
||||||
if results and not choose_specific_download:
|
if results and not choose_specific_download:
|
||||||
results = more_filtering(results, album, albumlength, new)
|
results = more_filtering(results, album, albumlength, new)
|
||||||
|
|
||||||
@@ -2086,30 +1858,47 @@ def preprocess(resultlist):
|
|||||||
for result in resultlist:
|
for result in resultlist:
|
||||||
if result[4] == 'torrent':
|
if result[4] == 'torrent':
|
||||||
|
|
||||||
|
headers = {}
|
||||||
|
|
||||||
# rutracker always needs the torrent data
|
# rutracker always needs the torrent data
|
||||||
if result[3] == 'rutracker.org':
|
if result[3] == 'rutracker.org':
|
||||||
return ruobj.get_torrent_data(result[2]), result
|
return ruobj.get_torrent_data(result[2]), result
|
||||||
|
|
||||||
# Get out of here if we're using Transmission
|
# Jackett sometimes redirects
|
||||||
if headphones.CONFIG.TORRENT_DOWNLOADER == 1: # if not a magnet link still need the .torrent to generate hash... uTorrent support labeling
|
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
|
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
|
# Get out of here if it's a magnet link
|
||||||
if result[2].lower().startswith("magnet:"):
|
if result[2].lower().startswith("magnet:"):
|
||||||
return True, result
|
return True, result
|
||||||
|
|
||||||
# Download the torrent file
|
# Download the torrent file
|
||||||
headers = {}
|
|
||||||
|
|
||||||
if result[3] == 'Kick Ass Torrents':
|
if result[3] == 'Orpheus.network':
|
||||||
headers['Referer'] = 'https://torcache.net/'
|
|
||||||
headers['User-Agent'] = USER_AGENT
|
|
||||||
elif result[3] == 'Apollo.rip':
|
|
||||||
headers['User-Agent'] = 'Headphones'
|
headers['User-Agent'] = 'Headphones'
|
||||||
elif result[3] == 'Redacted':
|
elif result[3] == 'Redacted':
|
||||||
headers['User-Agent'] = 'Headphones'
|
headers['User-Agent'] = 'Headphones'
|
||||||
elif result[3] == "The Pirate Bay" or result[3] == "Old Pirate Bay":
|
elif result[3] == "The Pirate Bay" or result[3] == "Old Pirate Bay":
|
||||||
headers[
|
headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2243.2 Safari/537.36'
|
||||||
'User-Agent'] = 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2243.2 Safari/537.36'
|
elif jackett_content:
|
||||||
|
return jackett_content, result
|
||||||
|
|
||||||
return request.request_content(url=result[2], headers=headers), result
|
return request.request_content(url=result[2], headers=headers), result
|
||||||
|
|
||||||
if result[4] == 'magnet':
|
if result[4] == 'magnet':
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ def getFolder(torrentid):
|
|||||||
single_file = True
|
single_file = True
|
||||||
else:
|
else:
|
||||||
torrent_folder = os.path.split(torrent_files[0]['name'])[0]
|
torrent_folder = os.path.split(torrent_files[0]['name'])[0]
|
||||||
|
torrent_folder = torrent_folder.split(os.sep)[0]
|
||||||
single_file = False
|
single_file = False
|
||||||
except:
|
except:
|
||||||
torrent_folder = None
|
torrent_folder = None
|
||||||
|
|||||||
+51
-55
@@ -14,7 +14,6 @@
|
|||||||
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
# NZBGet support added by CurlyMo <curlymoo1@gmail.com> as a part of XBian - XBMC on the Raspberry Pi
|
# NZBGet support added by CurlyMo <curlymoo1@gmail.com> as a part of XBian - XBMC on the Raspberry Pi
|
||||||
# t411 support added by a1ex, @likeitneverwentaway on github for maintenance
|
|
||||||
|
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
import threading
|
import threading
|
||||||
@@ -327,7 +326,7 @@ class WebInterface(object):
|
|||||||
'$first': firstchar.lower(),
|
'$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 = helpers.replace_illegal_chars(folder, type="folder")
|
||||||
folder = folder.replace('./', '_/').replace('/.', '/_')
|
folder = folder.replace('./', '_/').replace('/.', '/_')
|
||||||
@@ -641,6 +640,7 @@ class WebInterface(object):
|
|||||||
'SELECT Matched, CleanName, Location, BitRate, Format FROM have WHERE ArtistName=?',
|
'SELECT Matched, CleanName, Location, BitRate, Format FROM have WHERE ArtistName=?',
|
||||||
[existing_artist])
|
[existing_artist])
|
||||||
update_count = 0
|
update_count = 0
|
||||||
|
artist_id = None
|
||||||
for entry in have_tracks:
|
for entry in have_tracks:
|
||||||
old_clean_filename = entry['CleanName']
|
old_clean_filename = entry['CleanName']
|
||||||
if old_clean_filename.startswith(existing_artist_clean):
|
if old_clean_filename.startswith(existing_artist_clean):
|
||||||
@@ -649,31 +649,30 @@ class WebInterface(object):
|
|||||||
myDB.action(
|
myDB.action(
|
||||||
'UPDATE have SET CleanName=? WHERE ArtistName=? AND CleanName=?',
|
'UPDATE have SET CleanName=? WHERE ArtistName=? AND CleanName=?',
|
||||||
[new_clean_filename, existing_artist, old_clean_filename])
|
[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
|
# Attempt to match tracks with new CleanName
|
||||||
match_alltracks = myDB.action(
|
match_alltracks = myDB.action(
|
||||||
'SELECT CleanName from alltracks WHERE CleanName=?',
|
'SELECT CleanName FROM alltracks WHERE CleanName = ?',
|
||||||
[new_clean_filename]).fetchone()
|
[new_clean_filename]).fetchone()
|
||||||
if match_alltracks:
|
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(
|
match_tracks = myDB.action(
|
||||||
'SELECT CleanName, AlbumID from tracks WHERE CleanName=?',
|
'SELECT ArtistID, CleanName, AlbumID FROM tracks WHERE CleanName = ?',
|
||||||
[new_clean_filename]).fetchone()
|
[new_clean_filename]).fetchone()
|
||||||
if match_tracks:
|
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=?',
|
myDB.action('UPDATE have SET Matched="Manual" WHERE CleanName=?',
|
||||||
[new_clean_filename])
|
[new_clean_filename])
|
||||||
update_count += 1
|
update_count += 1
|
||||||
# This was throwing errors and I don't know why, but it seems to be working fine.
|
artist_id = match_tracks['Artist_ID']
|
||||||
# else:
|
|
||||||
# logger.info("There was an error modifying Artist %s. This should not have happened" % existing_artist)
|
|
||||||
logger.info("Manual matching yielded %s new matches for Artist: %s" % (update_count, new_artist))
|
logger.info("Manual matching yielded %s new matches for Artist: %s" % (update_count, new_artist))
|
||||||
if update_count > 0:
|
if artist_id:
|
||||||
librarysync.update_album_status()
|
librarysync.update_album_status(ArtistID=artist_id)
|
||||||
else:
|
else:
|
||||||
logger.info(
|
logger.info(
|
||||||
"Artist %s already named appropriately; nothing to modify" % existing_artist)
|
"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=?',
|
'UPDATE have SET CleanName=? WHERE ArtistName=? AND AlbumTitle=? AND CleanName=?',
|
||||||
[new_clean_filename, existing_artist, existing_album,
|
[new_clean_filename, existing_artist, existing_album,
|
||||||
old_clean_filename])
|
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
|
# Attempt to match tracks with new CleanName
|
||||||
match_alltracks = myDB.action(
|
match_alltracks = myDB.action(
|
||||||
'SELECT CleanName from alltracks WHERE CleanName=?',
|
'SELECT CleanName FROM alltracks WHERE CleanName = ?',
|
||||||
[new_clean_filename]).fetchone()
|
[new_clean_filename]).fetchone()
|
||||||
if match_alltracks:
|
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(
|
match_tracks = myDB.action(
|
||||||
'SELECT CleanName, AlbumID from tracks WHERE CleanName=?',
|
'SELECT CleanName, AlbumID FROM tracks WHERE CleanName = ?',
|
||||||
[new_clean_filename]).fetchone()
|
[new_clean_filename]).fetchone()
|
||||||
if match_tracks:
|
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=?',
|
myDB.action('UPDATE have SET Matched="Manual" WHERE CleanName=?',
|
||||||
[new_clean_filename])
|
[new_clean_filename])
|
||||||
album_id = match_tracks['AlbumID']
|
album_id = match_tracks['AlbumID']
|
||||||
update_count += 1
|
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" % (
|
logger.info("Manual matching yielded %s new matches for Artist: %s / Album: %s" % (
|
||||||
update_count, new_artist, new_album))
|
update_count, new_artist, new_album))
|
||||||
if update_count > 0:
|
if update_count > 0:
|
||||||
@@ -786,6 +784,10 @@ class WebInterface(object):
|
|||||||
album = tracks['AlbumTitle']
|
album = tracks['AlbumTitle']
|
||||||
track_title = tracks['TrackTitle']
|
track_title = tracks['TrackTitle']
|
||||||
if tracks['CleanName'] != original_clean:
|
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(
|
myDB.action(
|
||||||
'UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE CleanName = ?',
|
'UPDATE tracks SET Location=?, BitRate=?, Format=? WHERE CleanName = ?',
|
||||||
[None, None, None, tracks['CleanName']])
|
[None, None, None, tracks['CleanName']])
|
||||||
@@ -797,14 +799,14 @@ class WebInterface(object):
|
|||||||
(original_clean, artist, album, track_title))
|
(original_clean, artist, album, track_title))
|
||||||
update_count += 1
|
update_count += 1
|
||||||
if update_count > 0:
|
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)
|
logger.info("Artist: %s successfully restored to unmatched list" % artist)
|
||||||
|
|
||||||
elif action == "unmatchAlbum":
|
elif action == "unmatchAlbum":
|
||||||
artist = existing_artist
|
artist = existing_artist
|
||||||
album = existing_album
|
album = existing_album
|
||||||
update_clean = myDB.select(
|
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))
|
(artist, album))
|
||||||
update_count = 0
|
update_count = 0
|
||||||
for tracks in update_clean:
|
for tracks in update_clean:
|
||||||
@@ -813,7 +815,7 @@ class WebInterface(object):
|
|||||||
'TrackTitle']).lower()
|
'TrackTitle']).lower()
|
||||||
track_title = tracks['TrackTitle']
|
track_title = tracks['TrackTitle']
|
||||||
if tracks['CleanName'] != original_clean:
|
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()
|
[tracks['CleanName']]).fetchone()
|
||||||
if album_id_check:
|
if album_id_check:
|
||||||
album_id = album_id_check[0]
|
album_id = album_id_check[0]
|
||||||
@@ -1160,7 +1162,7 @@ class WebInterface(object):
|
|||||||
"download_scan_interval": headphones.CONFIG.DOWNLOAD_SCAN_INTERVAL,
|
"download_scan_interval": headphones.CONFIG.DOWNLOAD_SCAN_INTERVAL,
|
||||||
"update_db_interval": headphones.CONFIG.UPDATE_DB_INTERVAL,
|
"update_db_interval": headphones.CONFIG.UPDATE_DB_INTERVAL,
|
||||||
"mb_ignore_age": headphones.CONFIG.MB_IGNORE_AGE,
|
"mb_ignore_age": headphones.CONFIG.MB_IGNORE_AGE,
|
||||||
"mb_ignore_age_missing": headphones.CONFIG.MB_IGNORE_AGE_MISSING,
|
"mb_ignore_age_missing": checked(headphones.CONFIG.MB_IGNORE_AGE_MISSING),
|
||||||
"search_interval": headphones.CONFIG.SEARCH_INTERVAL,
|
"search_interval": headphones.CONFIG.SEARCH_INTERVAL,
|
||||||
"libraryscan_interval": headphones.CONFIG.LIBRARYSCAN_INTERVAL,
|
"libraryscan_interval": headphones.CONFIG.LIBRARYSCAN_INTERVAL,
|
||||||
"sab_host": headphones.CONFIG.SAB_HOST,
|
"sab_host": headphones.CONFIG.SAB_HOST,
|
||||||
@@ -1211,6 +1213,7 @@ class WebInterface(object):
|
|||||||
"use_torznab": checked(headphones.CONFIG.TORZNAB),
|
"use_torznab": checked(headphones.CONFIG.TORZNAB),
|
||||||
"torznab_host": headphones.CONFIG.TORZNAB_HOST,
|
"torznab_host": headphones.CONFIG.TORZNAB_HOST,
|
||||||
"torznab_apikey": headphones.CONFIG.TORZNAB_APIKEY,
|
"torznab_apikey": headphones.CONFIG.TORZNAB_APIKEY,
|
||||||
|
"torznab_ratio": headphones.CONFIG.TORZNAB_RATIO,
|
||||||
"torznab_enabled": checked(headphones.CONFIG.TORZNAB_ENABLED),
|
"torznab_enabled": checked(headphones.CONFIG.TORZNAB_ENABLED),
|
||||||
"extra_torznabs": headphones.CONFIG.get_extra_torznabs(),
|
"extra_torznabs": headphones.CONFIG.get_extra_torznabs(),
|
||||||
"use_nzbsorg": checked(headphones.CONFIG.NZBSORG),
|
"use_nzbsorg": checked(headphones.CONFIG.NZBSORG),
|
||||||
@@ -1226,17 +1229,12 @@ class WebInterface(object):
|
|||||||
"torrentblackhole_dir": headphones.CONFIG.TORRENTBLACKHOLE_DIR,
|
"torrentblackhole_dir": headphones.CONFIG.TORRENTBLACKHOLE_DIR,
|
||||||
"download_torrent_dir": headphones.CONFIG.DOWNLOAD_TORRENT_DIR,
|
"download_torrent_dir": headphones.CONFIG.DOWNLOAD_TORRENT_DIR,
|
||||||
"numberofseeders": headphones.CONFIG.NUMBEROFSEEDERS,
|
"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),
|
"use_piratebay": checked(headphones.CONFIG.PIRATEBAY),
|
||||||
"piratebay_proxy_url": headphones.CONFIG.PIRATEBAY_PROXY_URL,
|
"piratebay_proxy_url": headphones.CONFIG.PIRATEBAY_PROXY_URL,
|
||||||
"piratebay_ratio": headphones.CONFIG.PIRATEBAY_RATIO,
|
"piratebay_ratio": headphones.CONFIG.PIRATEBAY_RATIO,
|
||||||
"use_oldpiratebay": checked(headphones.CONFIG.OLDPIRATEBAY),
|
"use_oldpiratebay": checked(headphones.CONFIG.OLDPIRATEBAY),
|
||||||
"oldpiratebay_url": headphones.CONFIG.OLDPIRATEBAY_URL,
|
"oldpiratebay_url": headphones.CONFIG.OLDPIRATEBAY_URL,
|
||||||
"oldpiratebay_ratio": headphones.CONFIG.OLDPIRATEBAY_RATIO,
|
"oldpiratebay_ratio": headphones.CONFIG.OLDPIRATEBAY_RATIO,
|
||||||
"use_mininova": checked(headphones.CONFIG.MININOVA),
|
|
||||||
"mininova_ratio": headphones.CONFIG.MININOVA_RATIO,
|
|
||||||
"use_waffles": checked(headphones.CONFIG.WAFFLES),
|
"use_waffles": checked(headphones.CONFIG.WAFFLES),
|
||||||
"waffles_uid": headphones.CONFIG.WAFFLES_UID,
|
"waffles_uid": headphones.CONFIG.WAFFLES_UID,
|
||||||
"waffles_passkey": headphones.CONFIG.WAFFLES_PASSKEY,
|
"waffles_passkey": headphones.CONFIG.WAFFLES_PASSKEY,
|
||||||
@@ -1246,20 +1244,16 @@ class WebInterface(object):
|
|||||||
"rutracker_password": headphones.CONFIG.RUTRACKER_PASSWORD,
|
"rutracker_password": headphones.CONFIG.RUTRACKER_PASSWORD,
|
||||||
"rutracker_ratio": headphones.CONFIG.RUTRACKER_RATIO,
|
"rutracker_ratio": headphones.CONFIG.RUTRACKER_RATIO,
|
||||||
"rutracker_cookie": headphones.CONFIG.RUTRACKER_COOKIE,
|
"rutracker_cookie": headphones.CONFIG.RUTRACKER_COOKIE,
|
||||||
"use_apollo": checked(headphones.CONFIG.APOLLO),
|
"use_orpheus": checked(headphones.CONFIG.ORPHEUS),
|
||||||
"apollo_username": headphones.CONFIG.APOLLO_USERNAME,
|
"orpheus_username": headphones.CONFIG.ORPHEUS_USERNAME,
|
||||||
"apollo_password": headphones.CONFIG.APOLLO_PASSWORD,
|
"orpheus_password": headphones.CONFIG.ORPHEUS_PASSWORD,
|
||||||
"apollo_ratio": headphones.CONFIG.APOLLO_RATIO,
|
"orpheus_ratio": headphones.CONFIG.ORPHEUS_RATIO,
|
||||||
"apollo_url": headphones.CONFIG.APOLLO_URL,
|
"orpheus_url": headphones.CONFIG.ORPHEUS_URL,
|
||||||
"use_redacted": checked(headphones.CONFIG.REDACTED),
|
"use_redacted": checked(headphones.CONFIG.REDACTED),
|
||||||
"redacted_username": headphones.CONFIG.REDACTED_USERNAME,
|
"redacted_username": headphones.CONFIG.REDACTED_USERNAME,
|
||||||
"redacted_password": headphones.CONFIG.REDACTED_PASSWORD,
|
"redacted_password": headphones.CONFIG.REDACTED_PASSWORD,
|
||||||
"redacted_ratio": headphones.CONFIG.REDACTED_RATIO,
|
"redacted_ratio": headphones.CONFIG.REDACTED_RATIO,
|
||||||
"use_strike": checked(headphones.CONFIG.STRIKE),
|
"redacted_use_fltoken": checked(headphones.CONFIG.REDACTED_USE_FLTOKEN),
|
||||||
"strike_ratio": headphones.CONFIG.STRIKE_RATIO,
|
|
||||||
"use_tquattrecentonze": checked(headphones.CONFIG.TQUATTRECENTONZE),
|
|
||||||
"tquattrecentonze_user": headphones.CONFIG.TQUATTRECENTONZE_USER,
|
|
||||||
"tquattrecentonze_password": headphones.CONFIG.TQUATTRECENTONZE_PASSWORD,
|
|
||||||
"pref_qual_0": radio(headphones.CONFIG.PREFERRED_QUALITY, 0),
|
"pref_qual_0": radio(headphones.CONFIG.PREFERRED_QUALITY, 0),
|
||||||
"pref_qual_1": radio(headphones.CONFIG.PREFERRED_QUALITY, 1),
|
"pref_qual_1": radio(headphones.CONFIG.PREFERRED_QUALITY, 1),
|
||||||
"pref_qual_2": radio(headphones.CONFIG.PREFERRED_QUALITY, 2),
|
"pref_qual_2": radio(headphones.CONFIG.PREFERRED_QUALITY, 2),
|
||||||
@@ -1466,9 +1460,9 @@ class WebInterface(object):
|
|||||||
checked_configs = [
|
checked_configs = [
|
||||||
"launch_browser", "enable_https", "api_enabled", "use_blackhole", "headphones_indexer",
|
"launch_browser", "enable_https", "api_enabled", "use_blackhole", "headphones_indexer",
|
||||||
"use_newznab", "newznab_enabled", "use_torznab", "torznab_enabled",
|
"use_newznab", "newznab_enabled", "use_torznab", "torznab_enabled",
|
||||||
"use_nzbsorg", "use_omgwtfnzbs", "use_kat", "use_piratebay", "use_oldpiratebay",
|
"use_nzbsorg", "use_omgwtfnzbs", "use_piratebay", "use_oldpiratebay",
|
||||||
"use_mininova", "use_waffles", "use_rutracker",
|
"use_waffles", "use_rutracker",
|
||||||
"use_apollo", "use_redacted", "use_strike", "use_tquattrecentonze", "preferred_bitrate_allow_lossless",
|
"use_orpheus", "use_redacted", "redacted_use_fltoken", "preferred_bitrate_allow_lossless",
|
||||||
"detect_bitrate", "ignore_clean_releases", "freeze_db", "cue_split", "move_files",
|
"detect_bitrate", "ignore_clean_releases", "freeze_db", "cue_split", "move_files",
|
||||||
"rename_files", "correct_metadata", "cleanup_files", "keep_nfo", "add_album_art",
|
"rename_files", "correct_metadata", "cleanup_files", "keep_nfo", "add_album_art",
|
||||||
"embed_album_art", "embed_lyrics",
|
"embed_album_art", "embed_lyrics",
|
||||||
@@ -1476,7 +1470,7 @@ class WebInterface(object):
|
|||||||
"include_extras", "official_releases_only",
|
"include_extras", "official_releases_only",
|
||||||
"wait_until_release_date", "autowant_upcoming", "autowant_all",
|
"wait_until_release_date", "autowant_upcoming", "autowant_all",
|
||||||
"autowant_manually_added", "do_not_process_unmatched", "keep_torrent_files",
|
"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",
|
"encoderlossless", "encoder_multicore", "delete_lossless_files", "growl_enabled",
|
||||||
"growl_onsnatch", "prowl_enabled",
|
"growl_onsnatch", "prowl_enabled",
|
||||||
"prowl_onsnatch", "xbmc_enabled", "xbmc_update", "xbmc_notify", "lms_enabled",
|
"prowl_onsnatch", "xbmc_enabled", "xbmc_update", "xbmc_notify", "lms_enabled",
|
||||||
@@ -1543,13 +1537,15 @@ class WebInterface(object):
|
|||||||
if len(torznab_number):
|
if len(torznab_number):
|
||||||
torznab_api_key = 'torznab_api' + torznab_number
|
torznab_api_key = 'torznab_api' + torznab_number
|
||||||
torznab_enabled_key = 'torznab_enabled' + torznab_number
|
torznab_enabled_key = 'torznab_enabled' + torznab_number
|
||||||
|
torznab_ratio_key = 'torznab_ratio' + torznab_number
|
||||||
torznab_host = kwargs.get(torznab_host_key, '')
|
torznab_host = kwargs.get(torznab_host_key, '')
|
||||||
torznab_api = kwargs.get(torznab_api_key, '')
|
torznab_api = kwargs.get(torznab_api_key, '')
|
||||||
torznab_enabled = int(kwargs.get(torznab_enabled_key, 0))
|
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:
|
if key in kwargs:
|
||||||
del kwargs[key]
|
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)
|
# Convert the extras to list then string. Coming in as 0 or 1 (append new extras to the end)
|
||||||
temp_extras_list = []
|
temp_extras_list = []
|
||||||
@@ -1743,9 +1739,9 @@ class WebInterface(object):
|
|||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
def testPlex(self):
|
def testPlex(self):
|
||||||
logger.info(u"Testing plex notifications")
|
logger.info(u"Testing plex update")
|
||||||
plex = notifiers.Plex()
|
plex = notifiers.Plex()
|
||||||
plex.notify("hellooooo", "test album!", "")
|
plex.update()
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
def testPushbullet(self):
|
def testPushbullet(self):
|
||||||
|
|||||||
+1
-1
@@ -145,7 +145,7 @@ class _GNTPBase(object):
|
|||||||
pointerEnd = pointerStart + dataLength
|
pointerEnd = pointerStart + dataLength
|
||||||
data = self.raw[pointerStart:pointerEnd]
|
data = self.raw[pointerStart:pointerEnd]
|
||||||
if not len(data) == dataLength:
|
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
|
return data
|
||||||
|
|
||||||
def _validate_password(self, password):
|
def _validate_password(self, password):
|
||||||
|
|||||||
@@ -401,14 +401,15 @@ class GazelleAPI(object):
|
|||||||
|
|
||||||
return {'curr_page': curr_page, 'pages': pages, 'results': matching_torrents}
|
return {'curr_page': curr_page, 'pages': pages, 'results': matching_torrents}
|
||||||
|
|
||||||
def generate_torrent_link(self, id):
|
def generate_torrent_link(self, id, use_token=False):
|
||||||
url = "%storrents.php?action=download&id=%s&authkey=%s&torrent_pass=%s" %\
|
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)
|
(self.site, id, self.logged_in_user.authkey, self.logged_in_user.passkey, use_token)
|
||||||
return url
|
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',
|
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:
|
with open(dest, 'w+') as dest_file:
|
||||||
dest_file.write(file_data)
|
dest_file.write(file_data)
|
||||||
|
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ class Torrent(object):
|
|||||||
self.snatched = search_torrent_json_response['snatches']
|
self.snatched = search_torrent_json_response['snatches']
|
||||||
self.free_torrent = search_torrent_json_response['isFreeleech'] or search_torrent_json_response['isPersonalFreeleech']
|
self.free_torrent = search_torrent_json_response['isFreeleech'] or search_torrent_json_response['isPersonalFreeleech']
|
||||||
self.time = search_torrent_json_response['time']
|
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):
|
def set_torrent_top_10_data(self, top_10_json_response):
|
||||||
if self.id != top_10_json_response['torrentId']:
|
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
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