diff options
Diffstat (limited to 'deluge/plugins/Extractor')
-rw-r--r-- | deluge/plugins/Extractor/deluge_extractor/__init__.py | 37 | ||||
-rw-r--r-- | deluge/plugins/Extractor/deluge_extractor/common.py | 20 | ||||
-rw-r--r-- | deluge/plugins/Extractor/deluge_extractor/core.py | 186 | ||||
-rw-r--r-- | deluge/plugins/Extractor/deluge_extractor/data/extractor.js | 100 | ||||
-rw-r--r-- | deluge/plugins/Extractor/deluge_extractor/data/extractor_prefs.ui | 121 | ||||
-rw-r--r-- | deluge/plugins/Extractor/deluge_extractor/gtkui.py | 93 | ||||
-rw-r--r-- | deluge/plugins/Extractor/deluge_extractor/webui.py | 24 | ||||
-rw-r--r-- | deluge/plugins/Extractor/setup.py | 54 |
8 files changed, 635 insertions, 0 deletions
diff --git a/deluge/plugins/Extractor/deluge_extractor/__init__.py b/deluge/plugins/Extractor/deluge_extractor/__init__.py new file mode 100644 index 0000000..87d1584 --- /dev/null +++ b/deluge/plugins/Extractor/deluge_extractor/__init__.py @@ -0,0 +1,37 @@ +# +# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com> +# +# Basic plugin template created by: +# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> +# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com> +# +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. +# + +from deluge.plugins.init import PluginInitBase + + +class CorePlugin(PluginInitBase): + def __init__(self, plugin_name): + from .core import Core as _pluginCls + + self._plugin_cls = _pluginCls + super().__init__(plugin_name) + + +class GtkUIPlugin(PluginInitBase): + def __init__(self, plugin_name): + from .gtkui import GtkUI as _pluginCls + + self._plugin_cls = _pluginCls + super().__init__(plugin_name) + + +class WebUIPlugin(PluginInitBase): + def __init__(self, plugin_name): + from .webui import WebUI as _pluginCls + + self._plugin_cls = _pluginCls + super().__init__(plugin_name) diff --git a/deluge/plugins/Extractor/deluge_extractor/common.py b/deluge/plugins/Extractor/deluge_extractor/common.py new file mode 100644 index 0000000..eb47f13 --- /dev/null +++ b/deluge/plugins/Extractor/deluge_extractor/common.py @@ -0,0 +1,20 @@ +# +# Basic plugin template created by: +# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> +# 2007-2009 Andrew Resch <andrewresch@gmail.com> +# 2009 Damien Churchill <damoxc@gmail.com> +# 2010 Pedro Algarvio <pedro@algarvio.me> +# 2017 Calum Lind <calumlind+deluge@gmail.com> +# +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. +# + +import os.path + +from pkg_resources import resource_filename + + +def get_resource(filename): + return resource_filename(__package__, os.path.join('data', filename)) diff --git a/deluge/plugins/Extractor/deluge_extractor/core.py b/deluge/plugins/Extractor/deluge_extractor/core.py new file mode 100644 index 0000000..23b2a00 --- /dev/null +++ b/deluge/plugins/Extractor/deluge_extractor/core.py @@ -0,0 +1,186 @@ +# +# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com> +# +# Basic plugin template created by: +# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> +# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com> +# +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. +# + +import errno +import logging +import os + +from twisted.internet.utils import getProcessOutputAndValue +from twisted.python.procutils import which + +import deluge.component as component +import deluge.configmanager +from deluge.common import windows_check +from deluge.core.rpcserver import export +from deluge.plugins.pluginbase import CorePluginBase + +log = logging.getLogger(__name__) + +DEFAULT_PREFS = {'extract_path': '', 'use_name_folder': True} + +if windows_check(): + win_7z_exes = [ + '7z.exe', + 'C:\\Program Files\\7-Zip\\7z.exe', + 'C:\\Program Files (x86)\\7-Zip\\7z.exe', + ] + + import winreg + + try: + hkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, 'Software\\7-Zip') + except OSError: + pass + else: + win_7z_path = os.path.join(winreg.QueryValueEx(hkey, 'Path')[0], '7z.exe') + winreg.CloseKey(hkey) + win_7z_exes.insert(1, win_7z_path) + + switch_7z = 'x -y' + # Future suport: + # 7-zip cannot extract tar.* with single command. + # ".tar.gz", ".tgz", + # ".tar.bz2", ".tbz", + # ".tar.lzma", ".tlz", + # ".tar.xz", ".txz", + exts_7z = ['.rar', '.zip', '.tar', '.7z', '.xz', '.lzma'] + for win_7z_exe in win_7z_exes: + if which(win_7z_exe): + EXTRACT_COMMANDS = dict.fromkeys(exts_7z, [win_7z_exe, switch_7z]) + break +else: + required_cmds = ['unrar', 'unzip', 'tar', 'unxz', 'unlzma', '7zr', 'bunzip2'] + # Possible future suport: + # gunzip: gz (cmd will delete original archive) + # the following do not extract to dest dir + # ".xz": ["xz", "-d --keep"], + # ".lzma": ["xz", "-d --format=lzma --keep"], + # ".bz2": ["bzip2", "-d --keep"], + + EXTRACT_COMMANDS = { + '.rar': ['unrar', 'x -o+ -y'], + '.tar': ['tar', '-xf'], + '.zip': ['unzip', ''], + '.tar.gz': ['tar', '-xzf'], + '.tgz': ['tar', '-xzf'], + '.tar.bz2': ['tar', '-xjf'], + '.tbz': ['tar', '-xjf'], + '.tar.lzma': ['tar', '--lzma -xf'], + '.tlz': ['tar', '--lzma -xf'], + '.tar.xz': ['tar', '--xz -xf'], + '.txz': ['tar', '--xz -xf'], + '.7z': ['7zr', 'x'], + } + # Test command exists and if not, remove. + for command in required_cmds: + if not which(command): + for k, v in list(EXTRACT_COMMANDS.items()): + if command in v[0]: + log.warning('%s not found, disabling support for %s', command, k) + del EXTRACT_COMMANDS[k] + +if not EXTRACT_COMMANDS: + raise Exception('No archive extracting programs found, plugin will be disabled') + + +class Core(CorePluginBase): + def enable(self): + self.config = deluge.configmanager.ConfigManager( + 'extractor.conf', DEFAULT_PREFS + ) + if not self.config['extract_path']: + self.config['extract_path'] = deluge.configmanager.ConfigManager( + 'core.conf' + )['download_location'] + component.get('EventManager').register_event_handler( + 'TorrentFinishedEvent', self._on_torrent_finished + ) + + def disable(self): + component.get('EventManager').deregister_event_handler( + 'TorrentFinishedEvent', self._on_torrent_finished + ) + + def update(self): + pass + + def _on_torrent_finished(self, torrent_id): + """ + This is called when a torrent finishes and checks if any files to extract. + """ + tid = component.get('TorrentManager').torrents[torrent_id] + tid_status = tid.get_status(['download_location', 'name']) + + files = tid.get_files() + for f in files: + file_root, file_ext = os.path.splitext(f['path']) + file_ext_sec = os.path.splitext(file_root)[1] + if file_ext_sec and file_ext_sec + file_ext in EXTRACT_COMMANDS: + file_ext = file_ext_sec + file_ext + elif file_ext not in EXTRACT_COMMANDS or file_ext_sec == '.tar': + log.debug('Cannot extract file with unknown file type: %s', f['path']) + continue + elif file_ext == '.rar' and 'part' in file_ext_sec: + part_num = file_ext_sec.split('part')[1] + if part_num.isdigit() and int(part_num) != 1: + log.debug('Skipping remaining multi-part rar files: %s', f['path']) + continue + + cmd = EXTRACT_COMMANDS[file_ext] + fpath = os.path.join( + tid_status['download_location'], os.path.normpath(f['path']) + ) + dest = os.path.normpath(self.config['extract_path']) + if self.config['use_name_folder']: + dest = os.path.join(dest, tid_status['name']) + + try: + os.makedirs(dest) + except OSError as ex: + if not (ex.errno == errno.EEXIST and os.path.isdir(dest)): + log.error('Error creating destination folder: %s', ex) + break + + def on_extract(result, torrent_id, fpath): + # Check command exit code. + if not result[2]: + log.info('Extract successful: %s (%s)', fpath, torrent_id) + else: + log.error( + 'Extract failed: %s (%s) %s', fpath, torrent_id, result[1] + ) + + # Run the command and add callback. + log.debug( + 'Extracting %s from %s with %s %s to %s', + fpath, + torrent_id, + cmd[0], + cmd[1], + dest, + ) + d = getProcessOutputAndValue( + cmd[0], cmd[1].split() + [str(fpath)], os.environ, str(dest) + ) + d.addCallback(on_extract, torrent_id, fpath) + + @export + def set_config(self, config): + """Sets the config dictionary.""" + for key in config: + self.config[key] = config[key] + self.config.save() + + @export + def get_config(self): + """Returns the config dictionary.""" + return self.config.config diff --git a/deluge/plugins/Extractor/deluge_extractor/data/extractor.js b/deluge/plugins/Extractor/deluge_extractor/data/extractor.js new file mode 100644 index 0000000..952b645 --- /dev/null +++ b/deluge/plugins/Extractor/deluge_extractor/data/extractor.js @@ -0,0 +1,100 @@ +/** + * extractor.js + * + * Copyright (C) Calum Lind 2014 <calumlind@gmail.com> + * + * This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with + * the additional special exception to link portions of this program with the OpenSSL library. + * See LICENSE for more details. + * + */ + +Ext.ns('Deluge.ux.preferences'); + +/** + * @class Deluge.ux.preferences.ExtractorPage + * @extends Ext.Panel + */ +Deluge.ux.preferences.ExtractorPage = Ext.extend(Ext.Panel, { + title: _('Extractor'), + header: false, + layout: 'fit', + border: false, + + initComponent: function () { + Deluge.ux.preferences.ExtractorPage.superclass.initComponent.call(this); + + this.form = this.add({ + xtype: 'form', + layout: 'form', + border: false, + autoHeight: true, + }); + + fieldset = this.form.add({ + xtype: 'fieldset', + border: false, + title: '', + autoHeight: true, + labelAlign: 'top', + labelWidth: 80, + defaultType: 'textfield', + }); + + this.extract_path = fieldset.add({ + fieldLabel: _('Extract to:'), + labelSeparator: '', + name: 'extract_path', + width: '97%', + }); + + this.use_name_folder = fieldset.add({ + xtype: 'checkbox', + name: 'use_name_folder', + height: 22, + hideLabel: true, + boxLabel: _('Create torrent name sub-folder'), + }); + + this.on('show', this.updateConfig, this); + }, + + onApply: function () { + // build settings object + var config = {}; + + config['extract_path'] = this.extract_path.getValue(); + config['use_name_folder'] = this.use_name_folder.getValue(); + + deluge.client.extractor.set_config(config); + }, + + onOk: function () { + this.onApply(); + }, + + updateConfig: function () { + deluge.client.extractor.get_config({ + success: function (config) { + this.extract_path.setValue(config['extract_path']); + this.use_name_folder.setValue(config['use_name_folder']); + }, + scope: this, + }); + }, +}); + +Deluge.plugins.ExtractorPlugin = Ext.extend(Deluge.Plugin, { + name: 'Extractor', + + onDisable: function () { + deluge.preferences.removePage(this.prefsPage); + }, + + onEnable: function () { + this.prefsPage = deluge.preferences.addPage( + new Deluge.ux.preferences.ExtractorPage() + ); + }, +}); +Deluge.registerPlugin('Extractor', Deluge.plugins.ExtractorPlugin); diff --git a/deluge/plugins/Extractor/deluge_extractor/data/extractor_prefs.ui b/deluge/plugins/Extractor/deluge_extractor/data/extractor_prefs.ui new file mode 100644 index 0000000..9e8070b --- /dev/null +++ b/deluge/plugins/Extractor/deluge_extractor/data/extractor_prefs.ui @@ -0,0 +1,121 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.1 --> +<interface> + <requires lib="gtk+" version="3.0"/> + <object class="GtkWindow" id="window1"> + <property name="can_focus">False</property> + <child> + <placeholder/> + </child> + <child> + <object class="GtkBox" id="extractor_prefs_box"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkFrame" id="frame1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label_xalign">0</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkBox" id="vbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="border_width">5</property> + <property name="spacing">5</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkBox" id="hbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">5</property> + <child> + <object class="GtkLabel" id="label2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Extract to:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="hbox2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkFileChooserButton" id="folderchooser_path"> + <property name="can_focus">False</property> + <property name="action">select-folder</property> + <property name="title" translatable="yes">Select A Folder</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry_path"> + <property name="can_focus">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="chk_use_name"> + <property name="label" translatable="yes">Create torrent name sub-folder</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="tooltip_text" translatable="yes">This option will create a sub-folder using the torrent's name within the selected extract folder and put the extracted files there.</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <child type="label"> + <object class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes"><b>General</b></property> + <property name="use_markup">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + </object> + </child> + </object> +</interface> diff --git a/deluge/plugins/Extractor/deluge_extractor/gtkui.py b/deluge/plugins/Extractor/deluge_extractor/gtkui.py new file mode 100644 index 0000000..a754a5f --- /dev/null +++ b/deluge/plugins/Extractor/deluge_extractor/gtkui.py @@ -0,0 +1,93 @@ +# +# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com> +# +# Basic plugin template created by: +# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> +# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com> +# +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. +# + +import logging + +import gi # isort:skip (Required before Gtk import). + +gi.require_version('Gtk', '3.0') + +# isort:imports-thirdparty +from gi.repository import Gtk + +# isort:imports-firstparty +import deluge.component as component +from deluge.plugins.pluginbase import Gtk3PluginBase +from deluge.ui.client import client + +# isort:imports-localfolder +from .common import get_resource + +log = logging.getLogger(__name__) + + +class GtkUI(Gtk3PluginBase): + def enable(self): + self.builder = Gtk.Builder() + self.builder.add_from_file(get_resource('extractor_prefs.ui')) + + component.get('Preferences').add_page( + _('Extractor'), self.builder.get_object('extractor_prefs_box') + ) + component.get('PluginManager').register_hook( + 'on_apply_prefs', self.on_apply_prefs + ) + component.get('PluginManager').register_hook( + 'on_show_prefs', self.on_show_prefs + ) + self.on_show_prefs() + + def disable(self): + component.get('Preferences').remove_page(_('Extractor')) + component.get('PluginManager').deregister_hook( + 'on_apply_prefs', self.on_apply_prefs + ) + component.get('PluginManager').deregister_hook( + 'on_show_prefs', self.on_show_prefs + ) + del self.builder + + def on_apply_prefs(self): + log.debug('applying prefs for Extractor') + if client.is_localhost(): + path = self.builder.get_object('folderchooser_path').get_filename() + else: + path = self.builder.get_object('entry_path').get_text() + + config = { + 'extract_path': path, + 'use_name_folder': self.builder.get_object('chk_use_name').get_active(), + } + + client.extractor.set_config(config) + + def on_show_prefs(self): + if client.is_localhost(): + self.builder.get_object('folderchooser_path').show() + self.builder.get_object('entry_path').hide() + else: + self.builder.get_object('folderchooser_path').hide() + self.builder.get_object('entry_path').show() + + def on_get_config(config): + if client.is_localhost(): + self.builder.get_object('folderchooser_path').set_current_folder( + config['extract_path'] + ) + else: + self.builder.get_object('entry_path').set_text(config['extract_path']) + + self.builder.get_object('chk_use_name').set_active( + config['use_name_folder'] + ) + + client.extractor.get_config().addCallback(on_get_config) diff --git a/deluge/plugins/Extractor/deluge_extractor/webui.py b/deluge/plugins/Extractor/deluge_extractor/webui.py new file mode 100644 index 0000000..0f58658 --- /dev/null +++ b/deluge/plugins/Extractor/deluge_extractor/webui.py @@ -0,0 +1,24 @@ +# +# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com> +# +# Basic plugin template created by: +# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> +# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com> +# +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. +# + +import logging + +from deluge.plugins.pluginbase import WebPluginBase + +from .common import get_resource + +log = logging.getLogger(__name__) + + +class WebUI(WebPluginBase): + scripts = [get_resource('extractor.js')] + debug_scripts = scripts diff --git a/deluge/plugins/Extractor/setup.py b/deluge/plugins/Extractor/setup.py new file mode 100644 index 0000000..09385c6 --- /dev/null +++ b/deluge/plugins/Extractor/setup.py @@ -0,0 +1,54 @@ +# +# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com> +# +# Basic plugin template created by: +# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> +# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com> +# +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. +# + +from setuptools import find_packages, setup + +__plugin_name__ = 'Extractor' +__author__ = 'Andrew Resch' +__author_email__ = 'andrewresch@gmail.com' +__version__ = '0.7' +__url__ = 'http://deluge-torrent.org' +__license__ = 'GPLv3' +__description__ = 'Extract files upon torrent completion' +__long_description__ = """ +Extract files upon torrent completion + +Supports: .rar, .tar, .zip, .7z .tar.gz, .tgz, .tar.bz2, .tbz .tar.lzma, .tlz, .tar.xz, .txz + +Windows support: .rar, .zip, .tar, .7z, .xz, .lzma +( Requires 7-zip installed: http://www.7-zip.org/ ) + +Note: Will not extract with 'Move Completed' enabled +""" +__pkg_data__ = {'deluge_' + __plugin_name__.lower(): ['data/*']} + +setup( + name=__plugin_name__, + version=__version__, + description=__description__, + author=__author__, + author_email=__author_email__, + url=__url__, + license=__license__, + long_description=__long_description__ if __long_description__ else __description__, + packages=find_packages(), + package_data=__pkg_data__, + entry_points=""" + [deluge.plugin.core] + %s = deluge_%s:CorePlugin + [deluge.plugin.gtk3ui] + %s = deluge_%s:GtkUIPlugin + [deluge.plugin.web] + %s = deluge_%s:WebUIPlugin + """ + % ((__plugin_name__, __plugin_name__.lower()) * 3), +) |