mirror of https://github.com/evilhero/mylar
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:
parent
ef037dce1e
commit
abcc4e88ec
|
@ -9,7 +9,18 @@
|
|||
<div id="subhead_container">
|
||||
<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_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:
|
||||
<a id="menu_link_refresh" onclick="doAjaxCall('manualRename?comicid=${comic['ComicID']}', $(this),'table')" data-success="Renaming files.">Rename Files</a>
|
||||
%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>
|
||||
%endif
|
||||
%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:
|
||||
<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
|
||||
%if annuals:
|
||||
<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-2">Download settings</a></li>
|
||||
<li><a href="#tabs-3">Edit Settings</a></li>
|
||||
</ul>
|
||||
<div id="tabs-1">
|
||||
</ul>
|
||||
|
||||
<div id="tabs-1">
|
||||
|
||||
|
||||
<table class="comictable" summary="Comic Details">
|
||||
|
@ -77,39 +89,39 @@
|
|||
%elif comic['ComicPublisher'] == 'Marvel':
|
||||
<img src="interfaces/default/images/publisherlogos/logo-marvel.jpg" align="right" alt="Marvel" height="50" width="100"/>
|
||||
%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':
|
||||
<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':
|
||||
<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':
|
||||
<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':
|
||||
<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':
|
||||
<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':
|
||||
<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':
|
||||
<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':
|
||||
<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':
|
||||
<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':
|
||||
<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':
|
||||
<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':
|
||||
<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':
|
||||
<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':
|
||||
<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':
|
||||
<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':
|
||||
<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
|
||||
<fieldset>
|
||||
<div>
|
||||
|
@ -359,7 +371,7 @@
|
|||
<table class="display" id="issue_table">
|
||||
<thead>
|
||||
<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="issuenumber">Number</th>
|
||||
<th id="issuename">Name</th>
|
||||
|
@ -467,6 +479,9 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="table_wrapper">
|
||||
|
||||
%if annuals:
|
||||
<h1>Annuals</h1>
|
||||
%for aninfo in annualinfo:
|
||||
|
@ -501,7 +516,7 @@
|
|||
-->
|
||||
<thead>
|
||||
<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="aissuenumber">Number</th>
|
||||
<th id="aissuename">Name</th>
|
||||
|
@ -533,7 +548,7 @@
|
|||
agrade = 'A'
|
||||
%>
|
||||
<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:
|
||||
annual_Number = annual['Int_IssueNumber']
|
||||
|
@ -624,7 +639,7 @@
|
|||
</form>
|
||||
</div>
|
||||
%endif
|
||||
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
<%def name="headIncludes()">
|
||||
|
@ -713,52 +728,12 @@
|
|||
</script>
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
function log( message ) {
|
||||
$( "<div>" ).text( message ).prependTo( "#log" );
|
||||
$( "#log" ).scrollTop( 0 );
|
||||
}
|
||||
|
||||
$( "#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" );
|
||||
}
|
||||
});
|
||||
});
|
||||
function openDelete() {
|
||||
$("#dialog").dialog({modal:true});
|
||||
};
|
||||
function deleteDirCheck() {
|
||||
return document.getElementById("deleteCheck").checked;
|
||||
}
|
||||
</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() {
|
||||
$( "#tabs" ).tabs();
|
||||
});
|
||||
$("#menu_link_delete").click(openDelete);
|
||||
initActions();
|
||||
$('#issue_table').dataTable(
|
||||
{
|
||||
|
|
|
@ -899,13 +899,16 @@
|
|||
<div class="row">
|
||||
<label>API key</label><input type="text" name="prowl_keys" value="${config['prowl_keys']}" size="50">
|
||||
</div>
|
||||
<div class="row checkbox">
|
||||
<input type="checkbox" name="prowl_onsnatch" value="1" ${config['prowl_onsnatch']} /><label>Notify on snatch?</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Priority (-2,-1,0,1 or 2):</label>
|
||||
<input type="text" name="prowl_priority" value="${config['prowl_priority']}" size="2">
|
||||
</div>
|
||||
<div class="row checkbox">
|
||||
<input type="checkbox" name="prowl_onsnatch" value="1" ${config['prowl_onsnatch']} /><label>Notify on snatch?</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Priority (-2,-1,0,1 or 2):</label>
|
||||
<input type="text" name="prowl_priority" value="${config['prowl_priority']}" size="2">
|
||||
</div>
|
||||
<div class="row">
|
||||
<input type="button" value="Test PROWL" id="prowl_test" />
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
|
@ -923,8 +926,8 @@
|
|||
<small>Separate multiple api keys with commas</small>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Priority</label>
|
||||
<select name="nma_priority">
|
||||
<label>Priority</label>
|
||||
<select name="nma_priority">
|
||||
%for x in [-2,-1,0,1,2]:
|
||||
<%
|
||||
if config['nma_priority'] == x:
|
||||
|
@ -943,19 +946,22 @@
|
|||
else:
|
||||
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
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<input type="button" value="Test NMA" id="nma_test" />
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
<div class="row checkbox">
|
||||
<input type="checkbox" name="pushover_enabled" id="pushover" value="1" ${config['pushover_enabled']} /><label>Enable Pushover Notifications</label>
|
||||
</div>
|
||||
<div id="pushoveroptions">
|
||||
<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">
|
||||
<input type="checkbox" name="pushover_enabled" id="pushover" value="1" ${config['pushover_enabled']} /><label>Enable Pushover Notifications</label>
|
||||
</div>
|
||||
<div id="pushoveroptions">
|
||||
<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">
|
||||
</div>
|
||||
|
@ -969,7 +975,10 @@
|
|||
<label>Priority (-2,-1,0,1 or 2):</label>
|
||||
<input type="text" name="pushover_priority" value="${config['pushover_priority']}" size="2">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<input type="button" value="Test Pushover" id="pushover_test" />
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
|
@ -983,6 +992,9 @@
|
|||
</div>
|
||||
<label>Boxcar Token</label>
|
||||
<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>
|
||||
</fieldset>
|
||||
|
@ -1007,6 +1019,9 @@
|
|||
<input type="button" class="btn" value="Update device list" id="getPushbulletDevices" />
|
||||
-->
|
||||
</div>
|
||||
<div class="row">
|
||||
<input type="button" value="Test Pushbullet" id="pushbullet_test" />
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</td>
|
||||
|
@ -1286,6 +1301,35 @@
|
|||
$('#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() {
|
||||
$( "#tabs" ).tabs();
|
||||
|
|
|
@ -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
|
|
@ -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)
|
|
@ -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()
|
|
@ -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()
|
|
@ -174,10 +174,10 @@ class PostProcessor(object):
|
|||
|
||||
def Process(self):
|
||||
module = self.module
|
||||
self._log("nzb name: " + str(self.nzb_name))
|
||||
self._log("nzb folder: " + str(self.nzb_folder))
|
||||
logger.fdebug(module + ' nzb name: ' + str(self.nzb_name))
|
||||
logger.fdebug(module + ' nzb folder: ' + str(self.nzb_folder))
|
||||
self._log("nzb name: " + self.nzb_name)
|
||||
self._log("nzb folder: " + self.nzb_folder)
|
||||
logger.fdebug(module + ' nzb name: ' + self.nzb_name)
|
||||
logger.fdebug(module + ' nzb folder: ' + self.nzb_folder)
|
||||
if mylar.USE_SABNZBD==0:
|
||||
logger.fdebug(module + ' Not using SABnzbd')
|
||||
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.')
|
||||
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
|
||||
IssArcID = 'S' + str(ml['IssueArcID'])
|
||||
|
@ -635,7 +635,7 @@ class PostProcessor(object):
|
|||
logger.fdebug('[NZBNAME]: ' + nzbname)
|
||||
#gotta replace & or escape it
|
||||
nzbname = re.sub("\&", 'and', nzbname)
|
||||
nzbname = re.sub('[\,\:\?\']', '', nzbname)
|
||||
nzbname = re.sub('[\,\:\?\'\+]', '', nzbname)
|
||||
nzbname = re.sub('[\(\)]', ' ', nzbname)
|
||||
logger.fdebug('[NZBNAME] nzbname (remove chars): ' + nzbname)
|
||||
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.')
|
||||
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)
|
||||
#delete entry from nzblog table
|
||||
myDB.action('DELETE from nzblog WHERE issueid=?', [issueid])
|
||||
|
@ -1223,7 +1223,7 @@ class PostProcessor(object):
|
|||
logger.fdebug(module + ' ext:' + ext)
|
||||
|
||||
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,
|
||||
"mode": 'stop'})
|
||||
return self.queue.put(self.valreturn)
|
||||
|
@ -1302,7 +1302,7 @@ class PostProcessor(object):
|
|||
self.valreturn.append({"self.log": self.log,
|
||||
"mode": 'stop'})
|
||||
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)
|
||||
else:
|
||||
#downtype = for use with updater on history table to set status to 'Post-Processed'
|
||||
|
|
|
@ -104,6 +104,8 @@ DONATEBUTTON = True
|
|||
PULLNEW = None
|
||||
ALT_PULL = False
|
||||
|
||||
LOCAL_IP = None
|
||||
EXT_IP = None
|
||||
HTTP_PORT = None
|
||||
HTTP_HOST = None
|
||||
HTTP_USERNAME = None
|
||||
|
@ -147,6 +149,7 @@ CHOWNER = None
|
|||
CHGROUP = None
|
||||
USENET_RETENTION = None
|
||||
CREATE_FOLDERS = True
|
||||
DELETE_REMOVE_DIR = False
|
||||
|
||||
ADD_COMICS = False
|
||||
COMIC_DIR = None
|
||||
|
@ -402,8 +405,8 @@ def initialize():
|
|||
|
||||
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, \
|
||||
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, \
|
||||
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, \
|
||||
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, 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, \
|
||||
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, \
|
||||
|
@ -491,6 +494,7 @@ def initialize():
|
|||
DESTINATION_DIR = check_setting_str(CFG, 'General', 'destination_dir', '')
|
||||
MULTIPLE_DEST_DIRS = check_setting_str(CFG, 'General', 'multiple_dest_dirs', '')
|
||||
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_FILE = check_setting_str(CFG, 'General', 'chmod_file', '0660')
|
||||
CHOWNER = check_setting_str(CFG, 'General', 'chowner', '')
|
||||
|
@ -923,6 +927,19 @@ def initialize():
|
|||
# Start the logger, silence console logging if we need to
|
||||
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.
|
||||
if LOGTYPE == 'clog':
|
||||
logprog = 'Concurrent Rotational Log Handler'
|
||||
|
@ -1024,11 +1041,6 @@ def initialize():
|
|||
logger.info('Remapping the sorting to allow for new additions.')
|
||||
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.
|
||||
dbUpdateScheduler = scheduler.Scheduler(action=dbupdater.dbUpdate(),
|
||||
cycleTime=datetime.timedelta(hours=48),
|
||||
|
@ -1191,6 +1203,7 @@ def config_write():
|
|||
new_config['General']['destination_dir'] = DESTINATION_DIR
|
||||
new_config['General']['multiple_dest_dirs'] = MULTIPLE_DEST_DIRS
|
||||
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_file'] = CHMOD_FILE
|
||||
new_config['General']['chowner'] = CHOWNER
|
||||
|
|
|
@ -1239,7 +1239,7 @@ def setperms(path, dir=False):
|
|||
os.chown(os.path.join(root, momo), chowner, chgroup)
|
||||
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:
|
||||
for root, dirs, files in os.walk(path):
|
||||
|
@ -1250,7 +1250,7 @@ def setperms(path, dir=False):
|
|||
permission = int(mylar.CHMOD_FILE, 8)
|
||||
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:
|
||||
logger.error('Could not change permissions : ' + path + '. Exiting...')
|
||||
|
|
|
@ -26,6 +26,7 @@ import subprocess
|
|||
import time
|
||||
import lib.simplejson as simplejson
|
||||
import json
|
||||
import lib.requests as requests
|
||||
|
||||
# This was obviously all taken from headphones with great appreciation :)
|
||||
|
||||
|
@ -80,12 +81,7 @@ class PROWL:
|
|||
#For uniformity reasons not removed
|
||||
return
|
||||
|
||||
def test(self, keys, priority):
|
||||
|
||||
self.enabled = True
|
||||
self.keys = keys
|
||||
self.priority = priority
|
||||
|
||||
def test_notify(self):
|
||||
self.notify('ZOMG Lazors Pewpewpew!', 'Test Message')
|
||||
|
||||
class NMA:
|
||||
|
@ -137,6 +133,9 @@ class NMA:
|
|||
if not request:
|
||||
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.
|
||||
# No extra care has been put into API friendliness at the moment (read: https://pushover.net/api#friendly)
|
||||
class PUSHOVER:
|
||||
|
@ -198,13 +197,7 @@ class PUSHOVER:
|
|||
logger.info(module + ' Pushover notification failed.')
|
||||
return False
|
||||
|
||||
def test(self, apikey, userkey, priority):
|
||||
|
||||
self.enabled = True
|
||||
self.apikey = apikey
|
||||
self.userkey = userkey
|
||||
self.priority = priority
|
||||
|
||||
def test_notify(self):
|
||||
self.notify('ZOMG Lazors Pewpewpew!', 'Test Message')
|
||||
|
||||
|
||||
|
@ -284,11 +277,20 @@ class BOXCAR:
|
|||
self._sendBoxcar(message, title, module)
|
||||
return True
|
||||
|
||||
def test_notify(self):
|
||||
self.notify(prline='Test Message',prline2='ZOMG Lazors Pewpewpew!')
|
||||
|
||||
class PUSHBULLET:
|
||||
|
||||
def __init__(self):
|
||||
self.PUSH_URL = "https://api.pushbullet.com/v2/pushes"
|
||||
self.apikey = mylar.PUSHBULLET_APIKEY
|
||||
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):
|
||||
return self.notify(method="GET")
|
||||
|
@ -299,19 +301,20 @@ class PUSHBULLET:
|
|||
if module is None:
|
||||
module = ''
|
||||
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':
|
||||
uri = '/v2/devices'
|
||||
else:
|
||||
method = 'POST'
|
||||
uri = '/v2/pushes'
|
||||
|
||||
authString = base64.b64encode(self.apikey + ":")
|
||||
|
||||
if method == 'GET':
|
||||
http_handler.request(method, uri, None, headers={'Authorization': 'Basic %s:' % authString})
|
||||
pass
|
||||
# http_handler.request(method, uri, None, headers={'Authorization': 'Basic %s:' % authString})
|
||||
else:
|
||||
if snatched:
|
||||
if snatched[-1] == '.': snatched = snatched[:-1]
|
||||
|
@ -325,37 +328,35 @@ class PUSHBULLET:
|
|||
'title': event.encode('utf-8'), #"mylar",
|
||||
'body': message.encode('utf-8')}
|
||||
|
||||
http_handler.request("POST",
|
||||
"/v2/pushes",
|
||||
headers = {'Content-type': "application/json",
|
||||
'Authorization': 'Basic %s' % base64.b64encode(mylar.PUSHBULLET_APIKEY + ":")},
|
||||
body = json.dumps(data))
|
||||
r = self._session.post(self.PUSH_URL, data=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':
|
||||
return request_body
|
||||
return r.json()
|
||||
else:
|
||||
logger.info(module + ' PushBullet notifications sent.')
|
||||
return True
|
||||
elif request_status >= 400 and request_status < 500:
|
||||
logger.error(module + ' PushBullet request failed: %s' % response.reason)
|
||||
elif r.status_code >= 400 and r.status_code < 500:
|
||||
logger.error(module + ' PushBullet request failed: %s' % r.content)
|
||||
return False
|
||||
else:
|
||||
logger.error(module + ' PushBullet notification failed serverside.')
|
||||
return False
|
||||
|
||||
def test(self, apikey, deviceid):
|
||||
|
||||
self.enabled = True
|
||||
self.apikey = apikey
|
||||
self.deviceid = deviceid
|
||||
|
||||
self.notify('Main Screen Activate', 'Test Message')
|
||||
def test_notify(self):
|
||||
self.notify(prline='Test Message', prline2='Release the Ninjas!')
|
||||
|
||||
|
|
376
mylar/search.py
376
mylar/search.py
|
@ -16,7 +16,7 @@
|
|||
from __future__ import division
|
||||
|
||||
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 urllib
|
||||
|
@ -474,6 +474,10 @@ def NZB_SEARCH(ComicName, IssueNumber, ComicYear, SeriesYear, Publisher, IssueDa
|
|||
if nzbprov == '':
|
||||
bb = "no results"
|
||||
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"
|
||||
elif nzbprov == 'KAT':
|
||||
cmname = re.sub("%20", " ", str(comsrc))
|
||||
|
@ -752,44 +756,64 @@ def NZB_SEARCH(ComicName, IssueNumber, ComicYear, SeriesYear, Publisher, IssueDa
|
|||
ComVersChk = 0
|
||||
|
||||
ctchk = cleantitle.split()
|
||||
volfound = False
|
||||
vol_label = None
|
||||
fndcomicversion = None
|
||||
for ct in ctchk:
|
||||
if ct.lower().startswith('v') and ct[1:].isdigit():
|
||||
logger.fdebug("possible versioning..checking")
|
||||
#we hit a versioning # - account for it
|
||||
if ct[1:].isdigit():
|
||||
if len(ct[1:]) == 4: #v2013
|
||||
logger.fdebug("Version detected as " + str(ct))
|
||||
vers4year = "yes" #re.sub("[^0-9]", " ", str(ct)) #remove the v
|
||||
#cleantitle = re.sub(ct, "(" + str(vers4year) + ")", cleantitle)
|
||||
#logger.fdebug("volumized cleantitle : " + cleantitle)
|
||||
versionfound = "yes"
|
||||
break
|
||||
if any([ct.lower().startswith('v') and ct[1:].isdigit(), ct.lower()[:3] == 'vol', volfound == True]):
|
||||
if volfound == True:
|
||||
logger.fdebug('Split Volume label detected - ie. Vol 4. Attempting to adust.')
|
||||
if ct.isdigit():
|
||||
vol_label = vol_label + ' ' + str(ct)
|
||||
ct = 'v' + str(ct)
|
||||
volfound == False
|
||||
cleantitle = re.sub(vol_label, ct, cleantitle).strip()
|
||||
tmpsplit = ct
|
||||
if tmpsplit.lower().startswith('vol'):
|
||||
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:
|
||||
if len(ct) < 4:
|
||||
logger.fdebug("Version detected as " + str(ct))
|
||||
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
|
||||
logger.fdebug("error - unknown length for : " + str(tmpsplit))
|
||||
continue
|
||||
else:
|
||||
vers4vol = ct
|
||||
versionfound = "yes"
|
||||
logger.fdebug('volume indicator detected as version #:' + str(vers4vol))
|
||||
break
|
||||
|
||||
logger.fdebug("false version detection..ignoring.")
|
||||
logger.fdebug("error - unknown length for : " + str(tmpsplit))
|
||||
continue
|
||||
|
||||
if fndcomicversion:
|
||||
versionfound = "yes"
|
||||
break
|
||||
|
||||
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:
|
||||
|
@ -1064,112 +1088,113 @@ def NZB_SEARCH(ComicName, IssueNumber, ComicYear, SeriesYear, Publisher, IssueDa
|
|||
#splitst = splitst - 1
|
||||
|
||||
if versionfound == "yes":
|
||||
volfound = False
|
||||
for tstsplit in splitit:
|
||||
logger.fdebug('comparing ' + str(tstsplit))
|
||||
if volfound == True:
|
||||
logger.fdebug('Split Volume label detected - ie. Vol 4. Attempting to adust.')
|
||||
if tstsplit.isdigit():
|
||||
tstsplit = 'v' + str(tstsplit)
|
||||
volfound == False
|
||||
if tstsplit.lower().startswith('v'): #tstsplit[1:].isdigit():
|
||||
logger.fdebug("this has a version #...let's adjust")
|
||||
tmpsplit = tstsplit
|
||||
if tmpsplit.lower().startswith('vol'):
|
||||
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():
|
||||
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
|
||||
elif len(tmpsplit[1:]) == 1 and tmpsplit[1:].isdigit(): #v2
|
||||
logger.fdebug("[Vx] Version detected as " + str(tmpsplit))
|
||||
vers4vol = str(tmpsplit)
|
||||
elif tmpsplit[1:].isdigit() and len(tmpsplit) < 4:
|
||||
logger.fdebug('[Vxxx] Version detected as ' +str(tmpsplit))
|
||||
vers4vol = str(tmpsplit)
|
||||
elif tmpsplit.isdigit() and len(tmpsplit) <=4:
|
||||
# this stuff is necessary for 32P volume manipulation
|
||||
if len(tmpsplit) == 4:
|
||||
vers4year = "yes"
|
||||
elif len(tmpsplit) == 1:
|
||||
vers4vol = str(tmpsplit)
|
||||
elif len(tmpsplit) < 4:
|
||||
vers4vol = str(tmpsplit)
|
||||
else:
|
||||
logger.fdebug("error - unknown length for : " + str(tmpsplit))
|
||||
continue
|
||||
else:
|
||||
logger.fdebug("error - unknown length for : " + str(tmpsplit))
|
||||
continue
|
||||
# volfound = False
|
||||
# vol_label = None
|
||||
# for tstsplit in splitit:
|
||||
# logger.fdebug('comparing ' + str(tstsplit))
|
||||
# if volfound == True:
|
||||
# logger.fdebug('Split Volume label detected - ie. Vol 4. Attempting to adust.')
|
||||
# if tstsplit.isdigit():
|
||||
# vol_label = vol_label + ' ' + str(tstsplit)
|
||||
# tstsplit = 'v' + str(tstsplit)
|
||||
# volfound == False
|
||||
# if tstsplit.lower().startswith('v'): #tstsplit[1:].isdigit():
|
||||
# logger.fdebug("this has a version #...let's adjust")
|
||||
# tmpsplit = tstsplit
|
||||
# if tmpsplit.lower().startswith('vol'):
|
||||
# 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 = tstsplit #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
|
||||
# elif len(tmpsplit[1:]) == 1 and tmpsplit[1:].isdigit(): #v2
|
||||
# logger.fdebug("[Vx] Version detected as " + str(tmpsplit))
|
||||
# vers4vol = str(tmpsplit)
|
||||
# elif tmpsplit[1:].isdigit() and len(tmpsplit) < 4:
|
||||
# logger.fdebug('[Vxxx] Version detected as ' +str(tmpsplit))
|
||||
# vers4vol = str(tmpsplit)
|
||||
# elif tmpsplit.isdigit() and len(tmpsplit) <=4:
|
||||
# # this stuff is necessary for 32P volume manipulation
|
||||
# if len(tmpsplit) == 4:
|
||||
# vers4year = "yes"
|
||||
# elif len(tmpsplit) == 1:
|
||||
# vers4vol = str(tmpsplit)
|
||||
# elif len(tmpsplit) < 4:
|
||||
# vers4vol = str(tmpsplit)
|
||||
# else:
|
||||
# logger.fdebug("error - unknown length for : " + str(tmpsplit))
|
||||
# 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))
|
||||
fndcomicversion = str(tstsplit)
|
||||
logger.fdebug("version found: " + str(fndcomicversion))
|
||||
logger.fdebug("vers4year: " + str(vers4year))
|
||||
logger.fdebug("vers4vol: " + str(vers4vol))
|
||||
logger.fdebug("watch comicversion is " + str(ComicVersion))
|
||||
logger.fdebug("version found: " + str(fndcomicversion))
|
||||
logger.fdebug("vers4year: " + str(vers4year))
|
||||
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 ComVersChk == 0:
|
||||
D_ComicVersion = 1
|
||||
else:
|
||||
D_ComicVersion = ComVersChk
|
||||
#if the volume is None, assume it's a V1 to increase % hits
|
||||
if ComVersChk == 0:
|
||||
D_ComicVersion = 1
|
||||
else:
|
||||
D_ComicVersion = ComVersChk
|
||||
|
||||
#if this is a one-off, SeriesYear will be None and cause errors.
|
||||
if SeriesYear is None:
|
||||
S_ComicVersion = 0
|
||||
else:
|
||||
S_ComicVersion = str(SeriesYear)
|
||||
#if this is a one-off, SeriesYear will be None and cause errors.
|
||||
if SeriesYear is None:
|
||||
S_ComicVersion = 0
|
||||
else:
|
||||
S_ComicVersion = str(SeriesYear)
|
||||
|
||||
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 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.
|
||||
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))
|
||||
F_ComicVersion = D_ComicVersion
|
||||
else:
|
||||
F_ComicVersion = '1'
|
||||
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 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.
|
||||
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))
|
||||
F_ComicVersion = D_ComicVersion
|
||||
else:
|
||||
F_ComicVersion = '1'
|
||||
|
||||
logger.fdebug("FCVersion: " + str(F_ComicVersion))
|
||||
logger.fdebug("DCVersion: " + str(D_ComicVersion))
|
||||
logger.fdebug("SCVersion: " + str(S_ComicVersion))
|
||||
logger.fdebug("FCVersion: " + str(F_ComicVersion))
|
||||
logger.fdebug("DCVersion: " + str(D_ComicVersion))
|
||||
logger.fdebug("SCVersion: " + str(S_ComicVersion))
|
||||
|
||||
#here's the catch, sometimes annuals get posted as the Pub Year
|
||||
# instead of the Series they belong to (V2012 vs V2013)
|
||||
if annualize == "true" and int(ComicYear) == int(F_ComicVersion):
|
||||
logger.fdebug("We matched on versions for annuals " + str(fndcomicversion))
|
||||
scount+=1
|
||||
cvers = "true"
|
||||
#here's the catch, sometimes annuals get posted as the Pub Year
|
||||
# instead of the Series they belong to (V2012 vs V2013)
|
||||
if annualize == "true" and int(ComicYear) == int(F_ComicVersion):
|
||||
logger.fdebug("We matched on versions for annuals " + str(fndcomicversion))
|
||||
scount+=1
|
||||
cvers = "true"
|
||||
|
||||
elif int(F_ComicVersion) == int(D_ComicVersion) or int(F_ComicVersion) == int(S_ComicVersion):
|
||||
logger.fdebug("We matched on versions..." + str(fndcomicversion))
|
||||
scount+=1
|
||||
cvers = "true"
|
||||
elif int(F_ComicVersion) == int(D_ComicVersion) or int(F_ComicVersion) == int(S_ComicVersion):
|
||||
logger.fdebug("We matched on versions..." + str(fndcomicversion))
|
||||
scount+=1
|
||||
cvers = "true"
|
||||
|
||||
else:
|
||||
logger.fdebug("Versions wrong. Ignoring possible match.")
|
||||
scount = 0
|
||||
cvers = "false"
|
||||
else:
|
||||
logger.fdebug("Versions wrong. Ignoring possible match.")
|
||||
scount = 0
|
||||
cvers = "false"
|
||||
|
||||
if cvers == "true":
|
||||
#since we matched on versions, let's remove it entirely to improve matching.
|
||||
logger.fdebug('Removing versioning from nzb filename to improve matching algorithims.')
|
||||
cissb4vers = re.sub(tstsplit, "", comic_iss_b4).strip()
|
||||
logger.fdebug('New b4split : ' + str(cissb4vers))
|
||||
splitit = cissb4vers.split(None)
|
||||
splitst -=1
|
||||
break
|
||||
if cvers == "true":
|
||||
#since we matched on versions, let's remove it entirely to improve matching.
|
||||
logger.fdebug('Removing versioning [' + fndcomicversion + '] from nzb filename to improve matching algorithims.')
|
||||
cissb4vers = re.sub(fndcomicversion, "", comic_iss_b4).strip()
|
||||
logger.fdebug('New b4split : ' + str(cissb4vers))
|
||||
splitit = cissb4vers.split(None)
|
||||
splitst -=1
|
||||
|
||||
#do an initial check
|
||||
initialchk = 'ok'
|
||||
|
@ -1623,7 +1648,7 @@ def nzbname_create(provider, title=None, info=None):
|
|||
logger.fdebug('[SEARCHER] entry[title]: ' + title)
|
||||
#gotta replace & or escape it
|
||||
nzbname = re.sub("\&", 'and', title)
|
||||
nzbname = re.sub('[\,\:\?\']', '', nzbname)
|
||||
nzbname = re.sub('[\,\:\?\'\+]', '', nzbname)
|
||||
nzbname = re.sub('[\(\)]', ' ', nzbname)
|
||||
logger.fdebug('[SEARCHER] nzbname (remove chars): ' + nzbname)
|
||||
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).
|
||||
if os.path.exists(mylar.CACHE_DIR):
|
||||
logger.fdebug("Cache Directory successfully found at : " + mylar.CACHE_DIR)
|
||||
pass
|
||||
logger.fdebug("Cache Directory successfully found at : " + mylar.CACHE_DIR + ". Ensuring proper permissions.")
|
||||
#enforce the permissions here to ensure the lower portion writes successfully
|
||||
filechecker.setperms(mylar.CACHE_DIR, True)
|
||||
else:
|
||||
#let's make the dir.
|
||||
logger.fdebug("Could not locate Cache Directory, attempting to create at : " + mylar.CACHE_DIR)
|
||||
try:
|
||||
os.makedirs(mylar.CACHE_DIR)
|
||||
filechecker.validateAndCreateDirectory(mylar.CACHE_DIR, True)
|
||||
logger.info("Temporary NZB Download Directory successfully created at: " + mylar.CACHE_DIR)
|
||||
except OSError.e:
|
||||
if e.errno != errno.EEXIST:
|
||||
raise
|
||||
except OSError:
|
||||
raise
|
||||
|
||||
#save the nzb grabbed, so we can bypass all the 'send-url' crap.
|
||||
if not nzbname.endswith('.nzb'):
|
||||
|
@ -1860,7 +1885,6 @@ def searcher(nzbprov, nzbname, comicinfo, link, IssueID, ComicID, tmpprov, direc
|
|||
except (OSError, IOError):
|
||||
logger.warn('Failed to move nzb into blackhole directory - check blackhole directory and/or permissions.')
|
||||
return "blackhole-fail"
|
||||
|
||||
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))
|
||||
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]
|
||||
|
||||
#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:
|
||||
#from lib.pystun import as stun
|
||||
#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.')
|
||||
|
||||
#mylar has the return value already provided (easier and will work if it's right)
|
||||
if mylar.HOST_RETURN.endswith('/'):
|
||||
mylar_host = mylar.HOST_RETURN
|
||||
else:
|
||||
mylar_host = mylar.HOST_RETURN + '/'
|
||||
|
||||
else:
|
||||
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
|
||||
elif mylar.SAB_TO_MYLAR:
|
||||
#if sab & mylar are on different machines, check to see if they are local or external IP's provided for host.
|
||||
if mylar.HTTP_HOST == 'localhost' or mylar.HTTP_HOST == '0.0.0.0':
|
||||
#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:
|
||||
if mylar.HTTP_ROOT != '/':
|
||||
hroot = mylar.HTTP_ROOT + '/'
|
||||
if mylar.EXT_IP is None:
|
||||
#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:
|
||||
hroot = mylar.HTTP_ROOT
|
||||
mylar_host = proto + str(mylar.HTTP_HOST) + ':' + str(mylar.HTTP_PORT) + hroot
|
||||
mylar_host = proto + str(mylar.EXT_IP) + ':' + 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
|
||||
|
||||
tmpapi = tmpapi + SABtype
|
||||
|
|
|
@ -34,7 +34,7 @@ import shutil
|
|||
|
||||
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
|
||||
|
||||
|
@ -145,16 +145,21 @@ class WebInterface(object):
|
|||
}
|
||||
usethefuzzy = comic['UseFuzzy']
|
||||
skipped2wanted = "0"
|
||||
if usethefuzzy is None: usethefuzzy = "0"
|
||||
if usethefuzzy is None:
|
||||
usethefuzzy = "0"
|
||||
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 = {
|
||||
"comiclocation": mylar.COMIC_LOCATION,
|
||||
"fuzzy_year0": helpers.radio(int(usethefuzzy), 0),
|
||||
"fuzzy_year1": helpers.radio(int(usethefuzzy), 1),
|
||||
"fuzzy_year2": helpers.radio(int(usethefuzzy), 2),
|
||||
"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:
|
||||
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.')
|
||||
post_process.exposed = True
|
||||
|
||||
def pauseArtist(self, ComicID):
|
||||
def pauseSeries(self, ComicID):
|
||||
logger.info(u"Pausing comic: " + ComicID)
|
||||
myDB = db.DBConnection()
|
||||
controlValueDict = {'ComicID': ComicID}
|
||||
newValueDict = {'Status': 'Paused'}
|
||||
myDB.upsert("comics", newValueDict, controlValueDict)
|
||||
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)
|
||||
myDB = db.DBConnection()
|
||||
controlValueDict = {'ComicID': ComicID}
|
||||
newValueDict = {'Status': 'Active'}
|
||||
myDB.upsert("comics", newValueDict, controlValueDict)
|
||||
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()
|
||||
comic = myDB.selectone('SELECT * from comics WHERE ComicID=?', [ComicID]).fetchone()
|
||||
if comic['ComicName'] is None: ComicName = "None"
|
||||
else: ComicName = comic['ComicName']
|
||||
seriesdir = comic['ComicLocation']
|
||||
logger.info(u"Deleting all traces of Comic: " + ComicName)
|
||||
myDB.action('DELETE from comics WHERE ComicID=?', [ComicID])
|
||||
myDB.action('DELETE from issues WHERE ComicID=?', [ComicID])
|
||||
if mylar.ANNUALS_ON:
|
||||
myDB.action('DELETE from annuals 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')
|
||||
raise cherrypy.HTTPRedirect("home")
|
||||
deleteArtist.exposed = True
|
||||
deleteSeries.exposed = True
|
||||
|
||||
def wipenzblog(self, ComicID=None, IssueID=None):
|
||||
myDB = db.DBConnection()
|
||||
|
@ -4079,7 +4097,6 @@ class WebInterface(object):
|
|||
group_metatag.exposed = True
|
||||
|
||||
def CreateFolders(self, createfolders=None):
|
||||
print 'createfolders is ' + str(createfolders)
|
||||
if createfolders:
|
||||
mylar.CREATE_FOLDERS = int(createfolders)
|
||||
mylar.config_write()
|
||||
|
@ -4105,5 +4122,50 @@ class WebInterface(object):
|
|||
syncfiles.exposed = True
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
|
Loading…
Reference in New Issue