FIX:(#1046) if nzbname had '+' within title, would fail to create nzb within cache (nzb only), IMP:(#1079) test notification buttons added, FIX: Changed names of functions to ResumeSeries/DeleteSeries/PauseSeries, IMP: pushbullet notification converted now using requests module, FIX:(#1088) better versioning detection when searching, enforce permissions on cache directory before writing (to ensure nzbs will be written with proper permissions), IMP: Popup dialog box on Delete Series in Comic Details page - option to delete series folder & contents avaiable as well, IMP: Try to determine local IP on startup to allow for better execution of sending nzbs to nzb client

This commit is contained in:
evilhero 2015-07-21 02:37:41 -04:00
parent ef037dce1e
commit abcc4e88ec
13 changed files with 951 additions and 343 deletions

View File

@ -9,7 +9,18 @@
<div id="subhead_container"> <div id="subhead_container">
<div id="subhead_menu"> <div id="subhead_menu">
<a id="menu_link_refresh" onclick="doAjaxCall('refreshSeries?ComicID=${comic['ComicID']}', $(this),'table')" href="#" data-success="${comic['ComicName']} is being refreshed">Refresh Comic</a> <a id="menu_link_refresh" onclick="doAjaxCall('refreshSeries?ComicID=${comic['ComicID']}', $(this),'table')" href="#" data-success="${comic['ComicName']} is being refreshed">Refresh Comic</a>
<a id="menu_link_delete" href="deleteArtist?ComicID=${comic['ComicID']}">Delete Comic</a> <a id="menu_link_delete" href="#">Delete Comic</a>
<div id="dialog" title="Delete Series Confirmation" style="display:none" class="configtable">
<form action="deleteSeries" method="GET" style="vertical-align: middle; text-align: center">
</br><input type="submit" value="Delete Series">
<div class="row checkbox left clearfix">
</br>
<input type="checkbox" style="vertical-align: middle; margin: 3px; margin-top: -1px;" name="delete_dir" id="deleteCheck" value="1" ${comicConfig['delete_dir']} /><label>Remove directory when deleting Series?</label>
</div>
<input type="hidden" name="ComicID" value=${comic['ComicID']}>
</form>
</div>
%if mylar.RENAME_FILES: %if mylar.RENAME_FILES:
<a id="menu_link_refresh" onclick="doAjaxCall('manualRename?comicid=${comic['ComicID']}', $(this),'table')" data-success="Renaming files.">Rename Files</a> <a id="menu_link_refresh" onclick="doAjaxCall('manualRename?comicid=${comic['ComicID']}', $(this),'table')" data-success="Renaming files.">Rename Files</a>
%endif %endif
@ -18,9 +29,9 @@
<a id="menu_link_refresh" onclick="doAjaxCall('group_metatag?dirName=${comic['ComicLocation'] |u}&ComicID=${comic['ComicID']}', $(this),'table')" data-success="(re)tagging every issue present for '${comic['ComicName']}'">Manual MetaTagging</a> <a id="menu_link_refresh" onclick="doAjaxCall('group_metatag?dirName=${comic['ComicLocation'] |u}&ComicID=${comic['ComicID']}', $(this),'table')" data-success="(re)tagging every issue present for '${comic['ComicName']}'">Manual MetaTagging</a>
%endif %endif
%if comic['Status'] == 'Paused': %if comic['Status'] == 'Paused':
<a id="menu_link_resume" href="#" onclick="doAjaxCall('resumeArtist?ComicID=${comic['ComicID']}',$(this),true)" data-success="${comic['ComicName']} resumed">Resume Comic</a> <a id="menu_link_resume" href="#" onclick="doAjaxCall('resumeSeries?ComicID=${comic['ComicID']}',$(this),true)" data-success="${comic['ComicName']} resumed">Resume Comic</a>
%else: %else:
<a id="menu_link_pauze" href="#" onclick="doAjaxCall('pauseArtist?ComicID=${comic['ComicID']}',$(this),true)" data-success="${comic['ComicName']} paused">Pause Comic</a> <a id="menu_link_pauze" href="#" onclick="doAjaxCall('pauseSeries?ComicID=${comic['ComicID']}',$(this),true)" data-success="${comic['ComicName']} paused">Pause Comic</a>
%endif %endif
%if annuals: %if annuals:
<a id="menu_link_delete" href="annualDelete?comicid=${comic['ComicID']}">Delete Annuals</a> <a id="menu_link_delete" href="annualDelete?comicid=${comic['ComicID']}">Delete Annuals</a>
@ -58,8 +69,9 @@
<li><a href="#tabs-1">Comic Details</a></li> <li><a href="#tabs-1">Comic Details</a></li>
<li><a href="#tabs-2">Download settings</a></li> <li><a href="#tabs-2">Download settings</a></li>
<li><a href="#tabs-3">Edit Settings</a></li> <li><a href="#tabs-3">Edit Settings</a></li>
</ul> </ul>
<div id="tabs-1">
<div id="tabs-1">
<table class="comictable" summary="Comic Details"> <table class="comictable" summary="Comic Details">
@ -77,39 +89,39 @@
%elif comic['ComicPublisher'] == 'Marvel': %elif comic['ComicPublisher'] == 'Marvel':
<img src="interfaces/default/images/publisherlogos/logo-marvel.jpg" align="right" alt="Marvel" height="50" width="100"/> <img src="interfaces/default/images/publisherlogos/logo-marvel.jpg" align="right" alt="Marvel" height="50" width="100"/>
%elif comic['ComicPublisher'] == 'Image': %elif comic['ComicPublisher'] == 'Image':
<img src="interfaces/default/images/publisherlogos/logo-imagecomics.png" align="right" alt="Image" height="100" width="50" /> <img src="interfaces/default/images/publisherlogos/logo-imagecomics.png" align="right" alt="Image" height="100" width="50" />
%elif comic['ComicPublisher'] == 'Dark Horse Comics' or comic['ComicPublisher'] == 'Dark Horse': %elif comic['ComicPublisher'] == 'Dark Horse Comics' or comic['ComicPublisher'] == 'Dark Horse':
<img src="interfaces/default/images/publisherlogos/logo-darkhorse.png" align="right" alt="Darkhorse" height="100" width="75" /> <img src="interfaces/default/images/publisherlogos/logo-darkhorse.png" align="right" alt="Darkhorse" height="100" width="75" />
%elif comic['ComicPublisher'] == 'IDW Publishing': %elif comic['ComicPublisher'] == 'IDW Publishing':
<img src="interfaces/default/images/publisherlogos/logo-idwpublish.png" align="right" alt="IDW" height="50" width="100"/> <img src="interfaces/default/images/publisherlogos/logo-idwpublish.png" align="right" alt="IDW" height="50" width="100"/>
%elif comic['ComicPublisher'] == 'Icon': %elif comic['ComicPublisher'] == 'Icon':
<img src="interfaces/default/images/publisherlogos/logo-iconcomics.png" align="right" alt="Icon" height="50" width="100"/> <img src="interfaces/default/images/publisherlogos/logo-iconcomics.png" align="right" alt="Icon" height="50" width="100"/>
%elif comic['ComicPublisher'] == 'Red5': %elif comic['ComicPublisher'] == 'Red5':
<img src="interfaces/default/images/publisherlogos/logo-red5comics.png" align="right" alt="Red5" height="50" width="100"/> <img src="interfaces/default/images/publisherlogos/logo-red5comics.png" align="right" alt="Red5" height="50" width="100"/>
%elif comic['ComicPublisher'] == 'Vertigo': %elif comic['ComicPublisher'] == 'Vertigo':
<img src="interfaces/default/images/publisherlogos/logo-vertigo.jpg" align="right" alt="Vertigo" height="50" width="100"/> <img src="interfaces/default/images/publisherlogos/logo-vertigo.jpg" align="right" alt="Vertigo" height="50" width="100"/>
%elif comic['ComicPublisher'] == 'ShadowLine': %elif comic['ComicPublisher'] == 'ShadowLine':
<img src="interfaces/default/images/publisherlogos/logo-shadowline.png" align="right" alt="Shadowline" height="75" width="100"/> <img src="interfaces/default/images/publisherlogos/logo-shadowline.png" align="right" alt="Shadowline" height="75" width="100"/>
%elif comic['ComicPublisher'] == 'Archie Comics': %elif comic['ComicPublisher'] == 'Archie Comics':
<img src="interfaces/default/images/publisherlogos/logo-archiecomics.jpg" align="right" alt="Archie" height="75" width="75"/> <img src="interfaces/default/images/publisherlogos/logo-archiecomics.jpg" align="right" alt="Archie" height="75" width="75"/>
%elif comic['ComicPublisher'] == 'Oni Press': %elif comic['ComicPublisher'] == 'Oni Press':
<img src="interfaces/default/images/publisherlogos/logo-onipress.png" align="right" alt="Oni Press" height="50" width="100"/> <img src="interfaces/default/images/publisherlogos/logo-onipress.png" align="right" alt="Oni Press" height="50" width="100"/>
%elif comic['ComicPublisher'] == 'Tokyopop': %elif comic['ComicPublisher'] == 'Tokyopop':
<img src="interfaces/default/images/publisherlogos/logo-tokyopop.jpg" align="right" alt="Tokyopop" height="50" width="100"/> <img src="interfaces/default/images/publisherlogos/logo-tokyopop.jpg" align="right" alt="Tokyopop" height="50" width="100"/>
%elif comic['ComicPublisher'] == 'Midtown Comics': %elif comic['ComicPublisher'] == 'Midtown Comics':
<img src="interfaces/default/images/publisherlogos/logo-midtowncomics.jpg" align="right" alt="Midtown" height="50" width="100"/> <img src="interfaces/default/images/publisherlogos/logo-midtowncomics.jpg" align="right" alt="Midtown" height="50" width="100"/>
%elif comic['ComicPublisher'] == 'Boom! Studios': %elif comic['ComicPublisher'] == 'Boom! Studios':
<img src="interfaces/default/images/publisherlogos/logo-boom.jpg" align="right" alt="Boom!" height="50" width="100"/> <img src="interfaces/default/images/publisherlogos/logo-boom.jpg" align="right" alt="Boom!" height="50" width="100"/>
%elif comic['ComicPublisher'] == 'Skybound': %elif comic['ComicPublisher'] == 'Skybound':
<img src="interfaces/default/images/publisherlogos/logo-skybound.jpg" align="right" alt="Skybound" height="50" width="100"/> <img src="interfaces/default/images/publisherlogos/logo-skybound.jpg" align="right" alt="Skybound" height="50" width="100"/>
%elif comic['ComicPublisher'] == 'Vertigo': %elif comic['ComicPublisher'] == 'Vertigo':
<img src="interfaces/default/images/publisherlogos/logo-dynamite.jpg" align="right" alt="Dynamite" height="50" width="100"/> <img src="interfaces/default/images/publisherlogos/logo-dynamite.jpg" align="right" alt="Dynamite" height="50" width="100"/>
%elif comic['ComicPublisher'] == 'Top Cow': %elif comic['ComicPublisher'] == 'Top Cow':
<img src="interfaces/default/images/publisherlogos/logo-topcow.gif" align="right" alt="Top Cow" height="75" width="100"/> <img src="interfaces/default/images/publisherlogos/logo-topcow.gif" align="right" alt="Top Cow" height="75" width="100"/>
%elif comic['ComicPublisher'] == 'Dynamite Entertainment': %elif comic['ComicPublisher'] == 'Dynamite Entertainment':
<img src="interfaces/default/images/publisherlogos/logo-dynamite.png" align="right" alt="Dynamite" height="50" width="100"/> <img src="interfaces/default/images/publisherlogos/logo-dynamite.png" align="right" alt="Dynamite" height="50" width="100"/>
%elif comic['ComicPublisher'] == 'Cartoon Books': %elif comic['ComicPublisher'] == 'Cartoon Books':
<img src="interfaces/default/images/publisherlogos/logo-cartoonbooks.jpg" align="right" alt="Cartoon Books" height="75" width="90"/> <img src="interfaces/default/images/publisherlogos/logo-cartoonbooks.jpg" align="right" alt="Cartoon Books" height="75" width="90"/>
%endif %endif
<fieldset> <fieldset>
<div> <div>
@ -359,7 +371,7 @@
<table class="display" id="issue_table"> <table class="display" id="issue_table">
<thead> <thead>
<tr> <tr>
<th id="select" align="left"><input type="checkbox" onClick="toggle(this)" class="checkbox" /></th> <th id="select" align="left"><input type="checkbox" onClick="toggle(this)" name="issues" class="checkbox" /></th>
<th id="int_issuenumber">IntIssNum</th> <th id="int_issuenumber">IntIssNum</th>
<th id="issuenumber">Number</th> <th id="issuenumber">Number</th>
<th id="issuename">Name</th> <th id="issuename">Name</th>
@ -467,6 +479,9 @@
</form> </form>
</div> </div>
<div class="table_wrapper">
%if annuals: %if annuals:
<h1>Annuals</h1> <h1>Annuals</h1>
%for aninfo in annualinfo: %for aninfo in annualinfo:
@ -501,7 +516,7 @@
--> -->
<thead> <thead>
<tr> <tr>
<th id="select" align="left"><input type="checkbox" onClick="toggle(this)" class="checkbox" /></th> <th id="ann_action" align="left"><input type="checkbox" onClick="toggle(this)" class="checkbox" /></th>
<th id="aint_issuenumber">Int_IssNumber</th> <th id="aint_issuenumber">Int_IssNumber</th>
<th id="aissuenumber">Number</th> <th id="aissuenumber">Number</th>
<th id="aissuename">Name</th> <th id="aissuename">Name</th>
@ -533,7 +548,7 @@
agrade = 'A' agrade = 'A'
%> %>
<tr class="${annual['Status']} grade${agrade}"> <tr class="${annual['Status']} grade${agrade}">
<td id="select"><input type="checkbox" name="${annual['IssueID']}" class="checkbox" value="${annual['IssueID']}" /></td> <td id="ann_action"><input type="checkbox" name="${annual['IssueID']}" class="checkbox" value="${annual['IssueID']}" /></td>
<% <%
if annual['Int_IssueNumber'] is None: if annual['Int_IssueNumber'] is None:
annual_Number = annual['Int_IssueNumber'] annual_Number = annual['Int_IssueNumber']
@ -624,7 +639,7 @@
</form> </form>
</div> </div>
%endif %endif
</div>
</%def> </%def>
<%def name="headIncludes()"> <%def name="headIncludes()">
@ -713,52 +728,12 @@
</script> </script>
<script> <script>
$(function() { function openDelete() {
function log( message ) { $("#dialog").dialog({modal:true});
$( "<div>" ).text( message ).prependTo( "#log" ); };
$( "#log" ).scrollTop( 0 ); function deleteDirCheck() {
} return document.getElementById("deleteCheck").checked;
}
$( "#annseries" ).autocomplete({
source: function( request, response ) {
$.ajax({
url: "http://api.comicvine.com/search?",
contentType: "application/json; charset=utf-8",
dataType: "json",
data: {
resources: "volume",
format: "json",
api_key: "",
query: request.term
},
success: function( data ) {
alert("success");
response( $.map( data.results, function( item ) {
return {
label: item.name + (item.id),
value: item.name
}
}));
},
error: function( data ) {
alert("error");
}
});
},
minLength: 2,
select: function( event, ui ) {
log( ui.item ?
"Selected: " + ui.item.label :
"Nothing selected, input was " + this.value);
},
open: function() {
$( this ).removeClass( "ui-corner-all" ).addClass( "ui-corner-top" );
},
close: function() {
$( this ).removeClass( "ui-corner-top" ).addClass( "ui-corner-all" );
}
});
});
</script> </script>
<script> <script>
@ -796,30 +771,12 @@
}); });
} }
hideServerDivs = function () {
$("#customoptions").slideUp();
$("#hpserveroptions").slideUp();
};
handleNewSelection = function () {
hideServerDivs();
switch ($(this).val()) {
case 'custom':
$("#customoptions").slideDown();
break;
case 'mylar':
$("#hpserveroptions").slideDown();
break;
}
};
function initThisPage(){ function initThisPage(){
$(function() { $(function() {
$( "#tabs" ).tabs(); $( "#tabs" ).tabs();
}); });
$("#menu_link_delete").click(openDelete);
initActions(); initActions();
$('#issue_table').dataTable( $('#issue_table').dataTable(
{ {

View File

@ -899,13 +899,16 @@
<div class="row"> <div class="row">
<label>API key</label><input type="text" name="prowl_keys" value="${config['prowl_keys']}" size="50"> <label>API key</label><input type="text" name="prowl_keys" value="${config['prowl_keys']}" size="50">
</div> </div>
<div class="row checkbox"> <div class="row checkbox">
<input type="checkbox" name="prowl_onsnatch" value="1" ${config['prowl_onsnatch']} /><label>Notify on snatch?</label> <input type="checkbox" name="prowl_onsnatch" value="1" ${config['prowl_onsnatch']} /><label>Notify on snatch?</label>
</div> </div>
<div class="row"> <div class="row">
<label>Priority (-2,-1,0,1 or 2):</label> <label>Priority (-2,-1,0,1 or 2):</label>
<input type="text" name="prowl_priority" value="${config['prowl_priority']}" size="2"> <input type="text" name="prowl_priority" value="${config['prowl_priority']}" size="2">
</div> </div>
<div class="row">
<input type="button" value="Test PROWL" id="prowl_test" />
</div>
</div> </div>
</fieldset> </fieldset>
<fieldset> <fieldset>
@ -923,8 +926,8 @@
<small>Separate multiple api keys with commas</small> <small>Separate multiple api keys with commas</small>
</div> </div>
<div class="row"> <div class="row">
<label>Priority</label> <label>Priority</label>
<select name="nma_priority"> <select name="nma_priority">
%for x in [-2,-1,0,1,2]: %for x in [-2,-1,0,1,2]:
<% <%
if config['nma_priority'] == x: if config['nma_priority'] == x:
@ -943,19 +946,22 @@
else: else:
nma_priority_value = 'Emergency' nma_priority_value = 'Emergency'
%> %>
<option value=${x} ${nma_priority_selected}>${nma_priority_value}</option> <option value=${x} ${nma_priority_selected}>${nma_priority_value}</option>
%endfor %endfor
</select> </select>
</div> </div>
<div class="row">
<input type="button" value="Test NMA" id="nma_test" />
</div>
</div> </div>
</fieldset> </fieldset>
<fieldset> <fieldset>
<h3><img src="interfaces/default/images/pushover_logo.png" style="vertical-align: middle; margin: 3px; margin-top: -1px;" height="30" width="30"/>Pushover</h3> <h3><img src="interfaces/default/images/pushover_logo.png" style="vertical-align: middle; margin: 3px; margin-top: -1px;" height="30" width="30"/>Pushover</h3>
<div class="row checkbox"> <div class="row checkbox">
<input type="checkbox" name="pushover_enabled" id="pushover" value="1" ${config['pushover_enabled']} /><label>Enable Pushover Notifications</label> <input type="checkbox" name="pushover_enabled" id="pushover" value="1" ${config['pushover_enabled']} /><label>Enable Pushover Notifications</label>
</div> </div>
<div id="pushoveroptions"> <div id="pushoveroptions">
<div class="row"> <div class="row">
<label>API key</label><input type="text" title="Leave blank if you don't have your own API (recommended to get your own)" name="pushover_apikey" value="${config['pushover_apikey']}" size="50"> <label>API key</label><input type="text" title="Leave blank if you don't have your own API (recommended to get your own)" name="pushover_apikey" value="${config['pushover_apikey']}" size="50">
</div> </div>
@ -969,7 +975,10 @@
<label>Priority (-2,-1,0,1 or 2):</label> <label>Priority (-2,-1,0,1 or 2):</label>
<input type="text" name="pushover_priority" value="${config['pushover_priority']}" size="2"> <input type="text" name="pushover_priority" value="${config['pushover_priority']}" size="2">
</div> </div>
</div> <div class="row">
<input type="button" value="Test Pushover" id="pushover_test" />
</div>
</div>
</fieldset> </fieldset>
<fieldset> <fieldset>
<h3><img src="interfaces/default/images/boxcar_logo.png" style="vertical-align: middle; margin: 3px; margin-top: -1px;" height="30" width="30"/>Boxcar.IO</h3> <h3><img src="interfaces/default/images/boxcar_logo.png" style="vertical-align: middle; margin: 3px; margin-top: -1px;" height="30" width="30"/>Boxcar.IO</h3>
@ -983,6 +992,9 @@
</div> </div>
<label>Boxcar Token</label> <label>Boxcar Token</label>
<input type="text" name="boxcar_token" value="${config['boxcar_token']}" size="30"> <input type="text" name="boxcar_token" value="${config['boxcar_token']}" size="30">
<div class="row">
<input type="button" value="Test Boxcar" id="boxcar_test" />
</div>
</div> </div>
</div> </div>
</fieldset> </fieldset>
@ -1007,6 +1019,9 @@
<input type="button" class="btn" value="Update device list" id="getPushbulletDevices" /> <input type="button" class="btn" value="Update device list" id="getPushbulletDevices" />
--> -->
</div> </div>
<div class="row">
<input type="button" value="Test Pushbullet" id="pushbullet_test" />
</div>
</div> </div>
</fieldset> </fieldset>
</td> </td>
@ -1286,6 +1301,35 @@
$('#autoadd').append('<input type="hidden" name="tsab" value=1 />'); $('#autoadd').append('<input type="hidden" name="tsab" value=1 />');
}; };
$('#nma_test').click(function () {
$.get("/testNMA",
function (data) { $('#ajaxMsg').html("<div class='msg'><span class='ui-icon ui-icon-check'></span>"+data+"</div>"); });
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut();
});
$('#prowl_test').click(function () {
$.get("/testprowl",
function (data) { $('#ajaxMsg').html("<div class='msg'><span class='ui-icon ui-icon-check'></span>"+data+"</div>"); });
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut();
});
$('#pushover_test').click(function () {
$.get("/testpushover",
function (data) { $('#ajaxMsg').html("<div class='msg'><span class='ui-icon ui-icon-check'></span>"+data+"</div>"); });
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut();
});
$('#boxcar_test').click(function () {
$.get("/testboxcar",
function (data) { $('#ajaxMsg').html("<div class='msg'><span class='ui-icon ui-icon-check'></span>"+data+"</div>"); });
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut();
});
$('#pushbullet_test').click(function () {
$.get("/testpushbullet",
function (data) { $('#ajaxMsg').html("<div class='msg'><span class='ui-icon ui-icon-check'></span>"+data+"</div>"); });
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut();
});
$(function() { $(function() {
$( "#tabs" ).tabs(); $( "#tabs" ).tabs();

93
lib/pystun/README.rst Normal file
View File

@ -0,0 +1,93 @@
.. image:: https://travis-ci.org/jtriley/pystun.svg?branch=master
:target: https://travis-ci.org/jtriley/pystun
.. image:: https://coveralls.io/repos/jtriley/pystun/badge.png
:target: https://coveralls.io/r/jtriley/pystun
PyStun
======
A Python STUN client for getting NAT type and external IP
This is a fork of pystun originally created by gaohawk (http://code.google.com/p/pystun/)
PyStun follows RFC 3489: http://www.ietf.org/rfc/rfc3489.txt
A server following STUN-bis hasn't been found on internet so RFC3489 is the
only implementation.
Installation
------------
To install the latest version::
$ pip install pystun
or download/clone the source and install manually using::
$ cd /path/to/pystun/src
$ python setup.py install
If you're hacking on pystun you should use the 'develop' command instead::
$ python setup.py develop
This will make a link to the sources inside your site-packages directory so
that any changes are immediately available for testing.
Usage
-----
From command line::
$ pystun
NAT Type: Full Cone
External IP: <your-ip-here>
External Port: 54320
Pass --help for more options::
% pystun --help
usage: pystun [-h] [-d] [-H STUN_HOST] [-P STUN_PORT] [-i SOURCE_IP]
[-p SOURCE_PORT] [--version]
optional arguments:
-h, --help show this help message and exit
-d, --debug Enable debug logging (default: False)
-H STUN_HOST, --host STUN_HOST
STUN host to use (default: None)
-P STUN_PORT, --host-port STUN_PORT
STUN host port to use (default: 3478)
-i SOURCE_IP, --interface SOURCE_IP
network interface for client (default: 0.0.0.0)
-p SOURCE_PORT, --port SOURCE_PORT
port to listen on for client (default: 54320)
--version show program's version number and exit
From Python::
import stun
nat_type, external_ip, external_port = stun.get_ip_info()
This will rotate through an internal list of STUN servers until a response is
found. If no response is found you will get "Blocked" as the *nat_type* and
**None** for *external_ip* and *external_port*.
If you prefer to use a specific STUN server::
nat_type, external_ip, external_port = stun.get_ip_info(stun_host='stun.ekiga.net')
If you prefer to use a specific STUN server port::
nat_type, external_ip, external_port = stun.get_ip_info(stun_port=3478)
You may also specify the client interface and port that is used although this
is not needed::
sip = "0.0.0.0" # interface to listen on (all)
port = 54320 # port to listen on
nat_type, external_ip, external_port = stun.get_ip_info(sip, port)
Read the code for more details...
LICENSE
-------
MIT

257
lib/pystun/__init__.py Normal file
View File

@ -0,0 +1,257 @@
import binascii
import logging
import random
import socket
__version__ = '0.1.0'
log = logging.getLogger("pystun")
STUN_SERVERS = (
'stun.ekiga.net',
'stun.ideasip.com',
'stun.voiparound.com',
'stun.voipbuster.com',
'stun.voipstunt.com',
'stun.voxgratia.org'
)
stun_servers_list = STUN_SERVERS
DEFAULTS = {
'stun_port': 3478,
'source_ip': '0.0.0.0',
'source_port': 54320
}
# stun attributes
MappedAddress = '0001'
ResponseAddress = '0002'
ChangeRequest = '0003'
SourceAddress = '0004'
ChangedAddress = '0005'
Username = '0006'
Password = '0007'
MessageIntegrity = '0008'
ErrorCode = '0009'
UnknownAttribute = '000A'
ReflectedFrom = '000B'
XorOnly = '0021'
XorMappedAddress = '8020'
ServerName = '8022'
SecondaryAddress = '8050' # Non standard extension
# types for a stun message
BindRequestMsg = '0001'
BindResponseMsg = '0101'
BindErrorResponseMsg = '0111'
SharedSecretRequestMsg = '0002'
SharedSecretResponseMsg = '0102'
SharedSecretErrorResponseMsg = '0112'
dictAttrToVal = {'MappedAddress': MappedAddress,
'ResponseAddress': ResponseAddress,
'ChangeRequest': ChangeRequest,
'SourceAddress': SourceAddress,
'ChangedAddress': ChangedAddress,
'Username': Username,
'Password': Password,
'MessageIntegrity': MessageIntegrity,
'ErrorCode': ErrorCode,
'UnknownAttribute': UnknownAttribute,
'ReflectedFrom': ReflectedFrom,
'XorOnly': XorOnly,
'XorMappedAddress': XorMappedAddress,
'ServerName': ServerName,
'SecondaryAddress': SecondaryAddress}
dictMsgTypeToVal = {
'BindRequestMsg': BindRequestMsg,
'BindResponseMsg': BindResponseMsg,
'BindErrorResponseMsg': BindErrorResponseMsg,
'SharedSecretRequestMsg': SharedSecretRequestMsg,
'SharedSecretResponseMsg': SharedSecretResponseMsg,
'SharedSecretErrorResponseMsg': SharedSecretErrorResponseMsg}
dictValToMsgType = {}
dictValToAttr = {}
Blocked = "Blocked"
OpenInternet = "Open Internet"
FullCone = "Full Cone"
SymmetricUDPFirewall = "Symmetric UDP Firewall"
RestricNAT = "Restric NAT"
RestricPortNAT = "Restric Port NAT"
SymmetricNAT = "Symmetric NAT"
ChangedAddressError = "Meet an error, when do Test1 on Changed IP and Port"
def _initialize():
items = dictAttrToVal.items()
for i in range(len(items)):
dictValToAttr.update({items[i][1]: items[i][0]})
items = dictMsgTypeToVal.items()
for i in range(len(items)):
dictValToMsgType.update({items[i][1]: items[i][0]})
def gen_tran_id():
a = ''.join(random.choice('0123456789ABCDEF') for i in range(32))
# return binascii.a2b_hex(a)
return a
def stun_test(sock, host, port, source_ip, source_port, send_data=""):
retVal = {'Resp': False, 'ExternalIP': None, 'ExternalPort': None,
'SourceIP': None, 'SourcePort': None, 'ChangedIP': None,
'ChangedPort': None}
str_len = "%#04d" % (len(send_data) / 2)
tranid = gen_tran_id()
str_data = ''.join([BindRequestMsg, str_len, tranid, send_data])
data = binascii.a2b_hex(str_data)
recvCorr = False
while not recvCorr:
recieved = False
count = 3
while not recieved:
log.debug("sendto: %s", (host, port))
try:
sock.sendto(data, (host, port))
except socket.gaierror:
retVal['Resp'] = False
return retVal
try:
buf, addr = sock.recvfrom(2048)
log.debug("recvfrom: %s", addr)
recieved = True
except Exception:
recieved = False
if count > 0:
count -= 1
else:
retVal['Resp'] = False
return retVal
msgtype = binascii.b2a_hex(buf[0:2])
bind_resp_msg = dictValToMsgType[msgtype] == "BindResponseMsg"
tranid_match = tranid.upper() == binascii.b2a_hex(buf[4:20]).upper()
if bind_resp_msg and tranid_match:
recvCorr = True
retVal['Resp'] = True
len_message = int(binascii.b2a_hex(buf[2:4]), 16)
len_remain = len_message
base = 20
while len_remain:
attr_type = binascii.b2a_hex(buf[base:(base + 2)])
attr_len = int(binascii.b2a_hex(buf[(base + 2):(base + 4)]), 16)
if attr_type == MappedAddress:
port = int(binascii.b2a_hex(buf[base + 6:base + 8]), 16)
ip = ".".join([
str(int(binascii.b2a_hex(buf[base + 8:base + 9]), 16)),
str(int(binascii.b2a_hex(buf[base + 9:base + 10]), 16)),
str(int(binascii.b2a_hex(buf[base + 10:base + 11]), 16)),
str(int(binascii.b2a_hex(buf[base + 11:base + 12]), 16))
])
retVal['ExternalIP'] = ip
retVal['ExternalPort'] = port
if attr_type == SourceAddress:
port = int(binascii.b2a_hex(buf[base + 6:base + 8]), 16)
ip = ".".join([
str(int(binascii.b2a_hex(buf[base + 8:base + 9]), 16)),
str(int(binascii.b2a_hex(buf[base + 9:base + 10]), 16)),
str(int(binascii.b2a_hex(buf[base + 10:base + 11]), 16)),
str(int(binascii.b2a_hex(buf[base + 11:base + 12]), 16))
])
retVal['SourceIP'] = ip
retVal['SourcePort'] = port
if attr_type == ChangedAddress:
port = int(binascii.b2a_hex(buf[base + 6:base + 8]), 16)
ip = ".".join([
str(int(binascii.b2a_hex(buf[base + 8:base + 9]), 16)),
str(int(binascii.b2a_hex(buf[base + 9:base + 10]), 16)),
str(int(binascii.b2a_hex(buf[base + 10:base + 11]), 16)),
str(int(binascii.b2a_hex(buf[base + 11:base + 12]), 16))
])
retVal['ChangedIP'] = ip
retVal['ChangedPort'] = port
# if attr_type == ServerName:
# serverName = buf[(base+4):(base+4+attr_len)]
base = base + 4 + attr_len
len_remain = len_remain - (4 + attr_len)
# s.close()
return retVal
def get_nat_type(s, source_ip, source_port, stun_host=None, stun_port=3478):
_initialize()
port = stun_port
log.debug("Do Test1")
resp = False
if stun_host:
ret = stun_test(s, stun_host, port, source_ip, source_port)
resp = ret['Resp']
else:
for stun_host in stun_servers_list:
log.debug('Trying STUN host: %s', stun_host)
ret = stun_test(s, stun_host, port, source_ip, source_port)
resp = ret['Resp']
if resp:
break
if not resp:
return Blocked, ret
log.debug("Result: %s", ret)
exIP = ret['ExternalIP']
exPort = ret['ExternalPort']
changedIP = ret['ChangedIP']
changedPort = ret['ChangedPort']
if ret['ExternalIP'] == source_ip:
changeRequest = ''.join([ChangeRequest, '0004', "00000006"])
ret = stun_test(s, stun_host, port, source_ip, source_port,
changeRequest)
if ret['Resp']:
typ = OpenInternet
else:
typ = SymmetricUDPFirewall
else:
changeRequest = ''.join([ChangeRequest, '0004', "00000006"])
log.debug("Do Test2")
ret = stun_test(s, stun_host, port, source_ip, source_port,
changeRequest)
log.debug("Result: %s", ret)
if ret['Resp']:
typ = FullCone
else:
log.debug("Do Test1")
ret = stun_test(s, changedIP, changedPort, source_ip, source_port)
log.debug("Result: %s", ret)
if not ret['Resp']:
typ = ChangedAddressError
else:
if exIP == ret['ExternalIP'] and exPort == ret['ExternalPort']:
changePortRequest = ''.join([ChangeRequest, '0004',
"00000002"])
log.debug("Do Test3")
ret = stun_test(s, changedIP, port, source_ip, source_port,
changePortRequest)
log.debug("Result: %s", ret)
if ret['Resp']:
typ = RestricNAT
else:
typ = RestricPortNAT
else:
typ = SymmetricNAT
return typ, ret
def get_ip_info(source_ip="0.0.0.0", source_port=54320, stun_host=None,
stun_port=3478):
socket.setdefaulttimeout(2)
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((source_ip, source_port))
nat_type, nat = get_nat_type(s, source_ip, source_port,
stun_host=stun_host, stun_port=stun_port)
external_ip = nat['ExternalIP']
external_port = nat['ExternalPort']
s.close()
return (nat_type, external_ip, external_port)

64
lib/pystun/cli.py Normal file
View File

@ -0,0 +1,64 @@
from __future__ import print_function
import argparse
import logging
import sys
import stun
def make_argument_parser():
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter
)
parser.add_argument(
'-d', '--debug', action='store_true',
help='Enable debug logging'
)
parser.add_argument(
'-H', '--stun-host',
help='STUN host to use'
)
parser.add_argument(
'-P', '--stun-port', type=int,
default=stun.DEFAULTS['stun_port'],
help='STUN host port to use'
)
parser.add_argument(
'-i', '--source-ip',
default=stun.DEFAULTS['source_ip'],
help='network interface for client'
)
parser.add_argument(
'-p', '--source-port', type=int,
default=stun.DEFAULTS['source_port'],
help='port to listen on for client'
)
parser.add_argument('--version', action='version', version=stun.__version__)
return parser
def main():
try:
options = make_argument_parser().parse_args()
if options.debug:
logging.basicConfig()
stun.log.setLevel(logging.DEBUG)
nat_type, external_ip, external_port = stun.get_ip_info(
source_ip=options.source_ip,
source_port=options.source_port,
stun_host=options.stun_host,
stun_port=options.stun_port
)
print('NAT Type:', nat_type)
print('External IP:', external_ip)
print('External Port:', external_port)
except KeyboardInterrupt:
sys.exit()
if __name__ == '__main__':
main()

View File

View File

@ -0,0 +1,61 @@
import argparse
import unittest
import stun
from stun import cli
class TestCLI(unittest.TestCase):
"""Test the CLI API."""
@classmethod
def setUpClass(cls):
cls.source_ip = '123.45.67.89'
cls.source_port = 24816
cls.stun_port = 13579
cls.stun_host = 'stun.stub.org'
def test_cli_parser_default(self):
parser = cli.make_argument_parser()
options = parser.parse_args([])
self.assertEqual(options.source_ip, stun.DEFAULTS['source_ip'])
self.assertEqual(options.source_port, stun.DEFAULTS['source_port'])
self.assertEqual(options.stun_port, stun.DEFAULTS['stun_port'])
self.assertIsNone(options.stun_host)
def test_cli_parser_user_long_form(self):
parser = cli.make_argument_parser()
options = parser.parse_args([
'--source-port', str(self.source_port),
'--source-ip', self.source_ip,
'--stun-port', str(self.stun_port),
'--stun-host', self.stun_host,
'--debug'
])
self.assertTrue(options.debug)
self.assertEqual(options.source_ip, self.source_ip)
self.assertEqual(options.source_port, self.source_port)
self.assertEqual(options.stun_host, self.stun_host)
self.assertEqual(options.stun_port, self.stun_port)
def test_cli_parser_user_short_form(self):
parser = cli.make_argument_parser()
options = parser.parse_args([
'-p', str(self.source_port),
'-i', self.source_ip,
'-P', str(self.stun_port),
'-H', self.stun_host,
'-d'
])
self.assertTrue(options.debug)
self.assertEqual(options.source_ip, self.source_ip)
self.assertEqual(options.source_port, self.source_port)
self.assertEqual(options.stun_host, self.stun_host)
self.assertEqual(options.stun_port, self.stun_port)
if __name__ == '__main__':
unittest.main()

View File

@ -174,10 +174,10 @@ class PostProcessor(object):
def Process(self): def Process(self):
module = self.module module = self.module
self._log("nzb name: " + str(self.nzb_name)) self._log("nzb name: " + self.nzb_name)
self._log("nzb folder: " + str(self.nzb_folder)) self._log("nzb folder: " + self.nzb_folder)
logger.fdebug(module + ' nzb name: ' + str(self.nzb_name)) logger.fdebug(module + ' nzb name: ' + self.nzb_name)
logger.fdebug(module + ' nzb folder: ' + str(self.nzb_folder)) logger.fdebug(module + ' nzb folder: ' + self.nzb_folder)
if mylar.USE_SABNZBD==0: if mylar.USE_SABNZBD==0:
logger.fdebug(module + ' Not using SABnzbd') logger.fdebug(module + ' Not using SABnzbd')
elif mylar.USE_SABNZBD != 0 and self.nzb_name == 'Manual Run': elif mylar.USE_SABNZBD != 0 and self.nzb_name == 'Manual Run':
@ -606,7 +606,7 @@ class PostProcessor(object):
logger.warn(module + ' Failed to remove temporary directory - check directory and manually re-run.') logger.warn(module + ' Failed to remove temporary directory - check directory and manually re-run.')
return return
logger.fdebug(module + ' Removed temporary directory : ' + str(self.nzb_folder)) logger.fdebug(module + ' Removed temporary directory : ' + self.nzb_folder)
#delete entry from nzblog table #delete entry from nzblog table
IssArcID = 'S' + str(ml['IssueArcID']) IssArcID = 'S' + str(ml['IssueArcID'])
@ -635,7 +635,7 @@ class PostProcessor(object):
logger.fdebug('[NZBNAME]: ' + nzbname) logger.fdebug('[NZBNAME]: ' + nzbname)
#gotta replace & or escape it #gotta replace & or escape it
nzbname = re.sub("\&", 'and', nzbname) nzbname = re.sub("\&", 'and', nzbname)
nzbname = re.sub('[\,\:\?\']', '', nzbname) nzbname = re.sub('[\,\:\?\'\+]', '', nzbname)
nzbname = re.sub('[\(\)]', ' ', nzbname) nzbname = re.sub('[\(\)]', ' ', nzbname)
logger.fdebug('[NZBNAME] nzbname (remove chars): ' + nzbname) logger.fdebug('[NZBNAME] nzbname (remove chars): ' + nzbname)
nzbname = re.sub('.cbr', '', nzbname).strip() nzbname = re.sub('.cbr', '', nzbname).strip()
@ -830,7 +830,7 @@ class PostProcessor(object):
logger.debug(module + ' Failed to remove temporary directory - check directory and manually re-run.') logger.debug(module + ' Failed to remove temporary directory - check directory and manually re-run.')
return return
logger.debug(module + ' Removed temporary directory : ' + str(self.nzb_folder)) logger.debug(module + ' Removed temporary directory : ' + self.nzb_folder)
self._log("Removed temporary directory : " + self.nzb_folder) self._log("Removed temporary directory : " + self.nzb_folder)
#delete entry from nzblog table #delete entry from nzblog table
myDB.action('DELETE from nzblog WHERE issueid=?', [issueid]) myDB.action('DELETE from nzblog WHERE issueid=?', [issueid])
@ -1223,7 +1223,7 @@ class PostProcessor(object):
logger.fdebug(module + ' ext:' + ext) logger.fdebug(module + ' ext:' + ext)
if ofilename is None: if ofilename is None:
logger.error(module + ' Aborting PostProcessing - the filename does not exist in the location given. Make sure that ' + str(self.nzb_folder) + ' exists and is the correct location.') logger.error(module + ' Aborting PostProcessing - the filename does not exist in the location given. Make sure that ' + self.nzb_folder + ' exists and is the correct location.')
self.valreturn.append({"self.log": self.log, self.valreturn.append({"self.log": self.log,
"mode": 'stop'}) "mode": 'stop'})
return self.queue.put(self.valreturn) return self.queue.put(self.valreturn)
@ -1302,7 +1302,7 @@ class PostProcessor(object):
self.valreturn.append({"self.log": self.log, self.valreturn.append({"self.log": self.log,
"mode": 'stop'}) "mode": 'stop'})
return self.queue.put(self.valreturn) return self.queue.put(self.valreturn)
self._log("Removed temporary directory : " + str(self.nzb_folder)) self._log("Removed temporary directory : " + self.nzb_folder)
logger.fdebug(module + ' Removed temporary directory : ' + self.nzb_folder) logger.fdebug(module + ' Removed temporary directory : ' + self.nzb_folder)
else: else:
#downtype = for use with updater on history table to set status to 'Post-Processed' #downtype = for use with updater on history table to set status to 'Post-Processed'

View File

@ -104,6 +104,8 @@ DONATEBUTTON = True
PULLNEW = None PULLNEW = None
ALT_PULL = False ALT_PULL = False
LOCAL_IP = None
EXT_IP = None
HTTP_PORT = None HTTP_PORT = None
HTTP_HOST = None HTTP_HOST = None
HTTP_USERNAME = None HTTP_USERNAME = None
@ -147,6 +149,7 @@ CHOWNER = None
CHGROUP = None CHGROUP = None
USENET_RETENTION = None USENET_RETENTION = None
CREATE_FOLDERS = True CREATE_FOLDERS = True
DELETE_REMOVE_DIR = False
ADD_COMICS = False ADD_COMICS = False
COMIC_DIR = None COMIC_DIR = None
@ -402,8 +405,8 @@ def initialize():
with INIT_LOCK: with INIT_LOCK:
global __INITIALIZED__, DBCHOICE, DBUSER, DBPASS, DBNAME, COMICVINE_API, DEFAULT_CVAPI, CVAPI_COUNT, CVAPI_TIME, CVAPI_MAX, FULL_PATH, PROG_DIR, VERBOSE, DAEMON, UPCOMING_SNATCHED, COMICSORT, DATA_DIR, CONFIG_FILE, CFG, CONFIG_VERSION, LOG_DIR, CACHE_DIR, MAX_LOGSIZE, LOGVERBOSE, OLDCONFIG_VERSION, OS_DETECT, \ global __INITIALIZED__, DBCHOICE, DBUSER, DBPASS, DBNAME, COMICVINE_API, DEFAULT_CVAPI, CVAPI_COUNT, CVAPI_TIME, CVAPI_MAX, FULL_PATH, PROG_DIR, VERBOSE, DAEMON, UPCOMING_SNATCHED, COMICSORT, DATA_DIR, CONFIG_FILE, CFG, CONFIG_VERSION, LOG_DIR, CACHE_DIR, MAX_LOGSIZE, LOGVERBOSE, OLDCONFIG_VERSION, OS_DETECT, \
queue, HTTP_PORT, HTTP_HOST, HTTP_USERNAME, HTTP_PASSWORD, HTTP_ROOT, ENABLE_HTTPS, HTTPS_CERT, HTTPS_KEY, HTTPS_FORCE_ON, HOST_RETURN, API_ENABLED, API_KEY, DOWNLOAD_APIKEY, LAUNCH_BROWSER, GIT_PATH, SAFESTART, AUTO_UPDATE, \ queue, LOCAL_IP, EXT_IP, HTTP_PORT, HTTP_HOST, HTTP_USERNAME, HTTP_PASSWORD, HTTP_ROOT, ENABLE_HTTPS, HTTPS_CERT, HTTPS_KEY, HTTPS_FORCE_ON, HOST_RETURN, API_ENABLED, API_KEY, DOWNLOAD_APIKEY, LAUNCH_BROWSER, GIT_PATH, SAFESTART, AUTO_UPDATE, \
CURRENT_VERSION, LATEST_VERSION, CHECK_GITHUB, CHECK_GITHUB_ON_STARTUP, CHECK_GITHUB_INTERVAL, GIT_USER, GIT_BRANCH, USER_AGENT, DESTINATION_DIR, MULTIPLE_DEST_DIRS, CREATE_FOLDERS, \ CURRENT_VERSION, LATEST_VERSION, CHECK_GITHUB, CHECK_GITHUB_ON_STARTUP, CHECK_GITHUB_INTERVAL, GIT_USER, GIT_BRANCH, USER_AGENT, DESTINATION_DIR, MULTIPLE_DEST_DIRS, CREATE_FOLDERS, DELETE_REMOVE_DIR, \
DOWNLOAD_DIR, USENET_RETENTION, SEARCH_INTERVAL, NZB_STARTUP_SEARCH, INTERFACE, DUPECONSTRAINT, AUTOWANT_ALL, AUTOWANT_UPCOMING, ZERO_LEVEL, ZERO_LEVEL_N, COMIC_COVER_LOCAL, HIGHCOUNT, \ DOWNLOAD_DIR, USENET_RETENTION, SEARCH_INTERVAL, NZB_STARTUP_SEARCH, INTERFACE, DUPECONSTRAINT, AUTOWANT_ALL, AUTOWANT_UPCOMING, ZERO_LEVEL, ZERO_LEVEL_N, COMIC_COVER_LOCAL, HIGHCOUNT, \
LIBRARYSCAN, LIBRARYSCAN_INTERVAL, DOWNLOAD_SCAN_INTERVAL, NZB_DOWNLOADER, USE_SABNZBD, SAB_HOST, SAB_USERNAME, SAB_PASSWORD, SAB_APIKEY, SAB_CATEGORY, SAB_PRIORITY, SAB_TO_MYLAR, SAB_DIRECTORY, USE_BLACKHOLE, BLACKHOLE_DIR, ADD_COMICS, COMIC_DIR, IMP_MOVE, IMP_RENAME, IMP_METADATA, \ LIBRARYSCAN, LIBRARYSCAN_INTERVAL, DOWNLOAD_SCAN_INTERVAL, NZB_DOWNLOADER, USE_SABNZBD, SAB_HOST, SAB_USERNAME, SAB_PASSWORD, SAB_APIKEY, SAB_CATEGORY, SAB_PRIORITY, SAB_TO_MYLAR, SAB_DIRECTORY, USE_BLACKHOLE, BLACKHOLE_DIR, ADD_COMICS, COMIC_DIR, IMP_MOVE, IMP_RENAME, IMP_METADATA, \
USE_NZBGET, NZBGET_HOST, NZBGET_PORT, NZBGET_USERNAME, NZBGET_PASSWORD, NZBGET_CATEGORY, NZBGET_PRIORITY, NZBGET_DIRECTORY, NZBSU, NZBSU_UID, NZBSU_APIKEY, DOGNZB, DOGNZB_APIKEY, \ USE_NZBGET, NZBGET_HOST, NZBGET_PORT, NZBGET_USERNAME, NZBGET_PASSWORD, NZBGET_CATEGORY, NZBGET_PRIORITY, NZBGET_DIRECTORY, NZBSU, NZBSU_UID, NZBSU_APIKEY, DOGNZB, DOGNZB_APIKEY, \
@ -491,6 +494,7 @@ def initialize():
DESTINATION_DIR = check_setting_str(CFG, 'General', 'destination_dir', '') DESTINATION_DIR = check_setting_str(CFG, 'General', 'destination_dir', '')
MULTIPLE_DEST_DIRS = check_setting_str(CFG, 'General', 'multiple_dest_dirs', '') MULTIPLE_DEST_DIRS = check_setting_str(CFG, 'General', 'multiple_dest_dirs', '')
CREATE_FOLDERS = bool(check_setting_int(CFG, 'General', 'create_folders', 1)) CREATE_FOLDERS = bool(check_setting_int(CFG, 'General', 'create_folders', 1))
DELETE_REMOVE_DIR = bool(check_setting_int(CFG, 'General', 'delete_remove_dir', 0))
CHMOD_DIR = check_setting_str(CFG, 'General', 'chmod_dir', '0777') CHMOD_DIR = check_setting_str(CFG, 'General', 'chmod_dir', '0777')
CHMOD_FILE = check_setting_str(CFG, 'General', 'chmod_file', '0660') CHMOD_FILE = check_setting_str(CFG, 'General', 'chmod_file', '0660')
CHOWNER = check_setting_str(CFG, 'General', 'chowner', '') CHOWNER = check_setting_str(CFG, 'General', 'chowner', '')
@ -923,6 +927,19 @@ def initialize():
# Start the logger, silence console logging if we need to # Start the logger, silence console logging if we need to
logger.initLogger(verbose=VERBOSE) #logger.mylar_log.initLogger(verbose=VERBOSE) logger.initLogger(verbose=VERBOSE) #logger.mylar_log.initLogger(verbose=VERBOSE)
#try to get the local IP using socket. Get this on every startup so it's at least current for existing session.
import socket
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(('8.8.8.8', 80))
LOCAL_IP = s.getsockname()[0]
s.close()
logger.info('Successfully discovered local IP and locking it in as : ' + str(LOCAL_IP))
except:
logger.warn('Unable to determine local IP - this might cause problems when downloading (maybe use host_return in the config.ini)')
LOCAL_IP = HTTP_HOST
# verbatim back the logger being used since it's now started. # verbatim back the logger being used since it's now started.
if LOGTYPE == 'clog': if LOGTYPE == 'clog':
logprog = 'Concurrent Rotational Log Handler' logprog = 'Concurrent Rotational Log Handler'
@ -1024,11 +1041,6 @@ def initialize():
logger.info('Remapping the sorting to allow for new additions.') logger.info('Remapping the sorting to allow for new additions.')
COMICSORT = helpers.ComicSort(sequence='startup') COMICSORT = helpers.ComicSort(sequence='startup')
#start the db write only thread here.
#this is a thread that continually runs in the background as the ONLY thread that can write to the db.
#logger.info('Starting Write-Only thread.')
#db.WriteOnly()
#initialize the scheduler threads here. #initialize the scheduler threads here.
dbUpdateScheduler = scheduler.Scheduler(action=dbupdater.dbUpdate(), dbUpdateScheduler = scheduler.Scheduler(action=dbupdater.dbUpdate(),
cycleTime=datetime.timedelta(hours=48), cycleTime=datetime.timedelta(hours=48),
@ -1191,6 +1203,7 @@ def config_write():
new_config['General']['destination_dir'] = DESTINATION_DIR new_config['General']['destination_dir'] = DESTINATION_DIR
new_config['General']['multiple_dest_dirs'] = MULTIPLE_DEST_DIRS new_config['General']['multiple_dest_dirs'] = MULTIPLE_DEST_DIRS
new_config['General']['create_folders'] = int(CREATE_FOLDERS) new_config['General']['create_folders'] = int(CREATE_FOLDERS)
new_config['General']['delete_remove_dir'] = int(DELETE_REMOVE_DIR)
new_config['General']['chmod_dir'] = CHMOD_DIR new_config['General']['chmod_dir'] = CHMOD_DIR
new_config['General']['chmod_file'] = CHMOD_FILE new_config['General']['chmod_file'] = CHMOD_FILE
new_config['General']['chowner'] = CHOWNER new_config['General']['chowner'] = CHOWNER

View File

@ -1239,7 +1239,7 @@ def setperms(path, dir=False):
os.chown(os.path.join(root, momo), chowner, chgroup) os.chown(os.path.join(root, momo), chowner, chgroup)
os.chmod(os.path.join(root, momo), permission) os.chmod(os.path.join(root, momo), permission)
logger.info('Successfully changed ownership and permissions [' + str(mylar.CHOWNER) + ':' + str(mylar.CHGROUP) + '] / [' + str(mylar.CHMOD_DIR) + ' / ' + str(mylar.CHMOD_FILE) + ']') logger.fdebug('Successfully changed ownership and permissions [' + str(mylar.CHOWNER) + ':' + str(mylar.CHGROUP) + '] / [' + str(mylar.CHMOD_DIR) + ' / ' + str(mylar.CHMOD_FILE) + ']')
else: else:
for root, dirs, files in os.walk(path): for root, dirs, files in os.walk(path):
@ -1250,7 +1250,7 @@ def setperms(path, dir=False):
permission = int(mylar.CHMOD_FILE, 8) permission = int(mylar.CHMOD_FILE, 8)
os.chmod(os.path.join(root, momo), permission) os.chmod(os.path.join(root, momo), permission)
logger.info('Successfully changed permissions [' + str(mylar.CHMOD_DIR) + ' / ' + str(mylar.CHMOD_FILE) + ']') logger.fdebug('Successfully changed permissions [' + str(mylar.CHMOD_DIR) + ' / ' + str(mylar.CHMOD_FILE) + ']')
except OSError: except OSError:
logger.error('Could not change permissions : ' + path + '. Exiting...') logger.error('Could not change permissions : ' + path + '. Exiting...')

View File

@ -26,6 +26,7 @@ import subprocess
import time import time
import lib.simplejson as simplejson import lib.simplejson as simplejson
import json import json
import lib.requests as requests
# This was obviously all taken from headphones with great appreciation :) # This was obviously all taken from headphones with great appreciation :)
@ -80,12 +81,7 @@ class PROWL:
#For uniformity reasons not removed #For uniformity reasons not removed
return return
def test(self, keys, priority): def test_notify(self):
self.enabled = True
self.keys = keys
self.priority = priority
self.notify('ZOMG Lazors Pewpewpew!', 'Test Message') self.notify('ZOMG Lazors Pewpewpew!', 'Test Message')
class NMA: class NMA:
@ -137,6 +133,9 @@ class NMA:
if not request: if not request:
logger.warn(module + ' Error sending notification request to NotifyMyAndroid') logger.warn(module + ' Error sending notification request to NotifyMyAndroid')
def test_notify(self):
self.notify(prline='Test Message',prline2='ZOMG Lazors Pewpewpew!')
# 2013-04-01 Added Pushover.net notifications, based on copy of Prowl class above. # 2013-04-01 Added Pushover.net notifications, based on copy of Prowl class above.
# No extra care has been put into API friendliness at the moment (read: https://pushover.net/api#friendly) # No extra care has been put into API friendliness at the moment (read: https://pushover.net/api#friendly)
class PUSHOVER: class PUSHOVER:
@ -198,13 +197,7 @@ class PUSHOVER:
logger.info(module + ' Pushover notification failed.') logger.info(module + ' Pushover notification failed.')
return False return False
def test(self, apikey, userkey, priority): def test_notify(self):
self.enabled = True
self.apikey = apikey
self.userkey = userkey
self.priority = priority
self.notify('ZOMG Lazors Pewpewpew!', 'Test Message') self.notify('ZOMG Lazors Pewpewpew!', 'Test Message')
@ -284,11 +277,20 @@ class BOXCAR:
self._sendBoxcar(message, title, module) self._sendBoxcar(message, title, module)
return True return True
def test_notify(self):
self.notify(prline='Test Message',prline2='ZOMG Lazors Pewpewpew!')
class PUSHBULLET: class PUSHBULLET:
def __init__(self): def __init__(self):
self.PUSH_URL = "https://api.pushbullet.com/v2/pushes"
self.apikey = mylar.PUSHBULLET_APIKEY self.apikey = mylar.PUSHBULLET_APIKEY
self.deviceid = mylar.PUSHBULLET_DEVICEID self.deviceid = mylar.PUSHBULLET_DEVICEID
self._json_header = {'Content-Type': 'application/json'}
self._session = requests.Session()
self._session.auth = (self.apikey, "")
self._session.headers.update(self._json_header)
def get_devices(self, api): def get_devices(self, api):
return self.notify(method="GET") return self.notify(method="GET")
@ -299,19 +301,20 @@ class PUSHBULLET:
if module is None: if module is None:
module = '' module = ''
module += '[NOTIFIER]' module += '[NOTIFIER]'
# http_handler = HTTPSConnection("api.pushbullet.com")
http_handler = HTTPSConnection("api.pushbullet.com") # if method == 'GET':
# uri = '/v2/devices'
# else:
# method = 'POST'
# uri = '/v2/pushes'
# authString = base64.b64encode(self.apikey + ":")
if method == 'GET': if method == 'GET':
uri = '/v2/devices' pass
else: # http_handler.request(method, uri, None, headers={'Authorization': 'Basic %s:' % authString})
method = 'POST'
uri = '/v2/pushes'
authString = base64.b64encode(self.apikey + ":")
if method == 'GET':
http_handler.request(method, uri, None, headers={'Authorization': 'Basic %s:' % authString})
else: else:
if snatched: if snatched:
if snatched[-1] == '.': snatched = snatched[:-1] if snatched[-1] == '.': snatched = snatched[:-1]
@ -325,37 +328,35 @@ class PUSHBULLET:
'title': event.encode('utf-8'), #"mylar", 'title': event.encode('utf-8'), #"mylar",
'body': message.encode('utf-8')} 'body': message.encode('utf-8')}
http_handler.request("POST", r = self._session.post(self.PUSH_URL, data=json.dumps(data))
"/v2/pushes",
headers = {'Content-type': "application/json",
'Authorization': 'Basic %s' % base64.b64encode(mylar.PUSHBULLET_APIKEY + ":")},
body = json.dumps(data))
response = http_handler.getresponse()
request_body = response.read()
request_status = response.status
#logger.fdebug(u"PushBullet response status: %r" % request_status)
#logger.fdebug(u"PushBullet response headers: %r" % response.getheaders())
#logger.fdebug(u"PushBullet response body: %r" % response.read())
if request_status == 200: # http_handler.request("POST",
# "/v2/pushes",
# headers = {'Content-type': "application/json",
# 'Authorization': 'Basic %s' % base64.b64encode(mylar.PUSHBULLET_APIKEY + ":")},
# body = json.dumps(data))
#
# response = http_handler.getresponse()
# request_body = response.read()
# request_status = response.status
# #logger.fdebug(u"PushBullet response status: %r" % request_status)
# #logger.fdebug(u"PushBullet response headers: %r" % response.getheaders())
# #logger.fdebug(u"PushBullet response body: %r" % response.read())
if r.status_code == 200:
if method == 'GET': if method == 'GET':
return request_body return r.json()
else: else:
logger.info(module + ' PushBullet notifications sent.') logger.info(module + ' PushBullet notifications sent.')
return True return True
elif request_status >= 400 and request_status < 500: elif r.status_code >= 400 and r.status_code < 500:
logger.error(module + ' PushBullet request failed: %s' % response.reason) logger.error(module + ' PushBullet request failed: %s' % r.content)
return False return False
else: else:
logger.error(module + ' PushBullet notification failed serverside.') logger.error(module + ' PushBullet notification failed serverside.')
return False return False
def test(self, apikey, deviceid): def test_notify(self):
self.notify(prline='Test Message', prline2='Release the Ninjas!')
self.enabled = True
self.apikey = apikey
self.deviceid = deviceid
self.notify('Main Screen Activate', 'Test Message')

View File

@ -16,7 +16,7 @@
from __future__ import division from __future__ import division
import mylar import mylar
from mylar import logger, db, updater, helpers, parseit, findcomicfeed, notifiers, rsscheck, Failed from mylar import logger, db, updater, helpers, parseit, findcomicfeed, notifiers, rsscheck, Failed, filechecker
import lib.feedparser as feedparser import lib.feedparser as feedparser
import urllib import urllib
@ -474,6 +474,10 @@ def NZB_SEARCH(ComicName, IssueNumber, ComicYear, SeriesYear, Publisher, IssueDa
if nzbprov == '': if nzbprov == '':
bb = "no results" bb = "no results"
elif nzbprov == '32P': elif nzbprov == '32P':
#cmname = re.sub("%20", " ", str(comsrc))
#bb = rsscheck.torrents(pickfeed='4', seriesname=cmname, issue=mod_isssearch)
#rss = "no"
#logger.info('bb returned: ' + str(bb))
bb = "no results" bb = "no results"
elif nzbprov == 'KAT': elif nzbprov == 'KAT':
cmname = re.sub("%20", " ", str(comsrc)) cmname = re.sub("%20", " ", str(comsrc))
@ -752,44 +756,64 @@ def NZB_SEARCH(ComicName, IssueNumber, ComicYear, SeriesYear, Publisher, IssueDa
ComVersChk = 0 ComVersChk = 0
ctchk = cleantitle.split() ctchk = cleantitle.split()
volfound = False
vol_label = None
fndcomicversion = None
for ct in ctchk: for ct in ctchk:
if ct.lower().startswith('v') and ct[1:].isdigit(): if any([ct.lower().startswith('v') and ct[1:].isdigit(), ct.lower()[:3] == 'vol', volfound == True]):
logger.fdebug("possible versioning..checking") if volfound == True:
#we hit a versioning # - account for it logger.fdebug('Split Volume label detected - ie. Vol 4. Attempting to adust.')
if ct[1:].isdigit(): if ct.isdigit():
if len(ct[1:]) == 4: #v2013 vol_label = vol_label + ' ' + str(ct)
logger.fdebug("Version detected as " + str(ct)) ct = 'v' + str(ct)
vers4year = "yes" #re.sub("[^0-9]", " ", str(ct)) #remove the v volfound == False
#cleantitle = re.sub(ct, "(" + str(vers4year) + ")", cleantitle) cleantitle = re.sub(vol_label, ct, cleantitle).strip()
#logger.fdebug("volumized cleantitle : " + cleantitle) tmpsplit = ct
versionfound = "yes" if tmpsplit.lower().startswith('vol'):
break logger.fdebug('volume detected - stripping and re-analzying for volume label.')
if '.' in tmpsplit:
tmpsplit = re.sub('.', '', tmpsplit).strip()
tmpsplit = re.sub('vol', '', tmpsplit.lower()).strip()
#if vol label set as 'Vol 4' it will obliterate the Vol, but pass over the '4' - set
#volfound to True so that it can loop back around.
if not tmpsplit.isdigit():
vol_label = ct #store the wording of how the Vol is defined so we can skip it later on.
volfound = True
continue
if len(tmpsplit[1:]) == 4 and tmpsplit[1:].isdigit(): #v2013
logger.fdebug("[Vxxxx] Version detected as " + str(tmpsplit))
vers4year = "yes" #re.sub("[^0-9]", " ", str(ct)) #remove the v
fndcomicversion = str(tmpsplit)
elif len(tmpsplit[1:]) == 1 and tmpsplit[1:].isdigit(): #v2
logger.fdebug("[Vx] Version detected as " + str(tmpsplit))
vers4vol = str(tmpsplit)
fndcomicversion = str(tmpsplit)
elif tmpsplit[1:].isdigit() and len(tmpsplit) < 4:
logger.fdebug('[Vxxx] Version detected as ' +str(tmpsplit))
vers4vol = str(tmpsplit)
fndcomicversion = str(tmpsplit)
elif tmpsplit.isdigit() and len(tmpsplit) <=4:
# this stuff is necessary for 32P volume manipulation
if len(tmpsplit) == 4:
vers4year = "yes"
fndcomicversion = str(tmpsplit)
elif len(tmpsplit) == 1:
vers4vol = str(tmpsplit)
fndcomicversion = str(tmpsplit)
elif len(tmpsplit) < 4:
vers4vol = str(tmpsplit)
fndcomicversion = str(tmpsplit)
else: else:
if len(ct) < 4: logger.fdebug("error - unknown length for : " + str(tmpsplit))
logger.fdebug("Version detected as " + str(ct)) continue
vers4vol = str(ct)
versionfound = "yes"
break
logger.fdebug("false version detection..ignoring.")
elif ct.lower()[:3] == 'vol':
#if in format vol.2013/vol2013/vol01/vol.1, etc
ct = re.sub('vol', '', ct.lower())
if '.' in ct: re.sub('.', '', ct).strip()
if ct.lower()[4:].isdigit():
logger.fdebug('volume indicator detected as version #:' + str(ct))
vers4year = "yes"
versionfound = "yes"
break
else: else:
vers4vol = ct logger.fdebug("error - unknown length for : " + str(tmpsplit))
versionfound = "yes" continue
logger.fdebug('volume indicator detected as version #:' + str(vers4vol))
break
logger.fdebug("false version detection..ignoring.")
if fndcomicversion:
versionfound = "yes"
break
if len(re.findall('[^()]+', cleantitle)) == 1 or 'cover only' in cleantitle.lower(): if len(re.findall('[^()]+', cleantitle)) == 1 or 'cover only' in cleantitle.lower():
#some sites don't have (2013) or whatever..just v2 / v2013. Let's adjust: #some sites don't have (2013) or whatever..just v2 / v2013. Let's adjust:
@ -1064,112 +1088,113 @@ def NZB_SEARCH(ComicName, IssueNumber, ComicYear, SeriesYear, Publisher, IssueDa
#splitst = splitst - 1 #splitst = splitst - 1
if versionfound == "yes": if versionfound == "yes":
volfound = False # volfound = False
for tstsplit in splitit: # vol_label = None
logger.fdebug('comparing ' + str(tstsplit)) # for tstsplit in splitit:
if volfound == True: # logger.fdebug('comparing ' + str(tstsplit))
logger.fdebug('Split Volume label detected - ie. Vol 4. Attempting to adust.') # if volfound == True:
if tstsplit.isdigit(): # logger.fdebug('Split Volume label detected - ie. Vol 4. Attempting to adust.')
tstsplit = 'v' + str(tstsplit) # if tstsplit.isdigit():
volfound == False # vol_label = vol_label + ' ' + str(tstsplit)
if tstsplit.lower().startswith('v'): #tstsplit[1:].isdigit(): # tstsplit = 'v' + str(tstsplit)
logger.fdebug("this has a version #...let's adjust") # volfound == False
tmpsplit = tstsplit # if tstsplit.lower().startswith('v'): #tstsplit[1:].isdigit():
if tmpsplit.lower().startswith('vol'): # logger.fdebug("this has a version #...let's adjust")
logger.fdebug('volume detected - stripping and re-analzying for volume label.') # tmpsplit = tstsplit
if '.' in tmpsplit: # if tmpsplit.lower().startswith('vol'):
tmpsplit = re.sub('.', '', tmpsplit).strip() # logger.fdebug('volume detected - stripping and re-analzying for volume label.')
tmpsplit = re.sub('vol', '', tmpsplit.lower()).strip() # if '.' in tmpsplit:
#if vol label set as 'Vol 4' it will obliterate the Vol, but pass over the '4' - set # tmpsplit = re.sub('.', '', tmpsplit).strip()
#volfound to True so that it can loop back around. # tmpsplit = re.sub('vol', '', tmpsplit.lower()).strip()
if not tmpsplit.isdigit(): # #if vol label set as 'Vol 4' it will obliterate the Vol, but pass over the '4' - set
volfound = True # #volfound to True so that it can loop back around.
continue # if not tmpsplit.isdigit():
if len(tmpsplit[1:]) == 4 and tmpsplit[1:].isdigit(): #v2013 # vol_label = tstsplit #store the wording of how the Vol is defined so we can skip it later on.
logger.fdebug("[Vxxxx] Version detected as " + str(tmpsplit)) # volfound = True
vers4year = "yes" #re.sub("[^0-9]", " ", str(ct)) #remove the v # continue
elif len(tmpsplit[1:]) == 1 and tmpsplit[1:].isdigit(): #v2 # if len(tmpsplit[1:]) == 4 and tmpsplit[1:].isdigit(): #v2013
logger.fdebug("[Vx] Version detected as " + str(tmpsplit)) # logger.fdebug("[Vxxxx] Version detected as " + str(tmpsplit))
vers4vol = str(tmpsplit) # vers4year = "yes" #re.sub("[^0-9]", " ", str(ct)) #remove the v
elif tmpsplit[1:].isdigit() and len(tmpsplit) < 4: # elif len(tmpsplit[1:]) == 1 and tmpsplit[1:].isdigit(): #v2
logger.fdebug('[Vxxx] Version detected as ' +str(tmpsplit)) # logger.fdebug("[Vx] Version detected as " + str(tmpsplit))
vers4vol = str(tmpsplit) # vers4vol = str(tmpsplit)
elif tmpsplit.isdigit() and len(tmpsplit) <=4: # elif tmpsplit[1:].isdigit() and len(tmpsplit) < 4:
# this stuff is necessary for 32P volume manipulation # logger.fdebug('[Vxxx] Version detected as ' +str(tmpsplit))
if len(tmpsplit) == 4: # vers4vol = str(tmpsplit)
vers4year = "yes" # elif tmpsplit.isdigit() and len(tmpsplit) <=4:
elif len(tmpsplit) == 1: # # this stuff is necessary for 32P volume manipulation
vers4vol = str(tmpsplit) # if len(tmpsplit) == 4:
elif len(tmpsplit) < 4: # vers4year = "yes"
vers4vol = str(tmpsplit) # elif len(tmpsplit) == 1:
else: # vers4vol = str(tmpsplit)
logger.fdebug("error - unknown length for : " + str(tmpsplit)) # elif len(tmpsplit) < 4:
continue # vers4vol = str(tmpsplit)
else: # else:
logger.fdebug("error - unknown length for : " + str(tmpsplit)) # logger.fdebug("error - unknown length for : " + str(tmpsplit))
continue # continue
# else:
# logger.fdebug("error - unknown length for : " + str(tmpsplit))
# continue
logger.fdebug("volume detection commencing - adjusting length.") logger.fdebug("volume detection commencing - adjusting length.")
logger.fdebug("watch comicversion is " + str(ComicVersion)) logger.fdebug("watch comicversion is " + str(ComicVersion))
fndcomicversion = str(tstsplit) logger.fdebug("version found: " + str(fndcomicversion))
logger.fdebug("version found: " + str(fndcomicversion)) logger.fdebug("vers4year: " + str(vers4year))
logger.fdebug("vers4year: " + str(vers4year)) logger.fdebug("vers4vol: " + str(vers4vol))
logger.fdebug("vers4vol: " + str(vers4vol))
if vers4year is not "no" or vers4vol is not "no": if vers4year is not "no" or vers4vol is not "no":
#if the volume is None, assume it's a V1 to increase % hits #if the volume is None, assume it's a V1 to increase % hits
if ComVersChk == 0: if ComVersChk == 0:
D_ComicVersion = 1 D_ComicVersion = 1
else: else:
D_ComicVersion = ComVersChk D_ComicVersion = ComVersChk
#if this is a one-off, SeriesYear will be None and cause errors. #if this is a one-off, SeriesYear will be None and cause errors.
if SeriesYear is None: if SeriesYear is None:
S_ComicVersion = 0 S_ComicVersion = 0
else: else:
S_ComicVersion = str(SeriesYear) S_ComicVersion = str(SeriesYear)
F_ComicVersion = re.sub("[^0-9]", "", fndcomicversion) F_ComicVersion = re.sub("[^0-9]", "", fndcomicversion)
#if the found volume is a vol.0, up it to vol.1 (since there is no V0) #if the found volume is a vol.0, up it to vol.1 (since there is no V0)
if F_ComicVersion == '0': if F_ComicVersion == '0':
#need to convert dates to just be yyyy-mm-dd and do comparison, time operator in the below calc as well which probably throws off some accuracy. #need to convert dates to just be yyyy-mm-dd and do comparison, time operator in the below calc as well which probably throws off some accuracy.
if postdate_int >= issuedate_int and nzbprov == '32P': if postdate_int >= issuedate_int and nzbprov == '32P':
logger.fdebug('32P torrent discovery. Store date (' + str(stdate) + ') is before posting date (' + str(pubdate) + '), forcing volume label to be the same as series label (0-Day Enforcement): v' + str(F_ComicVersion) + ' --> v' + str(S_ComicVersion)) logger.fdebug('32P torrent discovery. Store date (' + str(stdate) + ') is before posting date (' + str(pubdate) + '), forcing volume label to be the same as series label (0-Day Enforcement): v' + str(F_ComicVersion) + ' --> v' + str(S_ComicVersion))
F_ComicVersion = D_ComicVersion F_ComicVersion = D_ComicVersion
else: else:
F_ComicVersion = '1' F_ComicVersion = '1'
logger.fdebug("FCVersion: " + str(F_ComicVersion)) logger.fdebug("FCVersion: " + str(F_ComicVersion))
logger.fdebug("DCVersion: " + str(D_ComicVersion)) logger.fdebug("DCVersion: " + str(D_ComicVersion))
logger.fdebug("SCVersion: " + str(S_ComicVersion)) logger.fdebug("SCVersion: " + str(S_ComicVersion))
#here's the catch, sometimes annuals get posted as the Pub Year #here's the catch, sometimes annuals get posted as the Pub Year
# instead of the Series they belong to (V2012 vs V2013) # instead of the Series they belong to (V2012 vs V2013)
if annualize == "true" and int(ComicYear) == int(F_ComicVersion): if annualize == "true" and int(ComicYear) == int(F_ComicVersion):
logger.fdebug("We matched on versions for annuals " + str(fndcomicversion)) logger.fdebug("We matched on versions for annuals " + str(fndcomicversion))
scount+=1 scount+=1
cvers = "true" cvers = "true"
elif int(F_ComicVersion) == int(D_ComicVersion) or int(F_ComicVersion) == int(S_ComicVersion): elif int(F_ComicVersion) == int(D_ComicVersion) or int(F_ComicVersion) == int(S_ComicVersion):
logger.fdebug("We matched on versions..." + str(fndcomicversion)) logger.fdebug("We matched on versions..." + str(fndcomicversion))
scount+=1 scount+=1
cvers = "true" cvers = "true"
else: else:
logger.fdebug("Versions wrong. Ignoring possible match.") logger.fdebug("Versions wrong. Ignoring possible match.")
scount = 0 scount = 0
cvers = "false" cvers = "false"
if cvers == "true": if cvers == "true":
#since we matched on versions, let's remove it entirely to improve matching. #since we matched on versions, let's remove it entirely to improve matching.
logger.fdebug('Removing versioning from nzb filename to improve matching algorithims.') logger.fdebug('Removing versioning [' + fndcomicversion + '] from nzb filename to improve matching algorithims.')
cissb4vers = re.sub(tstsplit, "", comic_iss_b4).strip() cissb4vers = re.sub(fndcomicversion, "", comic_iss_b4).strip()
logger.fdebug('New b4split : ' + str(cissb4vers)) logger.fdebug('New b4split : ' + str(cissb4vers))
splitit = cissb4vers.split(None) splitit = cissb4vers.split(None)
splitst -=1 splitst -=1
break
#do an initial check #do an initial check
initialchk = 'ok' initialchk = 'ok'
@ -1623,7 +1648,7 @@ def nzbname_create(provider, title=None, info=None):
logger.fdebug('[SEARCHER] entry[title]: ' + title) logger.fdebug('[SEARCHER] entry[title]: ' + title)
#gotta replace & or escape it #gotta replace & or escape it
nzbname = re.sub("\&", 'and', title) nzbname = re.sub("\&", 'and', title)
nzbname = re.sub('[\,\:\?\']', '', nzbname) nzbname = re.sub('[\,\:\?\'\+]', '', nzbname)
nzbname = re.sub('[\(\)]', ' ', nzbname) nzbname = re.sub('[\(\)]', ' ', nzbname)
logger.fdebug('[SEARCHER] nzbname (remove chars): ' + nzbname) logger.fdebug('[SEARCHER] nzbname (remove chars): ' + nzbname)
nzbname = re.sub('.cbr', '', nzbname).strip() nzbname = re.sub('.cbr', '', nzbname).strip()
@ -1797,17 +1822,17 @@ def searcher(nzbprov, nzbname, comicinfo, link, IssueID, ComicID, tmpprov, direc
#make sure the cache directory exists - if not, create it (used for storing nzbs). #make sure the cache directory exists - if not, create it (used for storing nzbs).
if os.path.exists(mylar.CACHE_DIR): if os.path.exists(mylar.CACHE_DIR):
logger.fdebug("Cache Directory successfully found at : " + mylar.CACHE_DIR) logger.fdebug("Cache Directory successfully found at : " + mylar.CACHE_DIR + ". Ensuring proper permissions.")
pass #enforce the permissions here to ensure the lower portion writes successfully
filechecker.setperms(mylar.CACHE_DIR, True)
else: else:
#let's make the dir. #let's make the dir.
logger.fdebug("Could not locate Cache Directory, attempting to create at : " + mylar.CACHE_DIR) logger.fdebug("Could not locate Cache Directory, attempting to create at : " + mylar.CACHE_DIR)
try: try:
os.makedirs(mylar.CACHE_DIR) filechecker.validateAndCreateDirectory(mylar.CACHE_DIR, True)
logger.info("Temporary NZB Download Directory successfully created at: " + mylar.CACHE_DIR) logger.info("Temporary NZB Download Directory successfully created at: " + mylar.CACHE_DIR)
except OSError.e: except OSError:
if e.errno != errno.EEXIST: raise
raise
#save the nzb grabbed, so we can bypass all the 'send-url' crap. #save the nzb grabbed, so we can bypass all the 'send-url' crap.
if not nzbname.endswith('.nzb'): if not nzbname.endswith('.nzb'):
@ -1860,7 +1885,6 @@ def searcher(nzbprov, nzbname, comicinfo, link, IssueID, ComicID, tmpprov, direc
except (OSError, IOError): except (OSError, IOError):
logger.warn('Failed to move nzb into blackhole directory - check blackhole directory and/or permissions.') logger.warn('Failed to move nzb into blackhole directory - check blackhole directory and/or permissions.')
return "blackhole-fail" return "blackhole-fail"
logger.fdebug("filename saved to your blackhole as : " + nzbname) logger.fdebug("filename saved to your blackhole as : " + nzbname)
logger.info(u"Successfully sent .nzb to your Blackhole directory : " + os.path.join(mylar.BLACKHOLE_DIR, nzbname)) logger.info(u"Successfully sent .nzb to your Blackhole directory : " + os.path.join(mylar.BLACKHOLE_DIR, nzbname))
sent_to = "your Blackhole Directory" sent_to = "your Blackhole Directory"
@ -1939,37 +1963,69 @@ def searcher(nzbprov, nzbname, comicinfo, link, IssueID, ComicID, tmpprov, direc
mylar.DOWNLOAD_APIKEY = hashlib.sha224(str(random.getrandbits(256))).hexdigest()[0:32] mylar.DOWNLOAD_APIKEY = hashlib.sha224(str(random.getrandbits(256))).hexdigest()[0:32]
#generate the mylar host address if applicable. #generate the mylar host address if applicable.
if mylar.ENABLE_HTTPS:
proto = 'https://'
else:
proto = 'http://'
if mylar.HTTP_ROOT is None:
hroot = '/'
elif mylar.HTTP_ROOT.endswith('/'):
hroot = mylar.HTTP_ROOT
else:
if mylar.HTTP_ROOT != '/':
hroot = mylar.HTTP_ROOT + '/'
else:
hroot = mylar.HTTP_ROOT
if mylar.LOCAL_IP is None:
#if mylar's local, get the local IP using socket.
try:
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(('8.8.8.8', 80))
mylar.LOCAL_IP = s.getsockname()[0]
s.close()
except:
logger.warn('Unable to determine local IP. Defaulting to host address for Mylar provided as : ' + str(mylar.HTTP_HOST))
if mylar.HOST_RETURN: if mylar.HOST_RETURN:
#from lib.pystun import as stun #mylar has the return value already provided (easier and will work if it's right)
#sip = '0.0.0.0'
#port = int(mylar.HTTP_PORT)
#try:
# nat_type, ext_ip, ext_port = stun.get_ip_info(sip,port)
#except:
# logger.warn('Unable to retrieve External IP.')
if mylar.HOST_RETURN.endswith('/'): if mylar.HOST_RETURN.endswith('/'):
mylar_host = mylar.HOST_RETURN mylar_host = mylar.HOST_RETURN
else: else:
mylar_host = mylar.HOST_RETURN + '/' mylar_host = mylar.HOST_RETURN + '/'
else:
if mylar.ENABLE_HTTPS:
proto = 'https://'
else:
proto = 'http://'
if mylar.HTTP_ROOT is None: elif mylar.SAB_TO_MYLAR:
hroot = '/' #if sab & mylar are on different machines, check to see if they are local or external IP's provided for host.
elif mylar.HTTP_ROOT.endswith('/'): if mylar.HTTP_HOST == 'localhost' or mylar.HTTP_HOST == '0.0.0.0':
hroot = mylar.HTTP_ROOT #if mylar's local, use the local IP already assigned to LOCAL_IP.
mylar_host = proto + str(mylar.LOCAL_IP) + ':' + str(mylar.HTTP_PORT) + hroot
else: else:
if mylar.HTTP_ROOT != '/': if mylar.EXT_IP is None:
hroot = mylar.HTTP_ROOT + '/' #if mylar isn't local, get the external IP using pystun.
import lib.pystun as stun
sip = mylar.HTTP_HOST
port = int(mylar.HTTP_PORT)
try:
nat_type, ext_ip, ext_port = stun.get_ip_info(sip,port)
mylar_host = proto + str(ext_ip) + ':' + str(mylar.HTTP_PORT) + hroot
mylar.EXT_IP = ext_ip
except:
logger.warn('Unable to retrieve External IP - try using the host_return option in the config.ini.')
mylar_host = proto + str(mylar.HTTP_HOST) + ':' + str(mylar.HTTP_PORT) + hroot
else: else:
hroot = mylar.HTTP_ROOT mylar_host = proto + str(mylar.EXT_IP) + ':' + str(mylar.HTTP_PORT) + hroot
mylar_host = proto + str(mylar.HTTP_HOST) + ':' + str(mylar.HTTP_PORT) + hroot
else:
#if all else fails, drop it back to the basic host:port and try that.
if mylar.LOCAL_IP is None:
tmp_host = mylar.HTTP_HOST
else:
tmp_host = mylar.LOCAL_IP
mylar_host = proto + str(tmp_host) + ':' + str(mylar.HTTP_PORT) + hroot
fileURL = mylar_host + 'api?apikey=' + mylar.DOWNLOAD_APIKEY + '&cmd=downloadNZB&nzbname=' + nzbname fileURL = mylar_host + 'api?apikey=' + mylar.DOWNLOAD_APIKEY + '&cmd=downloadNZB&nzbname=' + nzbname
tmpapi = tmpapi + SABtype tmpapi = tmpapi + SABtype

View File

@ -34,7 +34,7 @@ import shutil
import mylar import mylar
from mylar import logger, db, importer, mb, search, filechecker, helpers, updater, parseit, weeklypull, PostProcessor, librarysync, moveit, Failed, readinglist #,rsscheck from mylar import logger, db, importer, mb, search, filechecker, helpers, updater, parseit, weeklypull, PostProcessor, librarysync, moveit, Failed, readinglist, notifiers #,rsscheck
import lib.simplejson as simplejson import lib.simplejson as simplejson
@ -145,16 +145,21 @@ class WebInterface(object):
} }
usethefuzzy = comic['UseFuzzy'] usethefuzzy = comic['UseFuzzy']
skipped2wanted = "0" skipped2wanted = "0"
if usethefuzzy is None: usethefuzzy = "0" if usethefuzzy is None:
usethefuzzy = "0"
force_continuing = comic['ForceContinuing'] force_continuing = comic['ForceContinuing']
if force_continuing is None: force_continuing = 0 if force_continuing is None:
force_continuing = 0
if mylar.DELETE_REMOVE_DIR is None:
mylar.DELETE_REMOVE_DIR = 0
comicConfig = { comicConfig = {
"comiclocation": mylar.COMIC_LOCATION, "comiclocation": mylar.COMIC_LOCATION,
"fuzzy_year0": helpers.radio(int(usethefuzzy), 0), "fuzzy_year0": helpers.radio(int(usethefuzzy), 0),
"fuzzy_year1": helpers.radio(int(usethefuzzy), 1), "fuzzy_year1": helpers.radio(int(usethefuzzy), 1),
"fuzzy_year2": helpers.radio(int(usethefuzzy), 2), "fuzzy_year2": helpers.radio(int(usethefuzzy), 2),
"skipped2wanted": helpers.checked(skipped2wanted), "skipped2wanted": helpers.checked(skipped2wanted),
"force_continuing": helpers.checked(force_continuing) "force_continuing": helpers.checked(force_continuing),
"delete_dir": helpers.checked(mylar.DELETE_REMOVE_DIR)
} }
if mylar.ANNUALS_ON: if mylar.ANNUALS_ON:
annuals = myDB.select("SELECT * FROM annuals WHERE ComicID=?", [ComicID]) annuals = myDB.select("SELECT * FROM annuals WHERE ComicID=?", [ComicID])
@ -762,38 +767,51 @@ class WebInterface(object):
logger.warn('Failed Download Handling is not enabled. Leaving Failed Download as-is.') logger.warn('Failed Download Handling is not enabled. Leaving Failed Download as-is.')
post_process.exposed = True post_process.exposed = True
def pauseArtist(self, ComicID): def pauseSeries(self, ComicID):
logger.info(u"Pausing comic: " + ComicID) logger.info(u"Pausing comic: " + ComicID)
myDB = db.DBConnection() myDB = db.DBConnection()
controlValueDict = {'ComicID': ComicID} controlValueDict = {'ComicID': ComicID}
newValueDict = {'Status': 'Paused'} newValueDict = {'Status': 'Paused'}
myDB.upsert("comics", newValueDict, controlValueDict) myDB.upsert("comics", newValueDict, controlValueDict)
raise cherrypy.HTTPRedirect("comicDetails?ComicID=%s" % ComicID) raise cherrypy.HTTPRedirect("comicDetails?ComicID=%s" % ComicID)
pauseArtist.exposed = True pauseSeries.exposed = True
def resumeArtist(self, ComicID): def resumeSeries(self, ComicID):
logger.info(u"Resuming comic: " + ComicID) logger.info(u"Resuming comic: " + ComicID)
myDB = db.DBConnection() myDB = db.DBConnection()
controlValueDict = {'ComicID': ComicID} controlValueDict = {'ComicID': ComicID}
newValueDict = {'Status': 'Active'} newValueDict = {'Status': 'Active'}
myDB.upsert("comics", newValueDict, controlValueDict) myDB.upsert("comics", newValueDict, controlValueDict)
raise cherrypy.HTTPRedirect("comicDetails?ComicID=%s" % ComicID) raise cherrypy.HTTPRedirect("comicDetails?ComicID=%s" % ComicID)
resumeArtist.exposed = True resumeSeries.exposed = True
def deleteArtist(self, ComicID): def deleteSeries(self, ComicID, delete_dir=None):
print delete_dir
myDB = db.DBConnection() myDB = db.DBConnection()
comic = myDB.selectone('SELECT * from comics WHERE ComicID=?', [ComicID]).fetchone() comic = myDB.selectone('SELECT * from comics WHERE ComicID=?', [ComicID]).fetchone()
if comic['ComicName'] is None: ComicName = "None" if comic['ComicName'] is None: ComicName = "None"
else: ComicName = comic['ComicName'] else: ComicName = comic['ComicName']
seriesdir = comic['ComicLocation']
logger.info(u"Deleting all traces of Comic: " + ComicName) logger.info(u"Deleting all traces of Comic: " + ComicName)
myDB.action('DELETE from comics WHERE ComicID=?', [ComicID]) myDB.action('DELETE from comics WHERE ComicID=?', [ComicID])
myDB.action('DELETE from issues WHERE ComicID=?', [ComicID]) myDB.action('DELETE from issues WHERE ComicID=?', [ComicID])
if mylar.ANNUALS_ON: if mylar.ANNUALS_ON:
myDB.action('DELETE from annuals WHERE ComicID=?', [ComicID]) myDB.action('DELETE from annuals WHERE ComicID=?', [ComicID])
myDB.action('DELETE from upcoming WHERE ComicID=?', [ComicID]) myDB.action('DELETE from upcoming WHERE ComicID=?', [ComicID])
if delete_dir: #mylar.DELETE_REMOVE_DIR:
logger.fdebug('Remove directory on series removal enabled.')
if os.path.exists(seriesdir):
logger.fdebug('Attempting to remove the directory and contents of : ' + seriesdir)
try:
shutil.rmtree(seriesdir)
except:
logger.warn('Unable to remove directory after removing series from Mylar.')
else:
logger.warn('Unable to remove directory as it does not exist in : ' + seriesdir)
helpers.ComicSort(sequence='update') helpers.ComicSort(sequence='update')
raise cherrypy.HTTPRedirect("home") raise cherrypy.HTTPRedirect("home")
deleteArtist.exposed = True deleteSeries.exposed = True
def wipenzblog(self, ComicID=None, IssueID=None): def wipenzblog(self, ComicID=None, IssueID=None):
myDB = db.DBConnection() myDB = db.DBConnection()
@ -4079,7 +4097,6 @@ class WebInterface(object):
group_metatag.exposed = True group_metatag.exposed = True
def CreateFolders(self, createfolders=None): def CreateFolders(self, createfolders=None):
print 'createfolders is ' + str(createfolders)
if createfolders: if createfolders:
mylar.CREATE_FOLDERS = int(createfolders) mylar.CREATE_FOLDERS = int(createfolders)
mylar.config_write() mylar.config_write()
@ -4105,5 +4122,50 @@ class WebInterface(object):
syncfiles.exposed = True syncfiles.exposed = True
def search_32p(self, search=None): def search_32p(self, search=None):
mylar.rsscheck.torrents(pickfeed='4', seriesname=search) return mylar.rsscheck.torrents(pickfeed='4', seriesname=search)
search_32p.exposed = True search_32p.exposed = True
def testNMA(self):
nma = notifiers.NMA()
result = nma.test_notify()
if result:
return "Successfully sent NMA test - check to make sure it worked"
else:
return "Error sending test message to NMA"
testNMA.exposed = True
def testprowl(self):
prowl = notifiers.prowl()
result = prowl.test_notify()
if result:
return "Successfully sent Prowl test - check to make sure it worked"
else:
return "Error sending test message to Prowl"
testprowl.exposed = True
def testboxcar(self):
boxcar = notifiers.boxcar()
result = boxcar.test_notify()
if result:
return "Successfully sent Boxcar test - check to make sure it worked"
else:
return "Error sending test message to Boxcar"
testboxcar.exposed = True
def testpushover(self):
pushover = notifiers.pushover()
result = pushover.test_notify()
if result:
return "Successfully sent Pushover test - check to make sure it worked"
else:
return "Error sending test message to Pushover"
testpushover.exposed = True
def testpushbullet(self):
pushbullet = notifiers.pushbullet()
result = pushbullet.test_notify()
if result:
return "Successfully sent Pushbullet test - check to make sure it worked"
else:
return "Error sending test message to Pushbullet"
testpushbullet.exposed = True