summaryrefslogtreecommitdiffstats
path: root/deluge/plugins/Extractor/deluge_extractor
diff options
context:
space:
mode:
Diffstat (limited to 'deluge/plugins/Extractor/deluge_extractor')
-rw-r--r--deluge/plugins/Extractor/deluge_extractor/__init__.py37
-rw-r--r--deluge/plugins/Extractor/deluge_extractor/common.py20
-rw-r--r--deluge/plugins/Extractor/deluge_extractor/core.py186
-rw-r--r--deluge/plugins/Extractor/deluge_extractor/data/extractor.js100
-rw-r--r--deluge/plugins/Extractor/deluge_extractor/data/extractor_prefs.ui121
-rw-r--r--deluge/plugins/Extractor/deluge_extractor/gtkui.py93
-rw-r--r--deluge/plugins/Extractor/deluge_extractor/webui.py24
7 files changed, 581 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">&lt;b&gt;General&lt;/b&gt;</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