mirror of
https://github.com/rembo10/headphones.git
synced 2026-03-24 05:39:27 +00:00
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:
7
.gitignore
vendored
7
.gitignore
vendored
@@ -6,12 +6,13 @@
|
||||
*.pyproj
|
||||
*.sln
|
||||
|
||||
# Logs and databases #
|
||||
# Headphones files #
|
||||
######################
|
||||
*.log
|
||||
*.db*
|
||||
*.db-journal
|
||||
*.ini
|
||||
version.lock
|
||||
logs/*
|
||||
cache/*
|
||||
|
||||
@@ -19,6 +20,7 @@ cache/*
|
||||
##################
|
||||
*.crt
|
||||
*.key
|
||||
*.csr
|
||||
|
||||
# OS generated files #
|
||||
######################
|
||||
@@ -64,6 +66,3 @@ _ReSharper*/
|
||||
/logs
|
||||
.project
|
||||
.pydevproject
|
||||
|
||||
|
||||
headphones_docs
|
||||
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,5 +1,18 @@
|
||||
# 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
|
||||
Released 10 November 2014
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
## 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
|
||||
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`
|
||||
* `self.__really_private_field`
|
||||
* `_global`
|
||||
|
||||
|
||||
Document your code!
|
||||
@@ -164,6 +164,16 @@ def main():
|
||||
else:
|
||||
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.
|
||||
web_config = {
|
||||
'http_port': http_port,
|
||||
|
||||
9
contrib/clean_pyc.sh
Executable file
9
contrib/clean_pyc.sh
Executable 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
contrib/downgrade.sh
Executable file
32
contrib/downgrade.sh
Executable 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."
|
||||
@@ -101,6 +101,7 @@
|
||||
|
||||
<fieldset>
|
||||
<legend>Interval</legend>
|
||||
<small>An interval of 0 will disable a task.</small>
|
||||
<div class="row">
|
||||
<label title="Time between two searches for new downloads.">
|
||||
Search Interval
|
||||
@@ -137,7 +138,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -266,7 +267,7 @@
|
||||
prio_force = ''
|
||||
%>
|
||||
<div class="row">
|
||||
<label>NZBget Priority:</label>
|
||||
<label>NZBget Priority</label>
|
||||
<select name="nzbget_priority" id="nzbget_priority">
|
||||
<option value="-100" ${prio_verylow}>Very Low</option>
|
||||
<option value="-50" ${prio_low}>Low</option>
|
||||
@@ -294,7 +295,7 @@
|
||||
Music Download Directory:
|
||||
</label>
|
||||
<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 class="row">
|
||||
<label title="Number of days of retention your usenet provider provides.">
|
||||
@@ -381,14 +382,14 @@
|
||||
</fieldset>
|
||||
<fieldset id="general_torrent_options">
|
||||
<div class="row">
|
||||
<label>Minimum seeders:</label>
|
||||
<label>Minimum seeders</label>
|
||||
<input type="text" name="numberofseeders" value="${config['numberofseeders']}" size="5">
|
||||
<small>Number of minimum seeders a torrent must have to be accepted</small>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Music Download Directory</label>
|
||||
<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 class="row checkbox">
|
||||
<label>Keep Files for Seeding</label>
|
||||
@@ -526,11 +527,12 @@
|
||||
</div>
|
||||
<div class="config">
|
||||
<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">
|
||||
<small>Optional. Leave empty for default.</small>
|
||||
</div>
|
||||
<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">
|
||||
</div>
|
||||
</div>
|
||||
@@ -543,11 +545,12 @@
|
||||
</div>
|
||||
<div class="config">
|
||||
<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">
|
||||
<small>Optional. Leave empty for default.</small>
|
||||
</div>
|
||||
<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">
|
||||
</div>
|
||||
</div>
|
||||
@@ -560,15 +563,15 @@
|
||||
</div>
|
||||
<div class="config">
|
||||
<div class="row">
|
||||
<label>UID Number: </label>
|
||||
<label>UID Number</label>
|
||||
<input type="text" name="waffles_uid" value="${config['waffles_uid']}" size="36">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Passkey: </label>
|
||||
<label>Passkey</label>
|
||||
<input type="password" name="waffles_passkey" value="${config['waffles_passkey']}" size="36">
|
||||
</div>
|
||||
<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">
|
||||
</div>
|
||||
</div>
|
||||
@@ -581,15 +584,15 @@
|
||||
</div>
|
||||
<div class="config">
|
||||
<div class="row">
|
||||
<label>User Name: </label>
|
||||
<label>Userame</label>
|
||||
<input type="text" name="rutracker_user" value="${config['rutracker_user']}" size="36">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Password: </label>
|
||||
<label>Password</label>
|
||||
<input type="password" name="rutracker_password" value="${config['rutracker_password']}" size="36">
|
||||
</div>
|
||||
<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">
|
||||
</div>
|
||||
</div>
|
||||
@@ -688,7 +691,7 @@
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<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">
|
||||
<label>Ignored Words</label>
|
||||
<input type="text" name="ignored_words" value="${config['ignored_words']}" size="50">
|
||||
@@ -714,6 +717,7 @@
|
||||
Split single file albums into multiple tracks
|
||||
<input type="checkbox" name="cue_split" id="cue_split" value="1" ${config['cue_split']} />
|
||||
</label>
|
||||
</div>
|
||||
<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.">
|
||||
Freeze database for adding new artist
|
||||
@@ -767,14 +771,18 @@
|
||||
</label>
|
||||
</div>
|
||||
<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">
|
||||
<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 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">
|
||||
<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>
|
||||
</fieldset>
|
||||
</td>
|
||||
@@ -816,7 +824,7 @@
|
||||
<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>
|
||||
<label>Priority (-2,-1,0,1 or 2)</label>
|
||||
<input type="text" name="prowl_priority" value="${config['prowl_priority']}" size="2">
|
||||
</div>
|
||||
</div>
|
||||
@@ -991,11 +999,11 @@
|
||||
<input type="checkbox" name="pushover_onsnatch" value="1" ${config['pushover_onsnatch']} /><label>Notify on snatch?</label>
|
||||
</div>
|
||||
<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">
|
||||
</div>
|
||||
<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>
|
||||
</fieldset>
|
||||
@@ -1163,7 +1171,7 @@
|
||||
</div>
|
||||
<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">
|
||||
<small>Set equal to the number of cores, or 0 for auto</small>
|
||||
</div>
|
||||
@@ -1307,15 +1315,15 @@
|
||||
<label>Automatically mark manually added albums as wanted</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Folder Permissions:</label>
|
||||
<label>Folder Permissions</label>
|
||||
<input type="text" name="folder_permissions" value="${config['folder_permissions']}" size="7">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>File Permissions:</label>
|
||||
<label>File Permissions</label>
|
||||
<input type="text" name="file_permissions" value="${config['file_permissions']}" size="7">
|
||||
</div>
|
||||
<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">
|
||||
</div>
|
||||
</fieldset>
|
||||
@@ -1336,11 +1344,11 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Log Directory:</label>
|
||||
<label>Log Directory</label>
|
||||
<input type="text" name="log_dir" value="${config['log_dir']}" size="50">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Cache Directory:</label>
|
||||
<label>Cache Directory</label>
|
||||
<input type="text" name="cache_dir" value="${config['cache_dir']}" size="50">
|
||||
</div>
|
||||
</fieldset>
|
||||
@@ -1352,7 +1360,7 @@
|
||||
</div>
|
||||
<div id="songkickoptions">
|
||||
<div class="row">
|
||||
<label>API Key:</label>
|
||||
<label>API Key</label>
|
||||
<input type="text" name="songkick_apikey" value="${config['songkick_apikey']}" size="50">
|
||||
</div>
|
||||
<div class="row checkbox">
|
||||
@@ -1360,7 +1368,7 @@
|
||||
</div>
|
||||
<div id="songkick_filteroptions">
|
||||
<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">
|
||||
<a target="_blank" href="http://www.songkick.com/developer/location-search">Find Area ID</a>
|
||||
</div>
|
||||
|
||||
@@ -64,6 +64,7 @@ CREATEPID = False
|
||||
PIDFILE = None
|
||||
|
||||
SCHED = BackgroundScheduler()
|
||||
SCHED_LOCK = threading.Lock()
|
||||
|
||||
INIT_LOCK = threading.Lock()
|
||||
_INITIALIZED = False
|
||||
@@ -138,13 +139,12 @@ def initialize(config_file):
|
||||
if not os.path.exists(CONFIG.CACHE_DIR):
|
||||
try:
|
||||
os.makedirs(CONFIG.CACHE_DIR)
|
||||
except OSError:
|
||||
logger.error(
|
||||
'Could not create cache dir. Check permissions of datadir: %s', DATA_DIR)
|
||||
except OSError as e:
|
||||
logger.error("Could not create cache dir '%s': %s", DATA_DIR, e)
|
||||
|
||||
# Sanity check for search interval. Set it to at least 6 hours
|
||||
if CONFIG.SEARCH_INTERVAL < 360:
|
||||
logger.info("Search interval too low. Resetting to 6 hour minimum")
|
||||
if CONFIG.SEARCH_INTERVAL and CONFIG.SEARCH_INTERVAL < 360:
|
||||
logger.info("Search interval too low. Resetting to 6 hour minimum.")
|
||||
CONFIG.SEARCH_INTERVAL = 360
|
||||
|
||||
# Initialize the database
|
||||
@@ -154,10 +154,23 @@ def initialize(config_file):
|
||||
except Exception as e:
|
||||
logger.error("Can't connect to the database: %s", e)
|
||||
|
||||
# Get the currently installed version - returns None, 'win32' or the git hash
|
||||
# Also sets INSTALL_TYPE variable to 'win', 'git' or 'source'
|
||||
# Get the currently installed version. Returns None, 'win32' or the git
|
||||
# hash.
|
||||
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
|
||||
if CONFIG.CHECK_GITHUB_ON_STARTUP:
|
||||
try:
|
||||
@@ -244,36 +257,60 @@ def launch_browser(host, port, root):
|
||||
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():
|
||||
|
||||
global started
|
||||
|
||||
if _INITIALIZED:
|
||||
|
||||
# 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()
|
||||
|
||||
initialize_scheduler()
|
||||
started = True
|
||||
|
||||
|
||||
|
||||
@@ -604,6 +604,43 @@ def smartMove(src, dest, delete=True):
|
||||
except Exception as 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 #
|
||||
#########################
|
||||
@@ -660,44 +697,41 @@ def sab_sanitize_foldername(name):
|
||||
|
||||
return name
|
||||
|
||||
|
||||
def split_string(mystring, splitvar=','):
|
||||
mylist = []
|
||||
for each_word in mystring.split(splitvar):
|
||||
mylist.append(each_word.strip())
|
||||
return mylist
|
||||
|
||||
|
||||
def create_https_certificates(ssl_cert, ssl_key):
|
||||
"""
|
||||
Stolen from SickBeard (http://github.com/midgetspy/Sick-Beard):
|
||||
Create self-signed HTTPS certificares and store in paths 'ssl_cert' and 'ssl_key'
|
||||
Create a pair of self-signed HTTPS certificares and store in them in
|
||||
'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
|
||||
|
||||
try:
|
||||
from OpenSSL import crypto
|
||||
from certgen import createKeyPair, createCertRequest, createCertificate, TYPE_RSA, serial
|
||||
except:
|
||||
logger.warn("pyOpenSSL module missing, please install to enable HTTPS")
|
||||
return False
|
||||
from OpenSSL import crypto
|
||||
from certgen import createKeyPair, createCertRequest, createCertificate, \
|
||||
TYPE_RSA, serial
|
||||
|
||||
# Create the CA Certificate
|
||||
cakey = createKeyPair(TYPE_RSA, 1024)
|
||||
careq = createCertRequest(cakey, CN='Certificate Authority')
|
||||
cakey = createKeyPair(TYPE_RSA, 2048)
|
||||
careq = createCertRequest(cakey, CN="Certificate Authority")
|
||||
cacert = createCertificate(careq, (careq, cakey), serial, (0, 60 * 60 * 24 * 365 * 10)) # ten years
|
||||
|
||||
cname = 'Headphones'
|
||||
pkey = createKeyPair(TYPE_RSA, 1024)
|
||||
req = createCertRequest(pkey, CN=cname)
|
||||
pkey = createKeyPair(TYPE_RSA, 2048)
|
||||
req = createCertRequest(pkey, CN="Headphones")
|
||||
cert = createCertificate(req, (cacert, cakey), serial, (0, 60 * 60 * 24 * 365 * 10)) # ten years
|
||||
|
||||
# Save the key and certificate to disk
|
||||
try:
|
||||
with open(ssl_key, 'w') as f:
|
||||
f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
|
||||
with open(ssl_cert, 'w') as f:
|
||||
f.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
|
||||
with open(ssl_key, "w") as fp:
|
||||
fp.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
|
||||
with open(ssl_cert, "w") as fp:
|
||||
fp.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
|
||||
except IOError as e:
|
||||
logger.error("Error creating SSL key and certificate: %s", e)
|
||||
return False
|
||||
|
||||
@@ -78,7 +78,7 @@ def libraryScan(dir=None, append=False, ArtistID=None, ArtistName=None, cron=Fal
|
||||
|
||||
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
|
||||
# will skip the element after a deleted one using a list comprehension
|
||||
# will not work correctly for nested subdirectories (os.walk keeps its
|
||||
|
||||
@@ -17,6 +17,7 @@ from headphones import logger, helpers, common, request
|
||||
|
||||
from xml.dom import minidom
|
||||
from httplib import HTTPSConnection
|
||||
from urlparse import parse_qsl
|
||||
from urllib import urlencode
|
||||
from pynma import pynma
|
||||
|
||||
@@ -33,12 +34,6 @@ import json
|
||||
import oauth2 as oauth
|
||||
import pythontwitter as twitter
|
||||
|
||||
try:
|
||||
from urlparse import parse_qsl
|
||||
except ImportError:
|
||||
from cgi import parse_qsl
|
||||
|
||||
|
||||
class GROWL(object):
|
||||
"""
|
||||
Growl notifications, for OS X.
|
||||
|
||||
@@ -18,6 +18,7 @@ from headphones import logger, request
|
||||
import time
|
||||
import json
|
||||
import base64
|
||||
import urlparse
|
||||
import headphones
|
||||
|
||||
# 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
|
||||
username = headphones.CONFIG.TRANSMISSION_USERNAME
|
||||
password = headphones.CONFIG.TRANSMISSION_PASSWORD
|
||||
sessionid = None
|
||||
|
||||
if not host.startswith('http'):
|
||||
host = 'http://' + host
|
||||
@@ -134,30 +134,22 @@ def torrentAction(method, arguments):
|
||||
if host.endswith('/'):
|
||||
host = host[:-1]
|
||||
|
||||
# Either the host ends with a port, or some directory, or rpc
|
||||
# If it ends with /rpc we don't have to do anything
|
||||
# If it ends with a port we add /transmission/rpc
|
||||
# anything else we just add rpc
|
||||
if not host.endswith('/rpc'):
|
||||
# Check if it ends in a port number
|
||||
i = host.rfind(':')
|
||||
if i >= 0:
|
||||
possible_port = host[i + 1:]
|
||||
host = host + "/rpc"
|
||||
try:
|
||||
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
|
||||
# Fix the URL. We assume that the user does not point to the RPC endpoint,
|
||||
# so add it if it is missing.
|
||||
parts = list(urlparse.urlparse(host))
|
||||
|
||||
if not parts[0] in ("http", "https"):
|
||||
parts[0] = "http"
|
||||
|
||||
if not parts[2].endswith("/rpc"):
|
||||
parts[2] += "/transmission/rpc"
|
||||
|
||||
host = urlparse.urlunparse(parts)
|
||||
|
||||
# Retrieve session id
|
||||
auth = (username, password) if username and password else None
|
||||
|
||||
response = request.request_response(host, auth=auth, whitelist_status_code=[401, 409])
|
||||
response = request.request_response(host, auth=auth,
|
||||
whitelist_status_code=[401, 409])
|
||||
|
||||
if response is None:
|
||||
logger.error("Error gettings Transmission session ID")
|
||||
@@ -166,26 +158,30 @@ def torrentAction(method, arguments):
|
||||
# Parse response
|
||||
if response.status_code == 401:
|
||||
if auth:
|
||||
logger.error("Username and/or password not accepted by Transmission")
|
||||
logger.error("Username and/or password not accepted by " \
|
||||
"Transmission")
|
||||
else:
|
||||
logger.error("Transmission authorization required")
|
||||
|
||||
return
|
||||
elif response.status_code == 409:
|
||||
sessionid = response.headers['x-transmission-session-id']
|
||||
session_id = response.headers['x-transmission-session-id']
|
||||
|
||||
if not sessionid:
|
||||
logger.error("Error getting Session ID from Transmission")
|
||||
return
|
||||
if not session_id:
|
||||
logger.error("Expected a Session ID from Transmission")
|
||||
return
|
||||
|
||||
# Prepare next request
|
||||
headers = {'x-transmission-session-id': sessionid}
|
||||
headers = {'x-transmission-session-id': session_id}
|
||||
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:
|
||||
logger.error("Error sending torrent to Transmission")
|
||||
return
|
||||
|
||||
return response
|
||||
return response
|
||||
@@ -1241,14 +1241,17 @@ class WebInterface(object):
|
||||
headphones.CONFIG.add_extra_newznab(extra_newznab)
|
||||
|
||||
# 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")
|
||||
headphones.CONFIG.SEARCH_INTERVAL = 360
|
||||
|
||||
# Write the config
|
||||
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()
|
||||
|
||||
raise cherrypy.HTTPRedirect("config")
|
||||
|
||||
@@ -23,9 +23,7 @@ from headphones.webserve import WebInterface
|
||||
from headphones.helpers import create_https_certificates
|
||||
|
||||
|
||||
def initialize(options=None):
|
||||
if options is None:
|
||||
options = {}
|
||||
def initialize(options):
|
||||
|
||||
# HTTPS stuff stolen from sickbeard
|
||||
enable_https = options['enable_https']
|
||||
@@ -33,16 +31,17 @@ def initialize(options=None):
|
||||
https_key = options['https_key']
|
||||
|
||||
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 create_https_certificates(https_cert, https_key):
|
||||
logger.warn(u"Unable to create cert/key files, disabling HTTPS")
|
||||
headphones.CONFIG.ENABLE_HTTPS = False
|
||||
logger.warn("Unable to create certificate and key. Disabling " \
|
||||
"HTTPS")
|
||||
enable_https = False
|
||||
|
||||
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")
|
||||
headphones.CONFIG.ENABLE_HTTPS = False
|
||||
logger.warn("Disabled HTTPS because of missing certificate and " \
|
||||
"key.")
|
||||
enable_https = False
|
||||
|
||||
options_dict = {
|
||||
|
||||
@@ -1324,8 +1324,8 @@ class HTTPConnection(object):
|
||||
def __init__(self, server, sock, makefile=CP_fileobject):
|
||||
self.server = server
|
||||
self.socket = sock
|
||||
self.rfile = makefile(sock._sock, "rb", self.rbufsize)
|
||||
self.wfile = makefile(sock._sock, "wb", self.wbufsize)
|
||||
self.rfile = makefile(sock, "rb", self.rbufsize)
|
||||
self.wfile = makefile(sock, "wb", self.wbufsize)
|
||||
self.requests_seen = 0
|
||||
|
||||
def communicate(self):
|
||||
|
||||
2900
lib/pkg_resources.py
2900
lib/pkg_resources.py
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user