From e224ad1a6a010a90ede84f24f9c2217d95c71988 Mon Sep 17 00:00:00 2001 From: Ade Date: Mon, 11 Aug 2014 20:32:37 +1200 Subject: [PATCH] Torrent seed ratio part 2 Set scheduled job to remove torrent when post processed and finished seeding --- data/interfaces/default/config.html | 12 ++++----- headphones/__init__.py | 6 +++-- headphones/api.py | 2 +- headphones/postprocessor.py | 23 ++++++++++++++--- headphones/searcher.py | 20 ++++++++++----- headphones/torrentfinished.py | 40 +++++++++++++++++++++++++++++ headphones/transmission.py | 26 +++++++++++++++++-- headphones/utorrent.py | 19 ++++++++++++++ headphones/webserve.py | 6 ++--- 9 files changed, 130 insertions(+), 24 deletions(-) create mode 100644 headphones/torrentfinished.py diff --git a/data/interfaces/default/config.html b/data/interfaces/default/config.html index 1b25722a..81a584da 100644 --- a/data/interfaces/default/config.html +++ b/data/interfaces/default/config.html @@ -395,7 +395,7 @@
- +
@@ -412,7 +412,7 @@
- +
@@ -433,7 +433,7 @@
- +
@@ -454,7 +454,7 @@
- +
@@ -475,7 +475,7 @@
- +
@@ -488,7 +488,7 @@
- +
diff --git a/headphones/__init__.py b/headphones/__init__.py index b73df6a6..0560a9fa 100644 --- a/headphones/__init__.py +++ b/headphones/__init__.py @@ -514,7 +514,6 @@ def initialize(): WHATCD_USERNAME = check_setting_str(CFG, 'What.cd', 'whatcd_username', '') WHATCD_PASSWORD = check_setting_str(CFG, 'What.cd', 'whatcd_password', '') WHATCD_RATIO = check_setting_str(CFG, 'What.cd', 'whatcd_ratio', '') - WHATCD_PASSWORD = check_setting_str(CFG, 'What.cd', 'whatcd_password', '') SAB_HOST = check_setting_str(CFG, 'SABnzbd', 'sab_host', '') SAB_USERNAME = check_setting_str(CFG, 'SABnzbd', 'sab_username', '') @@ -1135,7 +1134,7 @@ def start(): if __INITIALIZED__: # Start our scheduled background tasks - from headphones import updater, searcher, librarysync, postprocessor + from headphones import updater, searcher, librarysync, postprocessor, torrentfinished SCHED.add_interval_job(updater.dbUpdate, hours=UPDATE_DB_INTERVAL) SCHED.add_interval_job(searcher.searchforalbum, minutes=SEARCH_INTERVAL) @@ -1147,6 +1146,9 @@ def start(): if DOWNLOAD_SCAN_INTERVAL > 0: SCHED.add_interval_job(postprocessor.checkFolder, minutes=DOWNLOAD_SCAN_INTERVAL) + # Remove Torrent + data if Post Processed and finished Seeding + SCHED.add_interval_job(torrentfinished.checkTorrentFinished, hours=12) + SCHED.start() started = True diff --git a/headphones/api.py b/headphones/api.py index 87f20ae0..1fcce419 100644 --- a/headphones/api.py +++ b/headphones/api.py @@ -145,7 +145,7 @@ class Api(object): return def _getHistory(self, **kwargs): - self.data = self._dic_from_query('SELECT * from snatched order by DateAdded DESC') + self.data = self._dic_from_query('SELECT * from snatched WHERE status NOT LIKE "Seed%" order by DateAdded DESC') return def _getUpcoming(self, **kwargs): diff --git a/headphones/postprocessor.py b/headphones/postprocessor.py index a89eee68..8e378551 100644 --- a/headphones/postprocessor.py +++ b/headphones/postprocessor.py @@ -24,7 +24,7 @@ import headphones from beets import autotag from beets.mediafile import MediaFile, FileTypeError, UnreadableFileError -from headphones import notifiers +from headphones import notifiers, utorrent, transmission from headphones import db, albumart, librarysync, lyrics from headphones import logger, helpers, request, mb, music_encoder @@ -305,7 +305,7 @@ def verify(albumid, albumpath, Kind=None, forced=False): return logger.warn(u'Could not identify album: %s. It may not be the intended album.' % albumpath.decode(headphones.SYS_ENCODING, 'replace')) - myDB.action('UPDATE snatched SET status = "Unprocessed" WHERE AlbumID=?', [albumid]) + myDB.action('UPDATE snatched SET status = "Unprocessed" WHERE status NOT LIKE "Seed%" and AlbumID=?', [albumid]) processed = re.search(r' \(Unprocessed\)(?:\[\d+\])?', albumpath) if not processed: renameUnprocessedFolder(albumpath) @@ -416,7 +416,24 @@ def doPostProcessing(albumid, albumpath, release, tracks, downloaded_track_list, myDB = db.DBConnection() myDB.action('UPDATE albums SET status = "Downloaded" WHERE AlbumID=?', [albumid]) - myDB.action('UPDATE snatched SET status = "Processed" WHERE AlbumID=?', [albumid]) + myDB.action('UPDATE snatched SET status = "Processed" WHERE Status NOT LIKE "Seed%" and AlbumID=?', [albumid]) + + # Check if torrent has finished seeding + if headphones.TORRENT_DOWNLOADER == (1 or 2): + seed_snatched = myDB.action('SELECT * from snatched WHERE Status="Seed_Snatched" and AlbumID=?', [albumid]).fetchone() + if seed_snatched: + hash = seed_snatched['FolderName'] + torrent_removed = False + if headphones.TORRENT_DOWNLOADER == 1: + torrent_removed = transmission.removeTorrent(hash, True) + else: + torrent_removed = utorrent.removeTorrent(hash, True) + + # Torrent removed, delete the snatched record, else update Status for scheduled job to check + if torrent_removed: + myDB.action('DELETE from snatched WHERE status = "Seed_Snatched" and AlbumID=?', [albumid]) + else: + myDB.action('UPDATE snatched SET status = "Seed_Processed" WHERE status = "Seed_Snatched" and AlbumID=?', [albumid]) # Update the have tracks for all created dirs: for albumpath in albumpaths: diff --git a/headphones/searcher.py b/headphones/searcher.py index 06293f08..3efc3cad 100644 --- a/headphones/searcher.py +++ b/headphones/searcher.py @@ -569,6 +569,8 @@ def send_to_downloader(data, bestqual, album): logger.info(u'Found best result from %s: %s - %s', bestqual[3], bestqual[2], bestqual[0], helpers.bytes_to_mb(bestqual[1])) # Get rid of any dodgy chars here so we can prevent sab from renaming our downloads kind = bestqual[4] + seed_ratio = None + torrentid = None if kind == 'nzb': folder_name = helpers.sab_sanitize_foldername(bestqual[0]) @@ -674,7 +676,7 @@ def send_to_downloader(data, bestqual, album): # rutracker needs cookies to be set, pass the .torrent file instead of url if bestqual[3] == 'rutracker.org': - file_or_url, _hash = rutracker.get_torrent(bestqual[2]) + file_or_url, torrentid = rutracker.get_torrent(bestqual[2]) else: file_or_url = bestqual[2] @@ -708,14 +710,14 @@ def send_to_downloader(data, bestqual, album): # rutracker needs cookies to be set, pass the .torrent file instead of url if bestqual[3] == 'rutracker.org': - file_or_url, _hash = rutracker.get_torrent(bestqual[2]) - folder_name, cacheid = utorrent.dirTorrent(_hash) + file_or_url, torrentid = rutracker.get_torrent(bestqual[2]) + folder_name, cacheid = utorrent.dirTorrent(torrentid) folder_name = os.path.basename(os.path.normpath(folder_name)) - utorrent.labelTorrent(_hash) + utorrent.labelTorrent(torrentid) else: file_or_url = bestqual[2] - _hash = CalculateTorrentHash(file_or_url, data) - folder_name = utorrent.addTorrent(file_or_url, _hash) + torrentid = CalculateTorrentHash(file_or_url, data) + folder_name = utorrent.addTorrent(file_or_url, torrentid) if folder_name: logger.info('Torrent folder name: %s' % folder_name) @@ -733,12 +735,16 @@ def send_to_downloader(data, bestqual, album): # Set Seed Ratio seed_ratio = getSeedRatio(bestqual[3]) if seed_ratio != None: - utorrent.setSeedRatio(_hash, seed_ratio) + utorrent.setSeedRatio(torrentid, seed_ratio) myDB = db.DBConnection() myDB.action('UPDATE albums SET status = "Snatched" WHERE AlbumID=?', [album['AlbumID']]) myDB.action('INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?, ?)', [album['AlbumID'], bestqual[0], bestqual[1], bestqual[2], "Snatched", folder_name, kind]) + # Store the torrent id so we can check later if it's finished seeding and can be removed + if seed_ratio != None and seed_ratio != 0 and torrentid: + myDB.action('INSERT INTO snatched VALUES( ?, ?, ?, ?, DATETIME("NOW", "localtime"), ?, ?, ?)', [album['AlbumID'], bestqual[0], bestqual[1], bestqual[2], "Seed_Snatched", torrentid, kind]) + # notify artist = album[1] albumname = album[2] diff --git a/headphones/torrentfinished.py b/headphones/torrentfinished.py new file mode 100644 index 00000000..a86614e2 --- /dev/null +++ b/headphones/torrentfinished.py @@ -0,0 +1,40 @@ +# This file is part of Headphones. +# +# Headphones is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Headphones is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Headphones. If not, see . + +import threading +import headphones +from headphones import db, utorrent, transmission, logger + +postprocessor_lock = threading.Lock() + +# Remove Torrent + data if Post Processed and finished Seeding +def checkTorrentFinished(): + + with postprocessor_lock: + + myDB = db.DBConnection() + results = myDB.select('SELECT * from snatched WHERE Status="Seed_Processed"') + + for album in results: + hash = album['FolderName'] + albumid = album['AlbumID'] + torrent_removed = False + if headphones.TORRENT_DOWNLOADER == 1: + torrent_removed = transmission.removeTorrent(hash, True) + else: + torrent_removed = utorrent.removeTorrent(hash, True) + + if torrent_removed: + myDB.action('DELETE from snatched WHERE status = "Seed_Processed" and AlbumID=?', [albumid]) diff --git a/headphones/transmission.py b/headphones/transmission.py index 13a24de4..427115ea 100644 --- a/headphones/transmission.py +++ b/headphones/transmission.py @@ -47,10 +47,10 @@ def addTorrent(link): if response['result'] == 'success': if 'torrent-added' in response['arguments']: name = response['arguments']['torrent-added']['name'] - retid = response['arguments']['torrent-added']['id'] + retid = response['arguments']['torrent-added']['hashString'] elif 'torrent-duplicate' in response['arguments']: name = response['arguments']['torrent-duplicate']['name'] - retid = response['arguments']['torrent-duplicate']['id'] + retid = response['arguments']['torrent-duplicate']['hashString'] else: name = link retid = False @@ -93,6 +93,28 @@ def setSeedRatio(torrentid, ratio): if not response: return False +def removeTorrent(torrentid, remove_data = False): + + method = 'torrent-get' + arguments = { 'ids': torrentid, 'fields': ['isFinished', 'name']} + + response = torrentAction(method, arguments) + + finished = response['arguments']['torrents'][0]['isFinished'] + name = response['arguments']['torrents'][0]['name'] + + if finished: + logger.info('%s has finished seeding, removing torrent and data' % name) + method = 'torrent-remove' + if remove_data: + arguments = {'delete-local-data': True, 'ids': torrentid} + else: + arguments = {'ids': torrentid} + response = torrentAction(method, arguments) + return True + + return False + def torrentAction(method, arguments): host = headphones.TRANSMISSION_HOST diff --git a/headphones/utorrent.py b/headphones/utorrent.py index b98456c0..1c1e9bb1 100644 --- a/headphones/utorrent.py +++ b/headphones/utorrent.py @@ -132,6 +132,13 @@ class utorrentclient(object): return settings[key] return settings + def remove(self, hash, remove_data = False): + if remove_data: + params = [('action', 'removedata'), ('hash', hash)] + else: + params = [('action', 'remove'), ('hash', hash)] + return self._action(params) + def _action(self, params, body=None, content_type=None): url = self.base_url + '/gui/' + '?token=' + self.token + '&' + urllib.urlencode(params) request = urllib2.Request(url) @@ -155,6 +162,17 @@ def labelTorrent(hash): if label: uTorrentClient.setprops(hash,'label',label) +def removeTorrent(hash): + uTorrentClient = utorrentclient() + status, torrentList = uTorrentClient.list() + torrents = torrentList['torrents'] + for torrent in torrents: + if torrent[0].lower() == hash and torrent[21] == 'Finished': + logger.info('%s has finished seeding, removing torrent and data' % torrent[2]) + uTorrentClient.remove(hash, True) + return True + return False + def setSeedRatio(hash, ratio): uTorrentClient = utorrentclient() uTorrentClient.setprops(hash, 'seed_override', '1') @@ -215,6 +233,7 @@ def addTorrent(link, hash): if torrent_folder == active_dir or not torrent_folder: torrent_folder, cacheid = dirTorrent(hash, cacheid, return_name=True) + labelTorrent(hash) return torrent_folder else: labelTorrent(hash) diff --git a/headphones/webserve.py b/headphones/webserve.py index fa1a9c54..401e5921 100644 --- a/headphones/webserve.py +++ b/headphones/webserve.py @@ -735,7 +735,7 @@ class WebInterface(object): def history(self): myDB = db.DBConnection() - history = myDB.select('''SELECT * from snatched order by DateAdded DESC''') + history = myDB.select('''SELECT * from snatched WHERE Status NOT LIKE "Seed%" order by DateAdded DESC''') return serve_template(templatename="history.html", title="History", history=history) history.exposed = True @@ -901,13 +901,13 @@ class WebInterface(object): if type: if type == 'all': logger.info(u"Clearing all history") - myDB.action('DELETE from snatched') + myDB.action('DELETE from snatched WHERE Status NOT LIKE "Seed%"') else: logger.info(u"Clearing history where status is %s" % type) myDB.action('DELETE from snatched WHERE Status=?', [type]) else: logger.info(u"Deleting '%s' from history" % title) - myDB.action('DELETE from snatched WHERE Title=? AND DateAdded=?', [title, date_added]) + myDB.action('DELETE from snatched WHERE Status NOT LIKE "Seed%" AND Title=? AND DateAdded=?', [title, date_added]) raise cherrypy.HTTPRedirect("history") clearhistory.exposed = True