Merge branch 'develop'

* develop:
  Release v0.5.1
  Prepare for next release
  Fixed HTML div closing tag. Updated titles
  Fixed typo
  Allow one to disable all tasks
  Allow one to adjust interval settings without restarting.
  Prevent infinite recursion while scanning library.
  Fix for new paths
  Added the real pkg_resources from setuptools 7.0. Should fix #2001
  Fixed contrib scripts. Added *.pyc cleaning scripts
  Check if pyOpenSSL is installed before enabling HTTPS.
  Ignore CSR files
  Fixed SSL too long errors in CherryPy.
  Spaces removal
  Add downgrade functionality (contributed script)
  Use urlparse to parse transmission URL
  Fix for #1998
This commit is contained in:
Bas Stottelaar
2014-11-13 20:00:27 +01:00
16 changed files with 3164 additions and 145 deletions
+3 -4
View File
@@ -6,12 +6,13 @@
*.pyproj *.pyproj
*.sln *.sln
# Logs and databases # # Headphones files #
###################### ######################
*.log *.log
*.db* *.db*
*.db-journal *.db-journal
*.ini *.ini
version.lock
logs/* logs/*
cache/* cache/*
@@ -19,6 +20,7 @@ cache/*
################## ##################
*.crt *.crt
*.key *.key
*.csr
# OS generated files # # OS generated files #
###################### ######################
@@ -64,6 +66,3 @@ _ReSharper*/
/logs /logs
.project .project
.pydevproject .pydevproject
headphones_docs
+13
View File
@@ -1,5 +1,18 @@
# Changelog # Changelog
## v0.5.1
Released 13 November 2014
Highlights:
* Added: allow one to disable interval tasks (#2002)
* Added: script to downgrade Headphones to last version that started (Linux only)
* Fixed: SSL issues in CherryPy. Self-generated certificates will be 2048 now (#1995)
* Fixed: Transmission URL detection (#1998)
* Fixed: missing dependencies for APScheduler and CherryPy (#2001)
* Improved: symlink infinite recursion detection
The full list of commits can be found [here](https://github.com/rembo10/headphones/compare/v0.5...v0.5.1).
## v0.5 ## v0.5
Released 10 November 2014 Released 10 November 2014
+2 -2
View File
@@ -9,7 +9,7 @@ In case you read this because you are posting an issue, please take a minute and
* Close your issue if you resolved it. * Close your issue if you resolved it.
## For developers ## For developers
If you think you can contribute code to the Headphones repository, do not hesitate to submit a pull request. If you think you can contribute code to the Headphones repository, do not hesitate to submit a pull request.
### Branches ### Branches
All pull requests should be based on the `develop` branch. When you want to develop a new feature, clone the repository with `git clone origin/develop -b FEATURE_NAME`. Use meaningful commit messages. All pull requests should be based on the `develop` branch. When you want to develop a new feature, clone the repository with `git clone origin/develop -b FEATURE_NAME`. Use meaningful commit messages.
@@ -31,5 +31,5 @@ Altough Headphones did not adapt a code convention in the past, we try to follow
* `_private_something` * `_private_something`
* `self.__really_private_field` * `self.__really_private_field`
* `_global` * `_global`
Document your code! Document your code!
+10
View File
@@ -164,6 +164,16 @@ def main():
else: else:
http_port = int(headphones.CONFIG.HTTP_PORT) http_port = int(headphones.CONFIG.HTTP_PORT)
# Check if pyOpenSSL is installed. It is required for certificate generation
# and for CherryPy.
if headphones.CONFIG.ENABLE_HTTPS:
try:
import OpenSSL
except ImportError:
logger.warn("The pyOpenSSL module is missing. Install this " \
"module to enable HTTPS. HTTPS will be disabled.")
headphones.CONFIG.ENABLE_HTTPS = False
# Try to start the server. Will exit here is address is already in use. # Try to start the server. Will exit here is address is already in use.
web_config = { web_config = {
'http_port': http_port, 'http_port': http_port,
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
# Display information
echo "This script will remove *.pyc files. These files are generated by Python, but they can cause conflicts after an upgrade. It's safe to remove them, because they will be regenerated."
echo "Press enter to continue, or CTRL + C to quit."
read
# Remove the *.pyc
find "`dirname $0`/.." -type f -name "*.pyc" -exec rm -rf {} \;
+32
View File
@@ -0,0 +1,32 @@
#!/bin/bash
# Parameter check
if [ -z "$1" ]; then
echo "Syntax: $0 <data directory>"
exit 1
fi
# Version file check
if [ ! -s "$1/version.lock" ]; then
echo "Missing the version.lock file in the data folder, or the file is empty. Did you start Headphones at least once?"
exit 1
fi
# Git installation check
if [ ! -x "$(command -v git)" ]; then
echo "Git is required to downgrade."
exit 1
fi
# Display information
HASH=$(cat $1/version.lock)
echo "This script will try to downgrade Headphones to the last version that started, version $HASH. Make sure you have a backup of your config file and database, just in case!"
echo "Press enter to continue, or CTRL + C to quit."
read
# Downgrade
cd "`dirname $0`/.."
git reset --hard "$HASH"
echo "All done, Headphones should be downgraded to the last version that started."
+39 -31
View File
@@ -101,6 +101,7 @@
<fieldset> <fieldset>
<legend>Interval</legend> <legend>Interval</legend>
<small>An interval of 0 will disable a task.</small>
<div class="row"> <div class="row">
<label title="Time between two searches for new downloads."> <label title="Time between two searches for new downloads.">
Search Interval Search Interval
@@ -137,7 +138,7 @@
</tr> </tr>
<tr> <tr>
<div class="configmessage"> <div class="configmessage">
<i class="fa fa-info-circle"></i> Web Interface changes require a restart to take effect <i class="fa fa-info-circle"></i> Web Interface changes require a restart to take effect. Saving settings will restart intervals.
</div> </div>
</tr> </tr>
</table> </table>
@@ -266,7 +267,7 @@
prio_force = '' prio_force = ''
%> %>
<div class="row"> <div class="row">
<label>NZBget Priority:</label> <label>NZBget Priority</label>
<select name="nzbget_priority" id="nzbget_priority"> <select name="nzbget_priority" id="nzbget_priority">
<option value="-100" ${prio_verylow}>Very Low</option> <option value="-100" ${prio_verylow}>Very Low</option>
<option value="-50" ${prio_low}>Low</option> <option value="-50" ${prio_low}>Low</option>
@@ -294,7 +295,7 @@
Music Download Directory: Music Download Directory:
</label> </label>
<input type="text" name="download_dir" value="${config['download_dir']}" size="50"> <input type="text" name="download_dir" value="${config['download_dir']}" size="50">
<small>Full path where SAB or NZBget downloads your music. e.g. /Users/name/Downloads/music</small> <small>Full path where SAB or NZBget downloads your music, e.g. /Users/name/Downloads/music</small>
</div> </div>
<div class="row"> <div class="row">
<label title="Number of days of retention your usenet provider provides."> <label title="Number of days of retention your usenet provider provides.">
@@ -381,14 +382,14 @@
</fieldset> </fieldset>
<fieldset id="general_torrent_options"> <fieldset id="general_torrent_options">
<div class="row"> <div class="row">
<label>Minimum seeders:</label> <label>Minimum seeders</label>
<input type="text" name="numberofseeders" value="${config['numberofseeders']}" size="5"> <input type="text" name="numberofseeders" value="${config['numberofseeders']}" size="5">
<small>Number of minimum seeders a torrent must have to be accepted</small> <small>Number of minimum seeders a torrent must have to be accepted</small>
</div> </div>
<div class="row"> <div class="row">
<label>Music Download Directory</label> <label>Music Download Directory</label>
<input type="text" name="download_torrent_dir" value="${config['download_torrent_dir']}" size="50"> <input type="text" name="download_torrent_dir" value="${config['download_torrent_dir']}" size="50">
<small>Full path where your torrent client downloads your music e.g. '/Users/name/Downloads/music'</small> <small>Full path where your torrent client downloads your music e.g. /Users/name/Downloads/music.</small>
</div> </div>
<div class="row checkbox"> <div class="row checkbox">
<label>Keep Files for Seeding</label> <label>Keep Files for Seeding</label>
@@ -526,11 +527,12 @@
</div> </div>
<div class="config"> <div class="config">
<div class="row"> <div class="row">
<label>Proxy URL (Optional): </label> <label>Proxy URL</label>
<input type="text" name="piratebay_proxy_url" value="${config['piratebay_proxy_url']}" size="36"> <input type="text" name="piratebay_proxy_url" value="${config['piratebay_proxy_url']}" size="36">
<small>Optional. Leave empty for default.</small>
</div> </div>
<div class="row"> <div class="row">
<label>Seed Ratio: </label> <label>Seed Ratio</label>
<input type="text" class="override-float" name="piratebay_ratio" value="${config['piratebay_ratio']}" size="10" title="Stop seeding when ratio met, 0 = unlimited. Scheduled job will remove torrent when post processed and finished seeding"> <input type="text" class="override-float" name="piratebay_ratio" value="${config['piratebay_ratio']}" size="10" title="Stop seeding when ratio met, 0 = unlimited. Scheduled job will remove torrent when post processed and finished seeding">
</div> </div>
</div> </div>
@@ -543,11 +545,12 @@
</div> </div>
<div class="config"> <div class="config">
<div class="row"> <div class="row">
<label>Proxy URL (Optional): </label> <label>Proxy URL</label>
<input type="text" name="kat_proxy_url" value="${config['kat_proxy_url']}" size="36"> <input type="text" name="kat_proxy_url" value="${config['kat_proxy_url']}" size="36">
<small>Optional. Leave empty for default.</small>
</div> </div>
<div class="row"> <div class="row">
<label>Seed Ratio: </label> <label>Seed Ratio</label>
<input type="text" class="override-float" name="kat_ratio" value="${config['kat_ratio']}" size="10" title="Stop seeding when ratio met, 0 = unlimited. Scheduled job will remove torrent when post processed and finished seeding"> <input type="text" class="override-float" name="kat_ratio" value="${config['kat_ratio']}" size="10" title="Stop seeding when ratio met, 0 = unlimited. Scheduled job will remove torrent when post processed and finished seeding">
</div> </div>
</div> </div>
@@ -560,15 +563,15 @@
</div> </div>
<div class="config"> <div class="config">
<div class="row"> <div class="row">
<label>UID Number: </label> <label>UID Number</label>
<input type="text" name="waffles_uid" value="${config['waffles_uid']}" size="36"> <input type="text" name="waffles_uid" value="${config['waffles_uid']}" size="36">
</div> </div>
<div class="row"> <div class="row">
<label>Passkey: </label> <label>Passkey</label>
<input type="password" name="waffles_passkey" value="${config['waffles_passkey']}" size="36"> <input type="password" name="waffles_passkey" value="${config['waffles_passkey']}" size="36">
</div> </div>
<div class="row"> <div class="row">
<label>Seed Ratio: </label> <label>Seed Ratio</label>
<input type="text" class="override-float" name="waffles_ratio" value="${config['waffles_ratio']}" size="10" title="Stop seeding when ratio met, 0 = unlimited. Scheduled job will remove torrent when post processed and finished seeding"> <input type="text" class="override-float" name="waffles_ratio" value="${config['waffles_ratio']}" size="10" title="Stop seeding when ratio met, 0 = unlimited. Scheduled job will remove torrent when post processed and finished seeding">
</div> </div>
</div> </div>
@@ -581,15 +584,15 @@
</div> </div>
<div class="config"> <div class="config">
<div class="row"> <div class="row">
<label>User Name: </label> <label>Userame</label>
<input type="text" name="rutracker_user" value="${config['rutracker_user']}" size="36"> <input type="text" name="rutracker_user" value="${config['rutracker_user']}" size="36">
</div> </div>
<div class="row"> <div class="row">
<label>Password: </label> <label>Password</label>
<input type="password" name="rutracker_password" value="${config['rutracker_password']}" size="36"> <input type="password" name="rutracker_password" value="${config['rutracker_password']}" size="36">
</div> </div>
<div class="row"> <div class="row">
<label>Seed Ratio: </label> <label>Seed Ratio</label>
<input type="text" class="override-float" name="rutracker_ratio" value="${config['rutracker_ratio']}" size="10" title="Stop seeding when ratio met, 0 = unlimited. Scheduled job will remove torrent when post processed and finished seeding"> <input type="text" class="override-float" name="rutracker_ratio" value="${config['rutracker_ratio']}" size="10" title="Stop seeding when ratio met, 0 = unlimited. Scheduled job will remove torrent when post processed and finished seeding">
</div> </div>
</div> </div>
@@ -688,7 +691,7 @@
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>Search Words</legend> <legend>Search Words</legend>
<small>Separate words with a comma, e.g. "word1,word2,word3"</small> <small>Separate words with a comma, e.g. "word1,word2,word3".</small>
<div class="row"> <div class="row">
<label>Ignored Words</label> <label>Ignored Words</label>
<input type="text" name="ignored_words" value="${config['ignored_words']}" size="50"> <input type="text" name="ignored_words" value="${config['ignored_words']}" size="50">
@@ -714,6 +717,7 @@
Split single file albums into multiple tracks Split single file albums into multiple tracks
<input type="checkbox" name="cue_split" id="cue_split" value="1" ${config['cue_split']} /> <input type="checkbox" name="cue_split" id="cue_split" value="1" ${config['cue_split']} />
</label> </label>
</div>
<div class="row checkbox left clearfix"> <div class="row checkbox left clearfix">
<label title="Freeze the database, so new artists won't be added automatically. Use this if Headphones adds artists because due to wrong snatches. This check is skipped when the folder name is appended with release group ID."> <label title="Freeze the database, so new artists won't be added automatically. Use this if Headphones adds artists because due to wrong snatches. This check is skipped when the folder name is appended with release group ID.">
Freeze database for adding new artist Freeze database for adding new artist
@@ -767,14 +771,18 @@
</label> </label>
</div> </div>
<div class="row"> <div class="row">
<label>Path to Destination Folder</label> <label>
Destination Directory:
</label>
<input type="text" name="destination_dir" value="${config['destination_dir']}" size="50"> <input type="text" name="destination_dir" value="${config['destination_dir']}" size="50">
<small>e.g. /Users/name/Music/iTunes or /Volumes/share/music</small> <small>The directory where Headphones will move file to after post processing, e.g. /Volumes/share/music.</small>
</div> </div>
<div class="row"> <div class="row">
<label>Path to Lossless Destination folder (optional)</label> <label>
Lossless Destination Directory:
</label>
<input type="text" name="lossless_destination_dir" value="${config['lossless_destination_dir']}" size="50"> <input type="text" name="lossless_destination_dir" value="${config['lossless_destination_dir']}" size="50">
<small>Set this if you have a separate directory for lossless music</small> <small>Optional. Set this if you have a separate directory for lossless music.</small>
</div> </div>
</fieldset> </fieldset>
</td> </td>
@@ -816,7 +824,7 @@
<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> </div>
@@ -991,11 +999,11 @@
<input type="checkbox" name="pushover_onsnatch" value="1" ${config['pushover_onsnatch']} /><label>Notify on snatch?</label> <input type="checkbox" name="pushover_onsnatch" value="1" ${config['pushover_onsnatch']} /><label>Notify on snatch?</label>
</div> </div>
<div class="row"> <div class="row">
<label>Priority (-1,0, or 1):</label> <label>Priority (-1,0, or 1)</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 class="row"> <div class="row">
<label>API Token (leave blank to use Headphones default):</label><input type="text" name="pushover_apitoken" value="${config['pushover_apitoken']}" size="50"> <label>API Token (leave blank to use Headphones default)</label><input type="text" name="pushover_apitoken" value="${config['pushover_apitoken']}" size="50">
</div> </div>
</div> </div>
</fieldset> </fieldset>
@@ -1163,7 +1171,7 @@
</div> </div>
<div class="row"> <div class="row">
<div class="row"> <div class="row">
<label>Multi-core count:</label> <label>Multi-core count</label>
<input type="text" name="encoder_multicore_count" value="${config['encoder_multicore_count']}" size="7"> <input type="text" name="encoder_multicore_count" value="${config['encoder_multicore_count']}" size="7">
<small>Set equal to the number of cores, or 0 for auto</small> <small>Set equal to the number of cores, or 0 for auto</small>
</div> </div>
@@ -1307,15 +1315,15 @@
<label>Automatically mark manually added albums as wanted</label> <label>Automatically mark manually added albums as wanted</label>
</div> </div>
<div class="row"> <div class="row">
<label>Folder Permissions:</label> <label>Folder Permissions</label>
<input type="text" name="folder_permissions" value="${config['folder_permissions']}" size="7"> <input type="text" name="folder_permissions" value="${config['folder_permissions']}" size="7">
</div> </div>
<div class="row"> <div class="row">
<label>File Permissions:</label> <label>File Permissions</label>
<input type="text" name="file_permissions" value="${config['file_permissions']}" size="7"> <input type="text" name="file_permissions" value="${config['file_permissions']}" size="7">
</div> </div>
<div class="row"> <div class="row">
<label>Cache Size (in MB):</label> <label>Cache Size (in MB)</label>
<input type="text" name="cache_sizemb" value="${config['cache_sizemb']}" size="7"> <input type="text" name="cache_sizemb" value="${config['cache_sizemb']}" size="7">
</div> </div>
</fieldset> </fieldset>
@@ -1336,11 +1344,11 @@
</select> </select>
</div> </div>
<div class="row"> <div class="row">
<label>Log Directory:</label> <label>Log Directory</label>
<input type="text" name="log_dir" value="${config['log_dir']}" size="50"> <input type="text" name="log_dir" value="${config['log_dir']}" size="50">
</div> </div>
<div class="row"> <div class="row">
<label>Cache Directory:</label> <label>Cache Directory</label>
<input type="text" name="cache_dir" value="${config['cache_dir']}" size="50"> <input type="text" name="cache_dir" value="${config['cache_dir']}" size="50">
</div> </div>
</fieldset> </fieldset>
@@ -1352,7 +1360,7 @@
</div> </div>
<div id="songkickoptions"> <div id="songkickoptions">
<div class="row"> <div class="row">
<label>API Key:</label> <label>API Key</label>
<input type="text" name="songkick_apikey" value="${config['songkick_apikey']}" size="50"> <input type="text" name="songkick_apikey" value="${config['songkick_apikey']}" size="50">
</div> </div>
<div class="row checkbox"> <div class="row checkbox">
@@ -1360,7 +1368,7 @@
</div> </div>
<div id="songkick_filteroptions"> <div id="songkick_filteroptions">
<div class="row"> <div class="row">
<label>Metro Area ID:</label> <label>Metro Area ID</label>
<input type="text" name="songkick_location" value="${config['songkick_location']}" size="10" title="Enter the Metro Area ID, e.g. the ID for London is 24426, this can be found by clicking the link and searching/selecting the city, e.g. London should find http://www.songkick.com/metro_areas/24426-uk-london"> <input type="text" name="songkick_location" value="${config['songkick_location']}" size="10" title="Enter the Metro Area ID, e.g. the ID for London is 24426, this can be found by clicking the link and searching/selecting the city, e.g. London should find http://www.songkick.com/metro_areas/24426-uk-london">
<a target="_blank" href="http://www.songkick.com/developer/location-search">Find Area ID</a> <a target="_blank" href="http://www.songkick.com/developer/location-search">Find Area ID</a>
</div> </div>
+69 -32
View File
@@ -64,6 +64,7 @@ CREATEPID = False
PIDFILE = None PIDFILE = None
SCHED = BackgroundScheduler() SCHED = BackgroundScheduler()
SCHED_LOCK = threading.Lock()
INIT_LOCK = threading.Lock() INIT_LOCK = threading.Lock()
_INITIALIZED = False _INITIALIZED = False
@@ -138,13 +139,12 @@ def initialize(config_file):
if not os.path.exists(CONFIG.CACHE_DIR): if not os.path.exists(CONFIG.CACHE_DIR):
try: try:
os.makedirs(CONFIG.CACHE_DIR) os.makedirs(CONFIG.CACHE_DIR)
except OSError: except OSError as e:
logger.error( logger.error("Could not create cache dir '%s': %s", DATA_DIR, e)
'Could not create cache dir. Check permissions of datadir: %s', DATA_DIR)
# Sanity check for search interval. Set it to at least 6 hours # Sanity check for search interval. Set it to at least 6 hours
if CONFIG.SEARCH_INTERVAL < 360: if CONFIG.SEARCH_INTERVAL and CONFIG.SEARCH_INTERVAL < 360:
logger.info("Search interval too low. Resetting to 6 hour minimum") logger.info("Search interval too low. Resetting to 6 hour minimum.")
CONFIG.SEARCH_INTERVAL = 360 CONFIG.SEARCH_INTERVAL = 360
# Initialize the database # Initialize the database
@@ -154,10 +154,23 @@ def initialize(config_file):
except Exception as e: except Exception as e:
logger.error("Can't connect to the database: %s", e) logger.error("Can't connect to the database: %s", e)
# Get the currently installed version - returns None, 'win32' or the git hash # Get the currently installed version. Returns None, 'win32' or the git
# Also sets INSTALL_TYPE variable to 'win', 'git' or 'source' # hash.
CURRENT_VERSION, CONFIG.GIT_BRANCH = versioncheck.getVersion() CURRENT_VERSION, CONFIG.GIT_BRANCH = versioncheck.getVersion()
# Write current version to a file, so we know which version did work.
# This allowes one to restore to that version. The idea is that if we
# arrive here, most parts of Headphones seem to work.
if CURRENT_VERSION:
version_lock_file = os.path.join(DATA_DIR, "version.lock")
try:
with open(version_lock_file, "w") as fp:
fp.write(CURRENT_VERSION)
except IOError as e:
logger.error("Unable to write current version to file '%s': %s",
version_lock_file, e)
# Check for new versions # Check for new versions
if CONFIG.CHECK_GITHUB_ON_STARTUP: if CONFIG.CHECK_GITHUB_ON_STARTUP:
try: try:
@@ -244,36 +257,60 @@ def launch_browser(host, port, root):
logger.error('Could not launch browser: %s', e) logger.error('Could not launch browser: %s', e)
def initialize_scheduler():
"""
Start the scheduled background tasks. Because this method can be called
multiple times, the old tasks will be first removed.
"""
from headphones import updater, searcher, librarysync, postprocessor, \
torrentfinished
with SCHED_LOCK:
# Remove all jobs
count = len(SCHED.get_jobs())
if count > 0:
logger.debug("Current number of background tasks: %d", count)
SCHED.shutdown()
SCHED.remove_all_jobs()
# Regular jobs
if CONFIG.UPDATE_DB_INTERVAL > 0:
SCHED.add_job(updater.dbUpdate, trigger=IntervalTrigger(
hours=CONFIG.UPDATE_DB_INTERVAL))
if CONFIG.SEARCH_INTERVAL > 0:
SCHED.add_job(searcher.searchforalbum, trigger=IntervalTrigger(
minutes=CONFIG.SEARCH_INTERVAL))
if CONFIG.LIBRARYSCAN_INTERVAL > 0:
SCHED.add_job(librarysync.libraryScan, trigger=IntervalTrigger(
hours=CONFIG.LIBRARYSCAN_INTERVAL))
if CONFIG.DOWNLOAD_SCAN_INTERVAL > 0:
SCHED.add_job(postprocessor.checkFolder, trigger=IntervalTrigger(
minutes=CONFIG.DOWNLOAD_SCAN_INTERVAL))
# Update check
if CONFIG.CHECK_GITHUB:
SCHED.add_job(versioncheck.checkGithub, trigger=IntervalTrigger(
minutes=CONFIG.CHECK_GITHUB_INTERVAL))
# Remove Torrent + data if Post Processed and finished Seeding
if CONFIG.TORRENT_REMOVAL_INTERVAL > 0:
SCHED.add_job(torrentfinished.checkTorrentFinished,
trigger=IntervalTrigger(
minutes=CONFIG.TORRENT_REMOVAL_INTERVAL))
# Start scheduler
logger.info("(Re-)Scheduling background tasks")
SCHED.start()
def start(): def start():
global started global started
if _INITIALIZED: if _INITIALIZED:
initialize_scheduler()
# Start our scheduled background tasks
from headphones import updater, searcher, librarysync, postprocessor, torrentfinished
SCHED.add_job(updater.dbUpdate, trigger=IntervalTrigger(
hours=CONFIG.UPDATE_DB_INTERVAL))
SCHED.add_job(searcher.searchforalbum, trigger=IntervalTrigger(
minutes=CONFIG.SEARCH_INTERVAL))
SCHED.add_job(librarysync.libraryScan, trigger=IntervalTrigger(
hours=CONFIG.LIBRARYSCAN_INTERVAL))
if CONFIG.CHECK_GITHUB:
SCHED.add_job(versioncheck.checkGithub, trigger=IntervalTrigger(
minutes=CONFIG.CHECK_GITHUB_INTERVAL))
if CONFIG.DOWNLOAD_SCAN_INTERVAL > 0:
SCHED.add_job(postprocessor.checkFolder, trigger=IntervalTrigger(
minutes=CONFIG.DOWNLOAD_SCAN_INTERVAL))
# Remove Torrent + data if Post Processed and finished Seeding
if CONFIG.TORRENT_REMOVAL_INTERVAL > 0:
SCHED.add_job(torrentfinished.checkTorrentFinished, trigger=IntervalTrigger(
minutes=CONFIG.TORRENT_REMOVAL_INTERVAL))
SCHED.start()
started = True started = True
+53 -19
View File
@@ -604,6 +604,43 @@ def smartMove(src, dest, delete=True):
except Exception as e: except Exception as e:
logger.warn('Error moving file %s: %s', filename.decode(headphones.SYS_ENCODING, 'replace'), e) logger.warn('Error moving file %s: %s', filename.decode(headphones.SYS_ENCODING, 'replace'), e)
def walk_directory(basedir, followlinks=True):
"""
Enhanced version of 'os.walk' where symlink directores are traversed, but
with care. In case a folder is already processed, don't traverse it again.
"""
import logger
# Add the base path, because symlinks poiting to the basedir should not be
# traversed again.
traversed = [os.path.abspath(basedir)]
def _inner(root, directories, files):
for directory in directories:
path = os.path.join(root, directory)
if followlinks and os.path.islink(path):
real_path = os.path.abspath(os.readlink(path))
if real_path in traversed:
logger.debug("Skipping '%s' since it is a symlink to "\
"'%s', which is already visited.", path, real_path)
else:
traversed.append(real_path)
for args in os.walk(real_path):
for result in _inner(*args):
yield result
# Pass on actual result
yield root, directories, files
# Start traversing
for args in os.walk(basedir):
for result in _inner(*args):
yield result
######################### #########################
#Sab renaming functions # #Sab renaming functions #
######################### #########################
@@ -660,44 +697,41 @@ def sab_sanitize_foldername(name):
return name return name
def split_string(mystring, splitvar=','): def split_string(mystring, splitvar=','):
mylist = [] mylist = []
for each_word in mystring.split(splitvar): for each_word in mystring.split(splitvar):
mylist.append(each_word.strip()) mylist.append(each_word.strip())
return mylist return mylist
def create_https_certificates(ssl_cert, ssl_key): def create_https_certificates(ssl_cert, ssl_key):
""" """
Stolen from SickBeard (http://github.com/midgetspy/Sick-Beard): Create a pair of self-signed HTTPS certificares and store in them in
Create self-signed HTTPS certificares and store in paths 'ssl_cert' and 'ssl_key' 'ssl_cert' and 'ssl_key'. Method assumes pyOpenSSL is installed.
This code is stolen from SickBeard (http://github.com/midgetspy/Sick-Beard).
""" """
from headphones import logger from headphones import logger
try: from OpenSSL import crypto
from OpenSSL import crypto from certgen import createKeyPair, createCertRequest, createCertificate, \
from certgen import createKeyPair, createCertRequest, createCertificate, TYPE_RSA, serial TYPE_RSA, serial
except:
logger.warn("pyOpenSSL module missing, please install to enable HTTPS")
return False
# Create the CA Certificate # Create the CA Certificate
cakey = createKeyPair(TYPE_RSA, 1024) cakey = createKeyPair(TYPE_RSA, 2048)
careq = createCertRequest(cakey, CN='Certificate Authority') careq = createCertRequest(cakey, CN="Certificate Authority")
cacert = createCertificate(careq, (careq, cakey), serial, (0, 60 * 60 * 24 * 365 * 10)) # ten years cacert = createCertificate(careq, (careq, cakey), serial, (0, 60 * 60 * 24 * 365 * 10)) # ten years
cname = 'Headphones' pkey = createKeyPair(TYPE_RSA, 2048)
pkey = createKeyPair(TYPE_RSA, 1024) req = createCertRequest(pkey, CN="Headphones")
req = createCertRequest(pkey, CN=cname)
cert = createCertificate(req, (cacert, cakey), serial, (0, 60 * 60 * 24 * 365 * 10)) # ten years cert = createCertificate(req, (cacert, cakey), serial, (0, 60 * 60 * 24 * 365 * 10)) # ten years
# Save the key and certificate to disk # Save the key and certificate to disk
try: try:
with open(ssl_key, 'w') as f: with open(ssl_key, "w") as fp:
f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)) fp.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
with open(ssl_cert, 'w') as f: with open(ssl_cert, "w") as fp:
f.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) fp.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
except IOError as e: except IOError as e:
logger.error("Error creating SSL key and certificate: %s", e) logger.error("Error creating SSL key and certificate: %s", e)
return False return False
+1 -1
View File
@@ -78,7 +78,7 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal
latest_subdirectory = [] latest_subdirectory = []
for r, d, f in os.walk(dir, followlinks=True): for r, d, f in helpers.walk_directory(dir):
# Need to abuse slicing to get a copy of the list, doing it directly # Need to abuse slicing to get a copy of the list, doing it directly
# will skip the element after a deleted one using a list comprehension # will skip the element after a deleted one using a list comprehension
# will not work correctly for nested subdirectories (os.walk keeps its # will not work correctly for nested subdirectories (os.walk keeps its
+1 -6
View File
@@ -17,6 +17,7 @@ from headphones import logger, helpers, common, request
from xml.dom import minidom from xml.dom import minidom
from httplib import HTTPSConnection from httplib import HTTPSConnection
from urlparse import parse_qsl
from urllib import urlencode from urllib import urlencode
from pynma import pynma from pynma import pynma
@@ -33,12 +34,6 @@ import json
import oauth2 as oauth import oauth2 as oauth
import pythontwitter as twitter import pythontwitter as twitter
try:
from urlparse import parse_qsl
except ImportError:
from cgi import parse_qsl
class GROWL(object): class GROWL(object):
""" """
Growl notifications, for OS X. Growl notifications, for OS X.
+26 -30
View File
@@ -18,6 +18,7 @@ from headphones import logger, request
import time import time
import json import json
import base64 import base64
import urlparse
import headphones import headphones
# This is just a simple script to send torrents to transmission. The # This is just a simple script to send torrents to transmission. The
@@ -126,7 +127,6 @@ def torrentAction(method, arguments):
host = headphones.CONFIG.TRANSMISSION_HOST host = headphones.CONFIG.TRANSMISSION_HOST
username = headphones.CONFIG.TRANSMISSION_USERNAME username = headphones.CONFIG.TRANSMISSION_USERNAME
password = headphones.CONFIG.TRANSMISSION_PASSWORD password = headphones.CONFIG.TRANSMISSION_PASSWORD
sessionid = None
if not host.startswith('http'): if not host.startswith('http'):
host = 'http://' + host host = 'http://' + host
@@ -134,30 +134,22 @@ def torrentAction(method, arguments):
if host.endswith('/'): if host.endswith('/'):
host = host[:-1] host = host[:-1]
# Either the host ends with a port, or some directory, or rpc # Fix the URL. We assume that the user does not point to the RPC endpoint,
# If it ends with /rpc we don't have to do anything # so add it if it is missing.
# If it ends with a port we add /transmission/rpc parts = list(urlparse.urlparse(host))
# anything else we just add rpc
if not host.endswith('/rpc'): if not parts[0] in ("http", "https"):
# Check if it ends in a port number parts[0] = "http"
i = host.rfind(':')
if i >= 0: if not parts[2].endswith("/rpc"):
possible_port = host[i + 1:] parts[2] += "/transmission/rpc"
host = host + "/rpc"
try: host = urlparse.urlunparse(parts)
port = int(possible_port)
if port:
host = host + "/transmission/rpc"
except ValueError:
logger.debug('No port, assuming not transmission')
else:
logger.error('Transmission port missing')
return
# Retrieve session id # Retrieve session id
auth = (username, password) if username and password else None auth = (username, password) if username and password else None
response = request.request_response(host, auth=auth,
response = request.request_response(host, auth=auth, whitelist_status_code=[401, 409]) whitelist_status_code=[401, 409])
if response is None: if response is None:
logger.error("Error gettings Transmission session ID") logger.error("Error gettings Transmission session ID")
@@ -166,26 +158,30 @@ def torrentAction(method, arguments):
# Parse response # Parse response
if response.status_code == 401: if response.status_code == 401:
if auth: if auth:
logger.error("Username and/or password not accepted by Transmission") logger.error("Username and/or password not accepted by " \
"Transmission")
else: else:
logger.error("Transmission authorization required") logger.error("Transmission authorization required")
return return
elif response.status_code == 409: elif response.status_code == 409:
sessionid = response.headers['x-transmission-session-id'] session_id = response.headers['x-transmission-session-id']
if not sessionid: if not session_id:
logger.error("Error getting Session ID from Transmission") logger.error("Expected a Session ID from Transmission")
return return
# Prepare next request # Prepare next request
headers = {'x-transmission-session-id': sessionid} headers = {'x-transmission-session-id': session_id}
data = {'method': method, 'arguments': arguments} data = {'method': method, 'arguments': arguments}
response = request.request_json(host, method="post", data=json.dumps(data), headers=headers, auth=auth) response = request.request_json(host, method="POST", data=json.dumps(data),
headers=headers, auth=auth)
print response
if not response: if not response:
logger.error("Error sending torrent to Transmission") logger.error("Error sending torrent to Transmission")
return return
return response return response
+5 -2
View File
@@ -1241,14 +1241,17 @@ class WebInterface(object):
headphones.CONFIG.add_extra_newznab(extra_newznab) headphones.CONFIG.add_extra_newznab(extra_newznab)
# Sanity checking # Sanity checking
if headphones.CONFIG.SEARCH_INTERVAL < 360: if headphones.CONFIG.SEARCH_INTERVAL and headphones.CONFIG.SEARCH_INTERVAL < 360:
logger.info("Search interval too low. Resetting to 6 hour minimum") logger.info("Search interval too low. Resetting to 6 hour minimum")
headphones.CONFIG.SEARCH_INTERVAL = 360 headphones.CONFIG.SEARCH_INTERVAL = 360
# Write the config # Write the config
headphones.CONFIG.write() headphones.CONFIG.write()
#reconfigure musicbrainz database connection with the new values # Reconfigure scheduler
headphones.initialize_scheduler()
# Reconfigure musicbrainz database connection with the new values
mb.startmb() mb.startmb()
raise cherrypy.HTTPRedirect("config") raise cherrypy.HTTPRedirect("config")
+7 -8
View File
@@ -23,9 +23,7 @@ from headphones.webserve import WebInterface
from headphones.helpers import create_https_certificates from headphones.helpers import create_https_certificates
def initialize(options=None): def initialize(options):
if options is None:
options = {}
# HTTPS stuff stolen from sickbeard # HTTPS stuff stolen from sickbeard
enable_https = options['enable_https'] enable_https = options['enable_https']
@@ -33,16 +31,17 @@ def initialize(options=None):
https_key = options['https_key'] https_key = options['https_key']
if enable_https: if enable_https:
# If either the HTTPS certificate or key do not exist, make some self-signed ones. # If either the HTTPS certificate or key do not exist, try to make
# self-signed ones.
if not (https_cert and os.path.exists(https_cert)) or not (https_key and os.path.exists(https_key)): if not (https_cert and os.path.exists(https_cert)) or not (https_key and os.path.exists(https_key)):
if not create_https_certificates(https_cert, https_key): if not create_https_certificates(https_cert, https_key):
logger.warn(u"Unable to create cert/key files, disabling HTTPS") logger.warn("Unable to create certificate and key. Disabling " \
headphones.CONFIG.ENABLE_HTTPS = False "HTTPS")
enable_https = False enable_https = False
if not (os.path.exists(https_cert) and os.path.exists(https_key)): if not (os.path.exists(https_cert) and os.path.exists(https_key)):
logger.warn(u"Disabled HTTPS because of missing CERT and KEY files") logger.warn("Disabled HTTPS because of missing certificate and " \
headphones.CONFIG.ENABLE_HTTPS = False "key.")
enable_https = False enable_https = False
options_dict = { options_dict = {
+2 -2
View File
@@ -1324,8 +1324,8 @@ class HTTPConnection(object):
def __init__(self, server, sock, makefile=CP_fileobject): def __init__(self, server, sock, makefile=CP_fileobject):
self.server = server self.server = server
self.socket = sock self.socket = sock
self.rfile = makefile(sock._sock, "rb", self.rbufsize) self.rfile = makefile(sock, "rb", self.rbufsize)
self.wfile = makefile(sock._sock, "wb", self.wbufsize) self.wfile = makefile(sock, "wb", self.wbufsize)
self.requests_seen = 0 self.requests_seen = 0
def communicate(self): def communicate(self):
+2892 -8
View File
File diff suppressed because it is too large Load Diff