Compare commits
144 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b5f45b486 | ||
|
|
05e13a6b20 | ||
|
|
b64084d248 | ||
|
|
e120536d87 | ||
|
|
f52cf760e4 | ||
|
|
94d790c159 | ||
|
|
f78506161d | ||
|
|
592b05cd87 | ||
|
|
6c8f9ce756 | ||
|
|
20efcfd345 | ||
|
|
19dba297ef | ||
|
|
cbacaf0545 | ||
|
|
75db47fc1f | ||
|
|
f1ec68704d | ||
|
|
ae3fbcca77 | ||
|
|
6a10e8f3cd | ||
|
|
b0dba97fec | ||
|
|
d7c520c85e | ||
|
|
ee3180fd94 | ||
|
|
47e548fdb5 | ||
|
|
cd63efd935 | ||
|
|
7f0a380576 | ||
|
|
68c75ccc05 | ||
|
|
96a0825add | ||
|
|
61a83bbd20 | ||
|
|
bc6611fc0d | ||
|
|
970a0ae240 | ||
|
|
5acb57b5af | ||
|
|
7fa0af3446 | ||
|
|
a954348567 | ||
|
|
13be64d355 | ||
|
|
11fe22e4cd | ||
|
|
a683b7e830 | ||
|
|
b0f80f9654 | ||
|
|
f9ca3932a8 | ||
|
|
5ec5271fdd | ||
|
|
e15731fcd4 | ||
|
|
2962f7cd2c | ||
|
|
c89a366dfb | ||
|
|
5f8acabb81 | ||
|
|
055a84bb15 | ||
|
|
03938839e0 | ||
|
|
8ff4683780 | ||
|
|
62a4052178 | ||
|
|
8ece036770 | ||
|
|
a5503c0c60 | ||
|
|
f754882498 | ||
|
|
191549074c | ||
|
|
2ec6e10c8e | ||
|
|
2bd095e5bf | ||
|
|
513d5f06e5 | ||
|
|
a1da2058bc | ||
|
|
af26fdfb37 | ||
|
|
66b5a2fc40 | ||
|
|
29f0789223 | ||
|
|
f8f997a6eb | ||
|
|
374997a8d7 | ||
|
|
dabb505376 | ||
|
|
aa74261d50 | ||
|
|
b29829f571 | ||
|
|
d559f67ab9 | ||
|
|
d4f8775f44 | ||
|
|
50647ab3a5 | ||
|
|
90744dc2e6 | ||
|
|
24a3987c3a | ||
|
|
e87236514d | ||
|
|
2fb41341c9 | ||
|
|
b76f2c0f20 | ||
|
|
bd88f78af6 | ||
|
|
bf97bec994 | ||
|
|
a27a77f8c1 | ||
|
|
e8fd07e5e3 | ||
|
|
1089adb844 | ||
|
|
4096cdfdfe | ||
|
|
099077fe20 | ||
|
|
a684029602 | ||
|
|
8b0c8392b6 | ||
|
|
222aeed2f3 | ||
|
|
ece31cf3cf | ||
|
|
0fbb3882f2 | ||
|
|
73394f1fc5 | ||
|
|
9b043cf2c1 | ||
|
|
1cd005c272 | ||
|
|
4107bf8f25 | ||
|
|
49bedda956 | ||
|
|
540d557cb2 | ||
|
|
d8acadb085 | ||
|
|
932c3c123f | ||
|
|
986375fa86 | ||
|
|
4497c9bbcc | ||
|
|
23f7c4dd6e | ||
|
|
a41f950d09 | ||
|
|
209716f7cd | ||
|
|
3dca30343f | ||
|
|
71cde7c05e | ||
|
|
dbf3495c4e | ||
|
|
fffc6ab7d7 | ||
|
|
a73e01f89f | ||
|
|
87ec04af16 | ||
|
|
d8746a8852 | ||
|
|
7c9a542006 | ||
|
|
e75ef7e31f | ||
|
|
4f87612a0f | ||
|
|
2cad0f46f2 | ||
|
|
5931d0cc0b | ||
|
|
9d4ca77ef7 | ||
|
|
ad27a278fd | ||
|
|
4f17fc41a5 | ||
|
|
15d2d27a53 | ||
|
|
65e5010e7f | ||
|
|
9b97c74025 | ||
|
|
d62362d6ae | ||
|
|
1a9affbbac | ||
|
|
2316088f5c | ||
|
|
d14310078b | ||
|
|
1696c69776 | ||
|
|
5f96ea4217 | ||
|
|
491a20cb08 | ||
|
|
490fb898af | ||
|
|
560a52a443 | ||
|
|
b9a208f18f | ||
|
|
6da4c4bf66 | ||
|
|
d2390cd247 | ||
|
|
c3cd7f5e5c | ||
|
|
2351d65844 | ||
|
|
e50927f575 | ||
|
|
79b7e6093f | ||
|
|
4f0c786649 | ||
|
|
fca08cf583 | ||
|
|
517b2c653b | ||
|
|
44dcbee5f4 | ||
|
|
efc9f465f0 | ||
|
|
5321d24f2a | ||
|
|
f30f7f4629 | ||
|
|
ec0bcc11f5 | ||
|
|
16895b4a49 | ||
|
|
f3784723ae | ||
|
|
7f5857296e | ||
|
|
897955f0a1 | ||
|
|
ff309ea4c5 | ||
|
|
3b11613cc7 | ||
|
|
a2d0cb7141 | ||
|
|
88ffd1b843 | ||
|
|
6a10e57f7e |
1
.gitattributes
vendored
1
.gitattributes
vendored
@ -3,3 +3,4 @@
|
|||||||
.gitignore export-ignore
|
.gitignore export-ignore
|
||||||
*.py diff=python
|
*.py diff=python
|
||||||
ext-all.js diff=minjs
|
ext-all.js diff=minjs
|
||||||
|
*.state -merge -text
|
||||||
|
|||||||
100
.github/workflows/cd.yml
vendored
Normal file
100
.github/workflows/cd.yml
vendored
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
name: Package
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "deluge-*"
|
||||||
|
- "!deluge*-dev*"
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
pull_request:
|
||||||
|
types: [labeled, opened, synchronize, reopened]
|
||||||
|
|
||||||
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
ref:
|
||||||
|
description: "Enter a tag or commit to package"
|
||||||
|
default: ""
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
windows_package:
|
||||||
|
runs-on: windows-2019
|
||||||
|
if: (github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'package'))
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
arch: [x64, x86]
|
||||||
|
python: ["3.9"]
|
||||||
|
libtorrent: [2.0.6, 1.2.15]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
# Checkout Deluge source to subdir to enable packaging any tag/commit
|
||||||
|
- name: Checkout Deluge source
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.inputs.ref }}
|
||||||
|
fetch-depth: 0
|
||||||
|
path: deluge_src
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python}}
|
||||||
|
architecture: ${{ matrix.arch }}
|
||||||
|
cache: pip
|
||||||
|
|
||||||
|
- name: Prepare pip
|
||||||
|
run: python -m pip install wheel
|
||||||
|
|
||||||
|
- name: Install GTK
|
||||||
|
run: |
|
||||||
|
$WebClient = New-Object System.Net.WebClient
|
||||||
|
$WebClient.DownloadFile("https://github.com/deluge-torrent/gvsbuild-release/releases/download/latest/gvsbuild-py${{ matrix.python }}-vs16-${{ matrix.arch }}.zip","C:\GTK.zip")
|
||||||
|
7z x C:\GTK.zip -oc:\GTK
|
||||||
|
echo "C:\GTK\release\lib" | Out-File -FilePath $env:GITHUB_PATH -Append
|
||||||
|
echo "C:\GTK\release\bin" | Out-File -FilePath $env:GITHUB_PATH -Append
|
||||||
|
echo "C:\GTK\release" | Out-File -FilePath $env:GITHUB_PATH -Append
|
||||||
|
python -m pip install --no-index --find-links="C:\GTK\release\python" pycairo PyGObject
|
||||||
|
|
||||||
|
- name: Install Python dependencies
|
||||||
|
run: >
|
||||||
|
python -m pip install
|
||||||
|
twisted[tls]==22.4.0
|
||||||
|
libtorrent==${{ matrix.libtorrent }}
|
||||||
|
pyinstaller==4.10
|
||||||
|
pygame
|
||||||
|
-r requirements.txt
|
||||||
|
|
||||||
|
- name: Install Deluge
|
||||||
|
working-directory: deluge_src
|
||||||
|
run: |
|
||||||
|
python -m pip install .
|
||||||
|
python setup.py install_scripts
|
||||||
|
|
||||||
|
- name: Freeze Deluge
|
||||||
|
working-directory: packaging/win
|
||||||
|
run: |
|
||||||
|
pyinstaller --clean delugewin.spec --distpath freeze
|
||||||
|
|
||||||
|
- name: Fix OpenSSL for libtorrent x64
|
||||||
|
if: ${{ matrix.arch == 'x64' }}
|
||||||
|
working-directory: packaging/win/freeze/Deluge
|
||||||
|
run: |
|
||||||
|
cp libssl-1_1.dll libssl-1_1-x64.dll
|
||||||
|
cp libcrypto-1_1.dll libcrypto-1_1-x64.dll
|
||||||
|
|
||||||
|
- name: Make Deluge Installer
|
||||||
|
working-directory: ./packaging/win
|
||||||
|
run: |
|
||||||
|
python setup_nsis.py
|
||||||
|
makensis /Darch=${{ matrix.arch }} deluge-win-installer.nsi
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: deluge-py${{ matrix.python }}-lt${{ matrix.libtorrent }}-${{ matrix.arch }}
|
||||||
|
path: packaging/win/*.exe
|
||||||
64
.github/workflows/ci.yml
vendored
64
.github/workflows/ci.yml
vendored
@ -10,6 +10,9 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
test-linux:
|
test-linux:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.7", "3.10"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||||
@ -20,26 +23,13 @@ jobs:
|
|||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: "3.8"
|
python-version: ${{ matrix.python-version }}
|
||||||
|
cache: "pip"
|
||||||
|
cache-dependency-path: "requirements*.txt"
|
||||||
|
|
||||||
- name: Cache pip
|
- name: Sets env var for security
|
||||||
uses: actions/cache@v2
|
if: (github.event_name == 'pull_request' && contains(github.event.pull_request.body, 'security_test')) || (github.event_name == 'push' && contains(github.event.head_commit.message, 'security_test'))
|
||||||
with:
|
run: echo "SECURITY_TESTS=True" >> $GITHUB_ENV
|
||||||
path: ~/.cache/pip
|
|
||||||
# Look to see if there is a cache hit for the corresponding requirements file
|
|
||||||
key: ${{ runner.os }}-pip-${{ hashFiles('tox.ini', 'setup.py', 'requirements*.txt') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-pip-
|
|
||||||
${{ runner.os }}-
|
|
||||||
|
|
||||||
- name: Add libtorrent deb repository
|
|
||||||
uses: myci-actions/add-deb-repo@8
|
|
||||||
with:
|
|
||||||
repo: deb http://ppa.launchpad.net/libtorrent.org/1.2-daily/ubuntu focal main
|
|
||||||
repo-name: libtorrent
|
|
||||||
keys: 58E5430D9667FAEFFCA0B93F32309D6B9E009EDB
|
|
||||||
key-server: keyserver.ubuntu.com
|
|
||||||
install: python3-libtorrent-dbg
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
@ -47,6 +37,15 @@ jobs:
|
|||||||
pip install -r requirements.txt -r requirements-tests.txt
|
pip install -r requirements.txt -r requirements-tests.txt
|
||||||
pip install -e .
|
pip install -e .
|
||||||
|
|
||||||
|
- name: Install security dependencies
|
||||||
|
if: contains(env.SECURITY_TESTS, 'True')
|
||||||
|
run: |
|
||||||
|
wget -O- $TESTSSL_URL$TESTSSL_VER | tar xz
|
||||||
|
mv -t deluge/tests/data testssl.sh-$TESTSSL_VER/testssl.sh testssl.sh-$TESTSSL_VER/etc/;
|
||||||
|
env:
|
||||||
|
TESTSSL_VER: 3.0.6
|
||||||
|
TESTSSL_URL: https://codeload.github.com/drwetter/testssl.sh/tar.gz/refs/tags/v
|
||||||
|
|
||||||
- name: Setup core dump directory
|
- name: Setup core dump directory
|
||||||
run: |
|
run: |
|
||||||
sudo mkdir /cores/ && sudo chmod 777 /cores/
|
sudo mkdir /cores/ && sudo chmod 777 /cores/
|
||||||
@ -55,9 +54,8 @@ jobs:
|
|||||||
- name: Test with pytest
|
- name: Test with pytest
|
||||||
run: |
|
run: |
|
||||||
ulimit -c unlimited # Enable core dumps to be captured
|
ulimit -c unlimited # Enable core dumps to be captured
|
||||||
cp /usr/lib/python3/dist-packages/libtorrent*.so $GITHUB_WORKSPACE/deluge
|
|
||||||
python -c 'from deluge._libtorrent import lt; print(lt.__version__)';
|
python -c 'from deluge._libtorrent import lt; print(lt.__version__)';
|
||||||
catchsegv python -X dev -m pytest -v -m "not (todo or gtkui or security)" deluge
|
catchsegv python -X dev -m pytest -v -m "not (todo or gtkui)" deluge
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v2
|
||||||
# capture all crashes as build artifacts
|
# capture all crashes as build artifacts
|
||||||
@ -67,7 +65,10 @@ jobs:
|
|||||||
path: /cores
|
path: /cores
|
||||||
|
|
||||||
test-windows:
|
test-windows:
|
||||||
runs-on: windows-latest
|
runs-on: windows-2019
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.7", "3.10"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||||
@ -78,26 +79,17 @@ jobs:
|
|||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: "3.7"
|
python-version: ${{ matrix.python-version }}
|
||||||
|
cache: "pip"
|
||||||
- name: Cache pip
|
cache-dependency-path: "requirements*.txt"
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: '%LOCALAPPDATA%\pip\Cache'
|
|
||||||
# Look to see if there is a cache hit for the corresponding requirements file
|
|
||||||
key: ${{ runner.os }}-pip-${{ hashFiles('tox.ini', 'setup.py', 'requirements*.txt') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-pip-
|
|
||||||
${{ runner.os }}-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip wheel
|
pip install --upgrade pip wheel
|
||||||
python -m pip install libtorrent==1.2.*
|
|
||||||
pip install -r requirements.txt -r requirements-tests.txt
|
pip install -r requirements.txt -r requirements-tests.txt
|
||||||
pip install -e .
|
pip install -e .
|
||||||
|
|
||||||
- name: Test with pytest
|
- name: Test with pytest
|
||||||
run: |
|
run: |
|
||||||
python -c 'import libtorrent as lt; print(lt.__version__)';
|
python -c 'import libtorrent as lt; print(lt.__version__)';
|
||||||
pytest -m "not (todo or gtkui or security)" deluge
|
pytest -v -m "not (todo or gtkui or security)" deluge
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -10,7 +10,6 @@ docs/source/modules/deluge*.rst
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*.tar.*
|
*.tar.*
|
||||||
_trial_temp
|
|
||||||
.tox/
|
.tox/
|
||||||
deluge/i18n/*/
|
deluge/i18n/*/
|
||||||
deluge.pot
|
deluge.pot
|
||||||
|
|||||||
@ -3,32 +3,35 @@ default_language_version:
|
|||||||
exclude: >
|
exclude: >
|
||||||
(?x)^(
|
(?x)^(
|
||||||
deluge/ui/web/docs/template/.*|
|
deluge/ui/web/docs/template/.*|
|
||||||
|
deluge/tests/data/.*svg|
|
||||||
)$
|
)$
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/ambv/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 20.8b1
|
rev: 22.3.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
name: Fmt Black
|
name: Fmt Black
|
||||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||||
rev: v2.2.1
|
rev: v2.5.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: prettier
|
- id: prettier
|
||||||
name: Fmt Prettier
|
name: Fmt Prettier
|
||||||
# Workaround to list modified files only.
|
# Workaround to list modified files only.
|
||||||
args: [--list-different]
|
args: [--list-different]
|
||||||
- repo: https://gitlab.com/pycqa/flake8
|
- repo: https://github.com/pycqa/isort
|
||||||
# v3.7.9 due to E402 issue: https://gitlab.com/pycqa/flake8/-/issues/638
|
rev: 5.10.1
|
||||||
rev: 3.7.9
|
hooks:
|
||||||
|
- id: isort
|
||||||
|
name: Fmt isort
|
||||||
|
- repo: https://github.com/pycqa/flake8
|
||||||
|
rev: 4.0.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
name: Chk Flake8
|
name: Chk Flake8
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- flake8-isort==4.0.0
|
- pep8-naming==0.12.1
|
||||||
- pep8-naming==0.11.1
|
|
||||||
args: [--isort-show-traceback]
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v3.4.0
|
rev: v4.1.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: double-quote-string-fixer
|
- id: double-quote-string-fixer
|
||||||
name: Fix Double-quotes
|
name: Fix Double-quotes
|
||||||
@ -40,3 +43,9 @@ repos:
|
|||||||
args: [--fix=auto]
|
args: [--fix=auto]
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
name: Fix Trailing whitespace
|
name: Fix Trailing whitespace
|
||||||
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
|
rev: v2.31.0
|
||||||
|
hooks:
|
||||||
|
- id: pyupgrade
|
||||||
|
args: [--py36-plus]
|
||||||
|
stages: [manual]
|
||||||
|
|||||||
@ -289,7 +289,7 @@ callbacks=cb_,_cb
|
|||||||
|
|
||||||
# List of qualified module names which can have objects that can redefine
|
# List of qualified module names which can have objects that can redefine
|
||||||
# builtins.
|
# builtins.
|
||||||
redefining-builtins-modules=six.moves,future.builtins,future_builtins
|
redefining-builtins-modules=
|
||||||
|
|
||||||
|
|
||||||
[TYPECHECK]
|
[TYPECHECK]
|
||||||
@ -359,11 +359,6 @@ known-standard-library=
|
|||||||
# Force import order to recognize a module as part of a third party library.
|
# Force import order to recognize a module as part of a third party library.
|
||||||
known-third-party=enchant
|
known-third-party=enchant
|
||||||
|
|
||||||
# Analyse import fallback blocks. This can be used to support both Python 2 and
|
|
||||||
# 3 compatible code, which means that the block might have code that exists
|
|
||||||
# only in one or another interpreter, leading to false positives when analysed.
|
|
||||||
analyse-fallback-blocks=no
|
|
||||||
|
|
||||||
|
|
||||||
[DESIGN]
|
[DESIGN]
|
||||||
|
|
||||||
|
|||||||
82
CHANGELOG.md
82
CHANGELOG.md
@ -1,5 +1,87 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2.1.1 (2022-07-10)
|
||||||
|
|
||||||
|
### Core
|
||||||
|
|
||||||
|
- Fix missing trackers added via magnet
|
||||||
|
- Fix handling magnets with tracker tiers
|
||||||
|
|
||||||
|
## 2.1.0 (2022-06-28)
|
||||||
|
|
||||||
|
### Breaking changes
|
||||||
|
|
||||||
|
- Python 2 support removed (Python >= 3.6)
|
||||||
|
- libtorrent minimum requirement increased (>= 1.2).
|
||||||
|
|
||||||
|
### Core
|
||||||
|
|
||||||
|
- Add support for SVG tracker icons.
|
||||||
|
- Fix tracker icon error handling.
|
||||||
|
- Fix cleaning-up tracker icon temp files.
|
||||||
|
- Fix Plugin manager to handle new metadata 2.1.
|
||||||
|
- Hide passwords in config logs.
|
||||||
|
- Fix cleaning-up temp files in add_torrent_url.
|
||||||
|
- Fix KeyError in sessionproxy after torrent delete.
|
||||||
|
- Remove libtorrent deprecated functions.
|
||||||
|
- Fix file_completed_alert handling.
|
||||||
|
- Add plugin keys to get_torrents_status.
|
||||||
|
- Add support for pygeoip dependency.
|
||||||
|
- Fix crash logging to Windows protected folder.
|
||||||
|
- Add is_interface and is_interface_name to validate network interfaces.
|
||||||
|
- Fix is_url and is_infohash error with None value.
|
||||||
|
- Fix load_libintl error.
|
||||||
|
- Add support for IPv6 in host lists.
|
||||||
|
- Add systemd user services.
|
||||||
|
- Fix refresh and expire the torrent status cache.
|
||||||
|
- Fix crash when logging errors initializing gettext.
|
||||||
|
|
||||||
|
### Web UI
|
||||||
|
|
||||||
|
- Fix ETA column sorting in correct order (#3413).
|
||||||
|
- Fix defining foreground and background colors.
|
||||||
|
- Accept charset in content-type for json messages.
|
||||||
|
- Fix 'Complete Seen' and 'Completed' sorting.
|
||||||
|
- Fix encoding HTML entities for torrent attributes to prevent XSS.
|
||||||
|
|
||||||
|
### Gtk UI
|
||||||
|
|
||||||
|
- Fix download location textbox width.
|
||||||
|
- Fix obscured port number in Connection Manager.
|
||||||
|
- Increase connection manager default height.
|
||||||
|
- Fix bug with setting move completed in Options tab.
|
||||||
|
- Fix adding daemon accounts.
|
||||||
|
- Add workaround for crash on Windows with ico or gif icons.
|
||||||
|
- Hide account password length in log.
|
||||||
|
- Added a torrent menu option for magnet copy.
|
||||||
|
- Fix unable to prefetch magnet in thinclient mode.
|
||||||
|
- Use GtkSpinner when testing open port.
|
||||||
|
- Update About Dialog year.
|
||||||
|
- Fix Edit Torrents dialogs close issues.
|
||||||
|
- Fix ETA being copied to neighboring empty cells.
|
||||||
|
- Disable GTK CSD by default on Windows.
|
||||||
|
|
||||||
|
### Console UI
|
||||||
|
|
||||||
|
- Fix curses.init_pair raise ValueError on Py3.10.
|
||||||
|
- Swap j and k key's behavior to fit vim mode.
|
||||||
|
- Fix torrent details status error.
|
||||||
|
- Fix incorrect test for when a host is online.
|
||||||
|
- Add the torrent label to info command.
|
||||||
|
|
||||||
|
### AutoAdd
|
||||||
|
|
||||||
|
- Fix handling torrent decode errors.
|
||||||
|
- Fix error dialog not being shown on error.
|
||||||
|
|
||||||
|
### Blocklist
|
||||||
|
|
||||||
|
- Add frequency unit to interval label.
|
||||||
|
|
||||||
|
### Notifications
|
||||||
|
|
||||||
|
- Fix UnicodeEncodeError upon non-ascii torrent name.
|
||||||
|
|
||||||
## 2.0.5 (2021-12-15)
|
## 2.0.5 (2021-12-15)
|
||||||
|
|
||||||
### WebUI
|
### WebUI
|
||||||
|
|||||||
11
DEPENDS.md
11
DEPENDS.md
@ -7,7 +7,7 @@ All modules will require the [common](#common) section dependencies.
|
|||||||
|
|
||||||
## Prerequisite
|
## Prerequisite
|
||||||
|
|
||||||
- [Python] _>= 3.5_
|
- [Python] _>= 3.6_
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
@ -23,12 +23,12 @@ All modules will require the [common](#common) section dependencies.
|
|||||||
- [rencode] _>= 1.0.2_ - Encoding library.
|
- [rencode] _>= 1.0.2_ - Encoding library.
|
||||||
- [PyXDG] - Access freedesktop.org standards for \*nix.
|
- [PyXDG] - Access freedesktop.org standards for \*nix.
|
||||||
- [xdg-utils] - Provides xdg-open for \*nix.
|
- [xdg-utils] - Provides xdg-open for \*nix.
|
||||||
- [six]
|
|
||||||
- [zope.interface]
|
- [zope.interface]
|
||||||
- [chardet] - Optional: Encoding detection.
|
- [chardet] - Optional: Encoding detection.
|
||||||
- [setproctitle] - Optional: Renaming processes.
|
- [setproctitle] - Optional: Renaming processes.
|
||||||
- [Pillow] - Optional: Support for resizing tracker icons.
|
- [Pillow] - Optional: Support for resizing tracker icons.
|
||||||
- [dbus-python] - Optional: Show item location in filemanager.
|
- [dbus-python] - Optional: Show item location in filemanager.
|
||||||
|
- [ifaddr] - Optional: Verify network interfaces.
|
||||||
|
|
||||||
### Linux and BSD
|
### Linux and BSD
|
||||||
|
|
||||||
@ -41,8 +41,8 @@ All modules will require the [common](#common) section dependencies.
|
|||||||
|
|
||||||
## Core (deluged daemon)
|
## Core (deluged daemon)
|
||||||
|
|
||||||
- [libtorrent] _>= 1.1.1_
|
- [libtorrent] _>= 1.2.0_
|
||||||
- [GeoIP] - Optional: IP address location lookup. (_Debian: `python-geoip`_)
|
- [GeoIP] or [pygeoip] - Optional: IP address country lookup. (_Debian: `python-geoip`_)
|
||||||
|
|
||||||
## GTK UI
|
## GTK UI
|
||||||
|
|
||||||
@ -81,14 +81,12 @@ All modules will require the [common](#common) section dependencies.
|
|||||||
[distro]: https://github.com/nir0s/distro
|
[distro]: https://github.com/nir0s/distro
|
||||||
[pywin32]: https://github.com/mhammond/pywin32
|
[pywin32]: https://github.com/mhammond/pywin32
|
||||||
[certifi]: https://pypi.org/project/certifi/
|
[certifi]: https://pypi.org/project/certifi/
|
||||||
[py2-ipaddress]: https://pypi.org/project/py2-ipaddress/
|
|
||||||
[dbus-python]: https://pypi.org/project/dbus-python/
|
[dbus-python]: https://pypi.org/project/dbus-python/
|
||||||
[setproctitle]: https://pypi.org/project/setproctitle/
|
[setproctitle]: https://pypi.org/project/setproctitle/
|
||||||
[gtkosxapplication]: https://github.com/jralls/gtk-mac-integration
|
[gtkosxapplication]: https://github.com/jralls/gtk-mac-integration
|
||||||
[chardet]: https://chardet.github.io/
|
[chardet]: https://chardet.github.io/
|
||||||
[rencode]: https://github.com/aresch/rencode
|
[rencode]: https://github.com/aresch/rencode
|
||||||
[pyxdg]: https://www.freedesktop.org/wiki/Software/pyxdg/
|
[pyxdg]: https://www.freedesktop.org/wiki/Software/pyxdg/
|
||||||
[six]: https://pythonhosted.org/six/
|
|
||||||
[xdg-utils]: https://www.freedesktop.org/wiki/Software/xdg-utils/
|
[xdg-utils]: https://www.freedesktop.org/wiki/Software/xdg-utils/
|
||||||
[gtk+]: https://www.gtk.org/
|
[gtk+]: https://www.gtk.org/
|
||||||
[pycairo]: https://cairographics.org/pycairo/
|
[pycairo]: https://cairographics.org/pycairo/
|
||||||
@ -99,3 +97,4 @@ All modules will require the [common](#common) section dependencies.
|
|||||||
[libnotify]: https://developer.gnome.org/libnotify/
|
[libnotify]: https://developer.gnome.org/libnotify/
|
||||||
[python-appindicator]: https://packages.ubuntu.com/xenial/python-appindicator
|
[python-appindicator]: https://packages.ubuntu.com/xenial/python-appindicator
|
||||||
[librsvg]: https://wiki.gnome.org/action/show/Projects/LibRsvg
|
[librsvg]: https://wiki.gnome.org/action/show/Projects/LibRsvg
|
||||||
|
[ifaddr]: https://pypi.org/project/ifaddr/
|
||||||
|
|||||||
@ -59,6 +59,7 @@ See the [Thinclient guide] to connect to the daemon from another computer.
|
|||||||
- [User guide][user guide]
|
- [User guide][user guide]
|
||||||
- [Forum](https://forum.deluge-torrent.org)
|
- [Forum](https://forum.deluge-torrent.org)
|
||||||
- [IRC Libera.Chat #deluge](irc://irc.libera.chat/deluge)
|
- [IRC Libera.Chat #deluge](irc://irc.libera.chat/deluge)
|
||||||
|
- [Discord](https://discord.gg/nwaHSE6tqn)
|
||||||
|
|
||||||
[user guide]: https://dev.deluge-torrent.org/wiki/UserGuide
|
[user guide]: https://dev.deluge-torrent.org/wiki/UserGuide
|
||||||
[thinclient guide]: https://dev.deluge-torrent.org/wiki/UserGuide/ThinClient
|
[thinclient guide]: https://dev.deluge-torrent.org/wiki/UserGuide/ThinClient
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
||||||
#
|
#
|
||||||
@ -15,8 +14,6 @@ Example:
|
|||||||
>>> from deluge._libtorrent import lt
|
>>> from deluge._libtorrent import lt
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from deluge.common import VersionSplit, get_version
|
from deluge.common import VersionSplit, get_version
|
||||||
from deluge.error import LibtorrentImportError
|
from deluge.error import LibtorrentImportError
|
||||||
|
|
||||||
@ -29,10 +26,10 @@ except ImportError:
|
|||||||
raise LibtorrentImportError('No libtorrent library found: %s' % (ex))
|
raise LibtorrentImportError('No libtorrent library found: %s' % (ex))
|
||||||
|
|
||||||
|
|
||||||
REQUIRED_VERSION = '1.1.2.0'
|
REQUIRED_VERSION = '1.2.0.0'
|
||||||
LT_VERSION = lt.__version__
|
LT_VERSION = lt.__version__
|
||||||
|
|
||||||
if VersionSplit(LT_VERSION) < VersionSplit(REQUIRED_VERSION):
|
if VersionSplit(LT_VERSION) < VersionSplit(REQUIRED_VERSION):
|
||||||
raise LibtorrentImportError(
|
raise LibtorrentImportError(
|
||||||
'Deluge %s requires libtorrent >= %s' % (get_version(), REQUIRED_VERSION)
|
f'Deluge {get_version()} requires libtorrent >= {REQUIRED_VERSION}'
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
|
||||||
#
|
#
|
||||||
@ -7,8 +6,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@ -95,7 +92,7 @@ def _get_version_detail():
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
version_str += 'Python: %s\n' % platform.python_version()
|
version_str += 'Python: %s\n' % platform.python_version()
|
||||||
version_str += 'OS: %s %s\n' % (platform.system(), common.get_os_version())
|
version_str += f'OS: {platform.system()} {common.get_os_version()}\n'
|
||||||
return version_str
|
return version_str
|
||||||
|
|
||||||
|
|
||||||
@ -109,8 +106,8 @@ class DelugeTextHelpFormatter(argparse.RawDescriptionHelpFormatter):
|
|||||||
line instead. This way list formatting is not mangled by textwrap.wrap.
|
line instead. This way list formatting is not mangled by textwrap.wrap.
|
||||||
"""
|
"""
|
||||||
wrapped_lines = []
|
wrapped_lines = []
|
||||||
for l in text.splitlines():
|
for line in text.splitlines():
|
||||||
wrapped_lines.extend(textwrap.wrap(l, width, subsequent_indent=' '))
|
wrapped_lines.extend(textwrap.wrap(line, width, subsequent_indent=' '))
|
||||||
return wrapped_lines
|
return wrapped_lines
|
||||||
|
|
||||||
def _format_action_invocation(self, action):
|
def _format_action_invocation(self, action):
|
||||||
@ -137,7 +134,7 @@ class DelugeTextHelpFormatter(argparse.RawDescriptionHelpFormatter):
|
|||||||
default = action.dest.upper()
|
default = action.dest.upper()
|
||||||
args_string = self._format_args(action, default)
|
args_string = self._format_args(action, default)
|
||||||
opt = ', '.join(action.option_strings)
|
opt = ', '.join(action.option_strings)
|
||||||
parts.append('%s %s' % (opt, args_string))
|
parts.append(f'{opt} {args_string}')
|
||||||
return ', '.join(parts)
|
return ', '.join(parts)
|
||||||
|
|
||||||
|
|
||||||
@ -165,7 +162,7 @@ class ArgParserBase(argparse.ArgumentParser):
|
|||||||
self.log_stream = kwargs['log_stream']
|
self.log_stream = kwargs['log_stream']
|
||||||
del kwargs['log_stream']
|
del kwargs['log_stream']
|
||||||
|
|
||||||
super(ArgParserBase, self).__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.common_setup = False
|
self.common_setup = False
|
||||||
self.process_arg_group = False
|
self.process_arg_group = False
|
||||||
@ -202,7 +199,7 @@ class ArgParserBase(argparse.ArgumentParser):
|
|||||||
self.group.add_argument(
|
self.group.add_argument(
|
||||||
'-L',
|
'-L',
|
||||||
'--loglevel',
|
'--loglevel',
|
||||||
choices=[l for k in deluge.log.levels for l in (k, k.upper())],
|
choices=[level for k in deluge.log.levels for level in (k, k.upper())],
|
||||||
help=_('Set the log level (none, error, warning, info, debug)'),
|
help=_('Set the log level (none, error, warning, info, debug)'),
|
||||||
metavar='<level>',
|
metavar='<level>',
|
||||||
)
|
)
|
||||||
@ -246,7 +243,7 @@ class ArgParserBase(argparse.ArgumentParser):
|
|||||||
argparse.Namespace: The parsed arguments.
|
argparse.Namespace: The parsed arguments.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
options = super(ArgParserBase, self).parse_args(args=args)
|
options = super().parse_args(args=args)
|
||||||
return self._handle_ui_options(options)
|
return self._handle_ui_options(options)
|
||||||
|
|
||||||
def parse_known_ui_args(self, args, withhold=None):
|
def parse_known_ui_args(self, args, withhold=None):
|
||||||
@ -262,7 +259,7 @@ class ArgParserBase(argparse.ArgumentParser):
|
|||||||
"""
|
"""
|
||||||
if withhold:
|
if withhold:
|
||||||
args = [a for a in args if a not in withhold]
|
args = [a for a in args if a not in withhold]
|
||||||
options, remaining = super(ArgParserBase, self).parse_known_args(args=args)
|
options, remaining = super().parse_known_args(args=args)
|
||||||
options.remaining = remaining
|
options.remaining = remaining
|
||||||
# Handle common and process group options
|
# Handle common and process group options
|
||||||
return self._handle_ui_options(options)
|
return self._handle_ui_options(options)
|
||||||
|
|||||||
@ -9,13 +9,7 @@
|
|||||||
# License.
|
# License.
|
||||||
|
|
||||||
# Written by Petru Paler
|
# Written by Petru Paler
|
||||||
# Updated by Calum Lind to support both Python 2 and Python 3.
|
# Updated by Calum Lind to support Python 3.
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from sys import version_info
|
|
||||||
|
|
||||||
PY2 = version_info.major == 2
|
|
||||||
|
|
||||||
|
|
||||||
class BTFailure(Exception):
|
class BTFailure(Exception):
|
||||||
@ -90,7 +84,7 @@ def bdecode(x):
|
|||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
class Bencached(object):
|
class Bencached:
|
||||||
|
|
||||||
__slots__ = ['bencoded']
|
__slots__ = ['bencoded']
|
||||||
|
|
||||||
@ -146,10 +140,6 @@ encode_func[dict] = encode_dict
|
|||||||
encode_func[bool] = encode_bool
|
encode_func[bool] = encode_bool
|
||||||
encode_func[str] = encode_string
|
encode_func[str] = encode_string
|
||||||
encode_func[bytes] = encode_bytes
|
encode_func[bytes] = encode_bytes
|
||||||
if PY2:
|
|
||||||
encode_func[long] = encode_int # noqa: F821
|
|
||||||
encode_func[str] = encode_bytes
|
|
||||||
encode_func[unicode] = encode_string # noqa: F821
|
|
||||||
|
|
||||||
|
|
||||||
def bencode(x):
|
def bencode(x):
|
||||||
|
|||||||
272
deluge/common.py
272
deluge/common.py
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2007,2008 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2007,2008 Andrew Resch <andrewresch@gmail.com>
|
||||||
#
|
#
|
||||||
@ -8,25 +7,25 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
"""Common functions for various parts of Deluge to use."""
|
"""Common functions for various parts of Deluge to use."""
|
||||||
from __future__ import division, print_function, unicode_literals
|
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import binascii
|
import binascii
|
||||||
import functools
|
import functools
|
||||||
import glob
|
import glob
|
||||||
import locale
|
|
||||||
import logging
|
import logging
|
||||||
import numbers
|
import numbers
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import re
|
import re
|
||||||
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tarfile
|
import tarfile
|
||||||
import time
|
import time
|
||||||
from contextlib import closing
|
from contextlib import closing
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from io import BytesIO, open
|
from io import BytesIO
|
||||||
|
from urllib.parse import unquote_plus, urljoin
|
||||||
|
from urllib.request import pathname2url
|
||||||
|
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
|
|
||||||
@ -38,14 +37,6 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
chardet = None
|
chardet = None
|
||||||
|
|
||||||
try:
|
|
||||||
from urllib.parse import unquote_plus, urljoin
|
|
||||||
from urllib.request import pathname2url
|
|
||||||
except ImportError:
|
|
||||||
# PY2 fallback
|
|
||||||
from urllib import pathname2url, unquote_plus # pylint: disable=ungrouped-imports
|
|
||||||
from urlparse import urljoin # pylint: disable=ungrouped-imports
|
|
||||||
|
|
||||||
# Windows workaround for HTTPS requests requiring certificate authority bundle.
|
# Windows workaround for HTTPS requests requiring certificate authority bundle.
|
||||||
# see: https://twistedmatrix.com/trac/ticket/9209
|
# see: https://twistedmatrix.com/trac/ticket/9209
|
||||||
if platform.system() in ('Windows', 'Microsoft'):
|
if platform.system() in ('Windows', 'Microsoft'):
|
||||||
@ -53,6 +44,11 @@ if platform.system() in ('Windows', 'Microsoft'):
|
|||||||
|
|
||||||
os.environ['SSL_CERT_FILE'] = where()
|
os.environ['SSL_CERT_FILE'] = where()
|
||||||
|
|
||||||
|
try:
|
||||||
|
import ifaddr
|
||||||
|
except ImportError:
|
||||||
|
ifaddr = None
|
||||||
|
|
||||||
|
|
||||||
if platform.system() not in ('Windows', 'Microsoft', 'Darwin'):
|
if platform.system() not in ('Windows', 'Microsoft', 'Darwin'):
|
||||||
# gi makes dbus available on Window but don't import it as unused.
|
# gi makes dbus available on Window but don't import it as unused.
|
||||||
@ -84,7 +80,8 @@ JSON_FORMAT = {'indent': 4, 'sort_keys': True, 'ensure_ascii': False}
|
|||||||
DBUS_FM_ID = 'org.freedesktop.FileManager1'
|
DBUS_FM_ID = 'org.freedesktop.FileManager1'
|
||||||
DBUS_FM_PATH = '/org/freedesktop/FileManager1'
|
DBUS_FM_PATH = '/org/freedesktop/FileManager1'
|
||||||
|
|
||||||
PY2 = sys.version_info.major == 2
|
# Retained for plugin backward compatibility
|
||||||
|
PY2 = False
|
||||||
|
|
||||||
|
|
||||||
def get_version():
|
def get_version():
|
||||||
@ -111,10 +108,8 @@ def get_default_config_dir(filename=None):
|
|||||||
def save_config_path(resource):
|
def save_config_path(resource):
|
||||||
app_data_path = os.environ.get('APPDATA')
|
app_data_path = os.environ.get('APPDATA')
|
||||||
if not app_data_path:
|
if not app_data_path:
|
||||||
try:
|
import winreg
|
||||||
import winreg
|
|
||||||
except ImportError:
|
|
||||||
import _winreg as winreg # For Python 2.
|
|
||||||
hkey = winreg.OpenKey(
|
hkey = winreg.OpenKey(
|
||||||
winreg.HKEY_CURRENT_USER,
|
winreg.HKEY_CURRENT_USER,
|
||||||
'Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders',
|
'Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders',
|
||||||
@ -147,14 +142,14 @@ def get_default_download_dir():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
user_dirs_path = os.path.join(xdg_config_home, 'user-dirs.dirs')
|
user_dirs_path = os.path.join(xdg_config_home, 'user-dirs.dirs')
|
||||||
with open(user_dirs_path, 'r', encoding='utf8') as _file:
|
with open(user_dirs_path, encoding='utf8') as _file:
|
||||||
for line in _file:
|
for line in _file:
|
||||||
if not line.startswith('#') and line.startswith('XDG_DOWNLOAD_DIR'):
|
if not line.startswith('#') and line.startswith('XDG_DOWNLOAD_DIR'):
|
||||||
download_dir = os.path.expandvars(
|
download_dir = os.path.expandvars(
|
||||||
line.partition('=')[2].rstrip().strip('"')
|
line.partition('=')[2].rstrip().strip('"')
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
except IOError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if not download_dir:
|
if not download_dir:
|
||||||
@ -178,8 +173,8 @@ def archive_files(arc_name, filepaths, message=None, rotate=10):
|
|||||||
|
|
||||||
from deluge.configmanager import get_config_dir
|
from deluge.configmanager import get_config_dir
|
||||||
|
|
||||||
# Set archive compression to lzma with bz2 fallback.
|
# Set archive compression to lzma
|
||||||
arc_comp = 'xz' if not PY2 else 'bz2'
|
arc_comp = 'xz'
|
||||||
|
|
||||||
archive_dir = os.path.join(get_config_dir(), 'archive')
|
archive_dir = os.path.join(get_config_dir(), 'archive')
|
||||||
timestamp = datetime.now().replace(microsecond=0).isoformat().replace(':', '-')
|
timestamp = datetime.now().replace(microsecond=0).isoformat().replace(':', '-')
|
||||||
@ -275,7 +270,7 @@ def get_os_version():
|
|||||||
os_version = list(platform.mac_ver())
|
os_version = list(platform.mac_ver())
|
||||||
os_version[1] = '' # versioninfo always empty.
|
os_version[1] = '' # versioninfo always empty.
|
||||||
elif distro:
|
elif distro:
|
||||||
os_version = distro.linux_distribution()
|
os_version = (distro.name(), distro.version(), distro.codename())
|
||||||
else:
|
else:
|
||||||
os_version = (platform.release(),)
|
os_version = (platform.release(),)
|
||||||
|
|
||||||
@ -441,22 +436,22 @@ def fsize(fsize_b, precision=1, shortform=False):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if fsize_b >= 1024 ** 4:
|
if fsize_b >= 1024**4:
|
||||||
return '%.*f %s' % (
|
return '%.*f %s' % (
|
||||||
precision,
|
precision,
|
||||||
fsize_b / 1024 ** 4,
|
fsize_b / 1024**4,
|
||||||
tib_txt_short if shortform else tib_txt,
|
tib_txt_short if shortform else tib_txt,
|
||||||
)
|
)
|
||||||
elif fsize_b >= 1024 ** 3:
|
elif fsize_b >= 1024**3:
|
||||||
return '%.*f %s' % (
|
return '%.*f %s' % (
|
||||||
precision,
|
precision,
|
||||||
fsize_b / 1024 ** 3,
|
fsize_b / 1024**3,
|
||||||
gib_txt_short if shortform else gib_txt,
|
gib_txt_short if shortform else gib_txt,
|
||||||
)
|
)
|
||||||
elif fsize_b >= 1024 ** 2:
|
elif fsize_b >= 1024**2:
|
||||||
return '%.*f %s' % (
|
return '%.*f %s' % (
|
||||||
precision,
|
precision,
|
||||||
fsize_b / 1024 ** 2,
|
fsize_b / 1024**2,
|
||||||
mib_txt_short if shortform else mib_txt,
|
mib_txt_short if shortform else mib_txt,
|
||||||
)
|
)
|
||||||
elif fsize_b >= 1024:
|
elif fsize_b >= 1024:
|
||||||
@ -508,28 +503,28 @@ def fspeed(bps, precision=1, shortform=False):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if bps < 1024 ** 2:
|
if bps < 1024**2:
|
||||||
return '%.*f %s' % (
|
return '%.*f %s' % (
|
||||||
precision,
|
precision,
|
||||||
bps / 1024,
|
bps / 1024,
|
||||||
_('K/s') if shortform else _('KiB/s'),
|
_('K/s') if shortform else _('KiB/s'),
|
||||||
)
|
)
|
||||||
elif bps < 1024 ** 3:
|
elif bps < 1024**3:
|
||||||
return '%.*f %s' % (
|
return '%.*f %s' % (
|
||||||
precision,
|
precision,
|
||||||
bps / 1024 ** 2,
|
bps / 1024**2,
|
||||||
_('M/s') if shortform else _('MiB/s'),
|
_('M/s') if shortform else _('MiB/s'),
|
||||||
)
|
)
|
||||||
elif bps < 1024 ** 4:
|
elif bps < 1024**4:
|
||||||
return '%.*f %s' % (
|
return '%.*f %s' % (
|
||||||
precision,
|
precision,
|
||||||
bps / 1024 ** 3,
|
bps / 1024**3,
|
||||||
_('G/s') if shortform else _('GiB/s'),
|
_('G/s') if shortform else _('GiB/s'),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return '%.*f %s' % (
|
return '%.*f %s' % (
|
||||||
precision,
|
precision,
|
||||||
bps / 1024 ** 4,
|
bps / 1024**4,
|
||||||
_('T/s') if shortform else _('TiB/s'),
|
_('T/s') if shortform else _('TiB/s'),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -552,9 +547,9 @@ def fpeer(num_peers, total_peers):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
if total_peers > -1:
|
if total_peers > -1:
|
||||||
return '{:d} ({:d})'.format(num_peers, total_peers)
|
return f'{num_peers:d} ({total_peers:d})'
|
||||||
else:
|
else:
|
||||||
return '{:d}'.format(num_peers)
|
return f'{num_peers:d}'
|
||||||
|
|
||||||
|
|
||||||
def ftime(secs):
|
def ftime(secs):
|
||||||
@ -580,17 +575,17 @@ def ftime(secs):
|
|||||||
if secs <= 0:
|
if secs <= 0:
|
||||||
time_str = ''
|
time_str = ''
|
||||||
elif secs < 60:
|
elif secs < 60:
|
||||||
time_str = '{}s'.format(secs)
|
time_str = f'{secs}s'
|
||||||
elif secs < 3600:
|
elif secs < 3600:
|
||||||
time_str = '{}m {}s'.format(secs // 60, secs % 60)
|
time_str = f'{secs // 60}m {secs % 60}s'
|
||||||
elif secs < 86400:
|
elif secs < 86400:
|
||||||
time_str = '{}h {}m'.format(secs // 3600, secs // 60 % 60)
|
time_str = f'{secs // 3600}h {secs // 60 % 60}m'
|
||||||
elif secs < 604800:
|
elif secs < 604800:
|
||||||
time_str = '{}d {}h'.format(secs // 86400, secs // 3600 % 24)
|
time_str = f'{secs // 86400}d {secs // 3600 % 24}h'
|
||||||
elif secs < 31449600:
|
elif secs < 31449600:
|
||||||
time_str = '{}w {}d'.format(secs // 604800, secs // 86400 % 7)
|
time_str = f'{secs // 604800}w {secs // 86400 % 7}d'
|
||||||
else:
|
else:
|
||||||
time_str = '{}y {}w'.format(secs // 31449600, secs // 604800 % 52)
|
time_str = f'{secs // 31449600}y {secs // 604800 % 52}w'
|
||||||
|
|
||||||
return time_str
|
return time_str
|
||||||
|
|
||||||
@ -644,17 +639,17 @@ def tokenize(text):
|
|||||||
|
|
||||||
size_units = [
|
size_units = [
|
||||||
{'prefix': 'b', 'divider': 1, 'singular': 'byte', 'plural': 'bytes'},
|
{'prefix': 'b', 'divider': 1, 'singular': 'byte', 'plural': 'bytes'},
|
||||||
{'prefix': 'KiB', 'divider': 1024 ** 1},
|
{'prefix': 'KiB', 'divider': 1024**1},
|
||||||
{'prefix': 'MiB', 'divider': 1024 ** 2},
|
{'prefix': 'MiB', 'divider': 1024**2},
|
||||||
{'prefix': 'GiB', 'divider': 1024 ** 3},
|
{'prefix': 'GiB', 'divider': 1024**3},
|
||||||
{'prefix': 'TiB', 'divider': 1024 ** 4},
|
{'prefix': 'TiB', 'divider': 1024**4},
|
||||||
{'prefix': 'PiB', 'divider': 1024 ** 5},
|
{'prefix': 'PiB', 'divider': 1024**5},
|
||||||
{'prefix': 'KB', 'divider': 1000 ** 1},
|
{'prefix': 'KB', 'divider': 1000**1},
|
||||||
{'prefix': 'MB', 'divider': 1000 ** 2},
|
{'prefix': 'MB', 'divider': 1000**2},
|
||||||
{'prefix': 'GB', 'divider': 1000 ** 3},
|
{'prefix': 'GB', 'divider': 1000**3},
|
||||||
{'prefix': 'TB', 'divider': 1000 ** 4},
|
{'prefix': 'TB', 'divider': 1000**4},
|
||||||
{'prefix': 'PB', 'divider': 1000 ** 5},
|
{'prefix': 'PB', 'divider': 1000**5},
|
||||||
{'prefix': 'm', 'divider': 1000 ** 2},
|
{'prefix': 'm', 'divider': 1000**2},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -712,6 +707,9 @@ def is_url(url):
|
|||||||
True
|
True
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
if not url:
|
||||||
|
return False
|
||||||
|
|
||||||
return url.partition('://')[0] in ('http', 'https', 'ftp', 'udp')
|
return url.partition('://')[0] in ('http', 'https', 'ftp', 'udp')
|
||||||
|
|
||||||
|
|
||||||
@ -726,6 +724,9 @@ def is_infohash(infohash):
|
|||||||
bool: True if valid infohash, False otherwise.
|
bool: True if valid infohash, False otherwise.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
if not infohash:
|
||||||
|
return False
|
||||||
|
|
||||||
return len(infohash) == 40 and infohash.isalnum()
|
return len(infohash) == 40 and infohash.isalnum()
|
||||||
|
|
||||||
|
|
||||||
@ -733,6 +734,8 @@ MAGNET_SCHEME = 'magnet:?'
|
|||||||
XT_BTIH_PARAM = 'xt=urn:btih:'
|
XT_BTIH_PARAM = 'xt=urn:btih:'
|
||||||
DN_PARAM = 'dn='
|
DN_PARAM = 'dn='
|
||||||
TR_PARAM = 'tr='
|
TR_PARAM = 'tr='
|
||||||
|
TR_TIER_PARAM = 'tr.'
|
||||||
|
TR_TIER_REGEX = re.compile(r'^tr.(\d+)=(\S+)')
|
||||||
|
|
||||||
|
|
||||||
def is_magnet(uri):
|
def is_magnet(uri):
|
||||||
@ -775,8 +778,6 @@ def get_magnet_info(uri):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
tr0_param = 'tr.'
|
|
||||||
tr0_param_regex = re.compile(r'^tr.(\d+)=(\S+)')
|
|
||||||
if not uri.startswith(MAGNET_SCHEME):
|
if not uri.startswith(MAGNET_SCHEME):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@ -804,12 +805,14 @@ def get_magnet_info(uri):
|
|||||||
tracker = unquote_plus(param[len(TR_PARAM) :])
|
tracker = unquote_plus(param[len(TR_PARAM) :])
|
||||||
trackers[tracker] = tier
|
trackers[tracker] = tier
|
||||||
tier += 1
|
tier += 1
|
||||||
elif param.startswith(tr0_param):
|
elif param.startswith(TR_TIER_PARAM):
|
||||||
try:
|
tracker_match = re.match(TR_TIER_REGEX, param)
|
||||||
tier, tracker = re.match(tr0_param_regex, param).groups()
|
if not tracker_match:
|
||||||
trackers[tracker] = tier
|
continue
|
||||||
except AttributeError:
|
|
||||||
pass
|
tier, tracker = tracker_match.groups()
|
||||||
|
tracker = unquote_plus(tracker)
|
||||||
|
trackers[tracker] = int(tier)
|
||||||
|
|
||||||
if info_hash:
|
if info_hash:
|
||||||
if not name:
|
if not name:
|
||||||
@ -904,6 +907,29 @@ def free_space(path):
|
|||||||
return disk_data.f_bavail * block_size
|
return disk_data.f_bavail * block_size
|
||||||
|
|
||||||
|
|
||||||
|
def is_interface(interface):
|
||||||
|
"""Check if interface is a valid IP or network adapter.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
interface (str): The IP or interface name to test.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: Whether interface is valid is not.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
Windows:
|
||||||
|
>>> is_interface('{7A30AE62-23ZA-3744-Z844-A5B042524871}')
|
||||||
|
>>> is_interface('127.0.0.1')
|
||||||
|
True
|
||||||
|
Linux:
|
||||||
|
>>> is_interface('lo')
|
||||||
|
>>> is_interface('127.0.0.1')
|
||||||
|
True
|
||||||
|
|
||||||
|
"""
|
||||||
|
return is_ip(interface) or is_interface_name(interface)
|
||||||
|
|
||||||
|
|
||||||
def is_ip(ip):
|
def is_ip(ip):
|
||||||
"""A test to see if 'ip' is a valid IPv4 or IPv6 address.
|
"""A test to see if 'ip' is a valid IPv4 or IPv6 address.
|
||||||
|
|
||||||
@ -939,15 +965,12 @@ def is_ipv4(ip):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import socket
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if windows_check():
|
socket.inet_pton(socket.AF_INET, ip)
|
||||||
return socket.inet_aton(ip)
|
except OSError:
|
||||||
else:
|
|
||||||
return socket.inet_pton(socket.AF_INET, ip)
|
|
||||||
except socket.error:
|
|
||||||
return False
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def is_ipv6(ip):
|
def is_ipv6(ip):
|
||||||
@ -966,23 +989,51 @@ def is_ipv6(ip):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import ipaddress
|
socket.inet_pton(socket.AF_INET6, ip)
|
||||||
except ImportError:
|
except OSError:
|
||||||
import socket
|
return False
|
||||||
|
|
||||||
try:
|
|
||||||
return socket.inet_pton(socket.AF_INET6, ip)
|
|
||||||
except (socket.error, AttributeError):
|
|
||||||
if windows_check():
|
|
||||||
log.warning('Unable to verify IPv6 Address on Windows.')
|
|
||||||
return True
|
|
||||||
else:
|
else:
|
||||||
try:
|
return True
|
||||||
return ipaddress.IPv6Address(decode_bytes(ip))
|
|
||||||
except ipaddress.AddressValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
def is_interface_name(name):
|
||||||
|
"""Returns True if an interface name exists.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): The Interface to test. eg. eth0 linux. GUID on Windows.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: Whether name is valid or not.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
>>> is_interface_name("eth0")
|
||||||
|
True
|
||||||
|
>>> is_interface_name("{7A30AE62-23ZA-3744-Z844-A5B042524871}")
|
||||||
|
True
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not windows_check():
|
||||||
|
try:
|
||||||
|
socket.if_nametoindex(name)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if ifaddr:
|
||||||
|
try:
|
||||||
|
adapters = ifaddr.get_adapters()
|
||||||
|
except OSError:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return any([name == a.name for a in adapters])
|
||||||
|
|
||||||
|
if windows_check():
|
||||||
|
regex = '^{[0-9A-Z]{8}-([0-9A-Z]{4}-){3}[0-9A-Z]{12}}$'
|
||||||
|
return bool(re.search(regex, str(name)))
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def decode_bytes(byte_str, encoding='utf8'):
|
def decode_bytes(byte_str, encoding='utf8'):
|
||||||
@ -1060,7 +1111,7 @@ def utf8_encode_structure(data):
|
|||||||
|
|
||||||
|
|
||||||
@functools.total_ordering
|
@functools.total_ordering
|
||||||
class VersionSplit(object):
|
class VersionSplit:
|
||||||
"""
|
"""
|
||||||
Used for comparing version numbers.
|
Used for comparing version numbers.
|
||||||
|
|
||||||
@ -1239,11 +1290,7 @@ def set_env_variable(name, value):
|
|||||||
http://sourceforge.net/p/gramps/code/HEAD/tree/branches/maintenance/gramps32/src/TransUtils.py
|
http://sourceforge.net/p/gramps/code/HEAD/tree/branches/maintenance/gramps32/src/TransUtils.py
|
||||||
"""
|
"""
|
||||||
# Update Python's copy of the environment variables
|
# Update Python's copy of the environment variables
|
||||||
try:
|
os.environ[name] = value
|
||||||
os.environ[name] = value
|
|
||||||
except UnicodeEncodeError:
|
|
||||||
# Python 2
|
|
||||||
os.environ[name] = value.encode('utf8')
|
|
||||||
|
|
||||||
if windows_check():
|
if windows_check():
|
||||||
from ctypes import cdll, windll
|
from ctypes import cdll, windll
|
||||||
@ -1262,56 +1309,13 @@ def set_env_variable(name, value):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Update the copy maintained by msvcrt (used by gtk+ runtime)
|
# Update the copy maintained by msvcrt (used by gtk+ runtime)
|
||||||
result = cdll.msvcrt._wputenv('%s=%s' % (name, value))
|
result = cdll.msvcrt._wputenv(f'{name}={value}')
|
||||||
if result != 0:
|
if result != 0:
|
||||||
log.info("Failed to set Env Var '%s' (msvcrt._putenv)", name)
|
log.info("Failed to set Env Var '%s' (msvcrt._putenv)", name)
|
||||||
else:
|
else:
|
||||||
log.debug("Set Env Var '%s' to '%s' (msvcrt._putenv)", name, value)
|
log.debug("Set Env Var '%s' to '%s' (msvcrt._putenv)", name, value)
|
||||||
|
|
||||||
|
|
||||||
def unicode_argv():
|
|
||||||
""" Gets sys.argv as list of unicode objects on any platform."""
|
|
||||||
if windows_check():
|
|
||||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
|
||||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
|
||||||
# characters with '?'.
|
|
||||||
from ctypes import POINTER, byref, c_int, cdll, windll
|
|
||||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
|
||||||
|
|
||||||
get_cmd_linew = cdll.kernel32.GetCommandLineW
|
|
||||||
get_cmd_linew.argtypes = []
|
|
||||||
get_cmd_linew.restype = LPCWSTR
|
|
||||||
|
|
||||||
cmdline_to_argvw = windll.shell32.CommandLineToArgvW
|
|
||||||
cmdline_to_argvw.argtypes = [LPCWSTR, POINTER(c_int)]
|
|
||||||
cmdline_to_argvw.restype = POINTER(LPWSTR)
|
|
||||||
|
|
||||||
cmd = get_cmd_linew()
|
|
||||||
argc = c_int(0)
|
|
||||||
argv = cmdline_to_argvw(cmd, byref(argc))
|
|
||||||
if argc.value > 0:
|
|
||||||
# Remove Python executable and commands if present
|
|
||||||
start = argc.value - len(sys.argv)
|
|
||||||
return [argv[i] for i in range(start, argc.value)]
|
|
||||||
else:
|
|
||||||
# On other platforms, we have to find the likely encoding of the args and decode
|
|
||||||
# First check if sys.stdout or stdin have encoding set
|
|
||||||
encoding = getattr(sys.stdout, 'encoding') or getattr(sys.stdin, 'encoding')
|
|
||||||
# If that fails, check what the locale is set to
|
|
||||||
encoding = encoding or locale.getpreferredencoding()
|
|
||||||
# As a last resort, just default to utf-8
|
|
||||||
encoding = encoding or 'utf-8'
|
|
||||||
|
|
||||||
arg_list = []
|
|
||||||
for arg in sys.argv:
|
|
||||||
try:
|
|
||||||
arg_list.append(arg.decode(encoding))
|
|
||||||
except AttributeError:
|
|
||||||
arg_list.append(arg)
|
|
||||||
|
|
||||||
return arg_list
|
|
||||||
|
|
||||||
|
|
||||||
def run_profiled(func, *args, **kwargs):
|
def run_profiled(func, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Profile a function with cProfile
|
Profile a function with cProfile
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2007-2010 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2007-2010 Andrew Resch <andrewresch@gmail.com>
|
||||||
#
|
#
|
||||||
@ -7,13 +6,10 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import traceback
|
import traceback
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from six import string_types
|
|
||||||
from twisted.internet import reactor
|
from twisted.internet import reactor
|
||||||
from twisted.internet.defer import DeferredList, fail, maybeDeferred, succeed
|
from twisted.internet.defer import DeferredList, fail, maybeDeferred, succeed
|
||||||
from twisted.internet.task import LoopingCall, deferLater
|
from twisted.internet.task import LoopingCall, deferLater
|
||||||
@ -27,13 +23,13 @@ class ComponentAlreadyRegistered(Exception):
|
|||||||
|
|
||||||
class ComponentException(Exception):
|
class ComponentException(Exception):
|
||||||
def __init__(self, message, tb):
|
def __init__(self, message, tb):
|
||||||
super(ComponentException, self).__init__(message)
|
super().__init__(message)
|
||||||
self.message = message
|
self.message = message
|
||||||
self.tb = tb
|
self.tb = tb
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
s = super(ComponentException, self).__str__()
|
s = super().__str__()
|
||||||
return '%s\n%s' % (s, ''.join(self.tb))
|
return '{}\n{}'.format(s, ''.join(self.tb))
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
if isinstance(other, self.__class__):
|
if isinstance(other, self.__class__):
|
||||||
@ -45,7 +41,7 @@ class ComponentException(Exception):
|
|||||||
return not self.__eq__(other)
|
return not self.__eq__(other)
|
||||||
|
|
||||||
|
|
||||||
class Component(object):
|
class Component:
|
||||||
"""Component objects are singletons managed by the :class:`ComponentRegistry`.
|
"""Component objects are singletons managed by the :class:`ComponentRegistry`.
|
||||||
|
|
||||||
When a new Component object is instantiated, it will be automatically
|
When a new Component object is instantiated, it will be automatically
|
||||||
@ -250,7 +246,7 @@ class Component(object):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ComponentRegistry(object):
|
class ComponentRegistry:
|
||||||
"""The ComponentRegistry holds a list of currently registered :class:`Component` objects.
|
"""The ComponentRegistry holds a list of currently registered :class:`Component` objects.
|
||||||
|
|
||||||
It is used to manage the Components by starting, stopping, pausing and shutting them down.
|
It is used to manage the Components by starting, stopping, pausing and shutting them down.
|
||||||
@ -325,7 +321,7 @@ class ComponentRegistry(object):
|
|||||||
# Start all the components if names is empty
|
# Start all the components if names is empty
|
||||||
if not names:
|
if not names:
|
||||||
names = list(self.components)
|
names = list(self.components)
|
||||||
elif isinstance(names, string_types):
|
elif isinstance(names, str):
|
||||||
names = [names]
|
names = [names]
|
||||||
|
|
||||||
def on_depends_started(result, name):
|
def on_depends_started(result, name):
|
||||||
@ -359,7 +355,7 @@ class ComponentRegistry(object):
|
|||||||
"""
|
"""
|
||||||
if not names:
|
if not names:
|
||||||
names = list(self.components)
|
names = list(self.components)
|
||||||
elif isinstance(names, string_types):
|
elif isinstance(names, str):
|
||||||
names = [names]
|
names = [names]
|
||||||
|
|
||||||
def on_dependents_stopped(result, name):
|
def on_dependents_stopped(result, name):
|
||||||
@ -399,7 +395,7 @@ class ComponentRegistry(object):
|
|||||||
"""
|
"""
|
||||||
if not names:
|
if not names:
|
||||||
names = list(self.components)
|
names = list(self.components)
|
||||||
elif isinstance(names, string_types):
|
elif isinstance(names, str):
|
||||||
names = [names]
|
names = [names]
|
||||||
|
|
||||||
deferreds = []
|
deferreds = []
|
||||||
@ -425,7 +421,7 @@ class ComponentRegistry(object):
|
|||||||
"""
|
"""
|
||||||
if not names:
|
if not names:
|
||||||
names = list(self.components)
|
names = list(self.components)
|
||||||
elif isinstance(names, string_types):
|
elif isinstance(names, str):
|
||||||
names = [names]
|
names = [names]
|
||||||
|
|
||||||
deferreds = []
|
deferreds = []
|
||||||
|
|||||||
178
deluge/config.py
178
deluge/config.py
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com>
|
||||||
#
|
#
|
||||||
@ -39,39 +38,17 @@ this can only be done for the 'config file version' and not for the 'format'
|
|||||||
version as this will be done internally.
|
version as this will be done internally.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import pickle
|
||||||
import shutil
|
import shutil
|
||||||
from codecs import getwriter
|
from codecs import getwriter
|
||||||
from io import open
|
|
||||||
from tempfile import NamedTemporaryFile
|
from tempfile import NamedTemporaryFile
|
||||||
|
|
||||||
import six.moves.cPickle as pickle # noqa: N813
|
|
||||||
|
|
||||||
from deluge.common import JSON_FORMAT, get_default_config_dir
|
from deluge.common import JSON_FORMAT, get_default_config_dir
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
callLater = None # noqa: N816 Necessary for the config tests
|
|
||||||
|
|
||||||
|
|
||||||
def prop(func):
|
|
||||||
"""Function decorator for defining property attributes
|
|
||||||
|
|
||||||
The decorated function is expected to return a dictionary
|
|
||||||
containing one or more of the following pairs:
|
|
||||||
|
|
||||||
fget - function for getting attribute value
|
|
||||||
fset - function for setting attribute value
|
|
||||||
fdel - function for deleting attribute
|
|
||||||
|
|
||||||
This can be conveniently constructed by the locals() builtin
|
|
||||||
function; see:
|
|
||||||
http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/205183
|
|
||||||
"""
|
|
||||||
return property(doc=func.__doc__, **func())
|
|
||||||
|
|
||||||
|
|
||||||
def find_json_objects(text, decoder=json.JSONDecoder()):
|
def find_json_objects(text, decoder=json.JSONDecoder()):
|
||||||
@ -105,7 +82,22 @@ def find_json_objects(text, decoder=json.JSONDecoder()):
|
|||||||
return objects
|
return objects
|
||||||
|
|
||||||
|
|
||||||
class Config(object):
|
def cast_to_existing_type(value, old_value):
|
||||||
|
"""Attempt to convert new value type to match old value type"""
|
||||||
|
types_match = isinstance(old_value, (type(None), type(value)))
|
||||||
|
if value is not None and not types_match:
|
||||||
|
old_type = type(old_value)
|
||||||
|
# Skip convert to bytes since requires knowledge of encoding and value should
|
||||||
|
# be unicode anyway.
|
||||||
|
if old_type is bytes:
|
||||||
|
return value
|
||||||
|
|
||||||
|
return old_type(value)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class Config:
|
||||||
"""This class is used to access/create/modify config files.
|
"""This class is used to access/create/modify config files.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -115,13 +107,23 @@ class Config(object):
|
|||||||
file_version (int): The file format for the default config values when creating
|
file_version (int): The file format for the default config values when creating
|
||||||
a fresh config. This value should be increased whenever a new migration function is
|
a fresh config. This value should be increased whenever a new migration function is
|
||||||
setup to convert old config files. (default: 1)
|
setup to convert old config files. (default: 1)
|
||||||
|
log_mask_funcs (dict): A dict of key:function, used to mask sensitive
|
||||||
|
key values (e.g. passwords) when logging is enabled.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, filename, defaults=None, config_dir=None, file_version=1):
|
def __init__(
|
||||||
|
self,
|
||||||
|
filename,
|
||||||
|
defaults=None,
|
||||||
|
config_dir=None,
|
||||||
|
file_version=1,
|
||||||
|
log_mask_funcs=None,
|
||||||
|
):
|
||||||
self.__config = {}
|
self.__config = {}
|
||||||
self.__set_functions = {}
|
self.__set_functions = {}
|
||||||
self.__change_callbacks = []
|
self.__change_callbacks = []
|
||||||
|
self.__log_mask_funcs = log_mask_funcs if log_mask_funcs else {}
|
||||||
|
|
||||||
# These hold the version numbers and they will be set when loaded
|
# These hold the version numbers and they will be set when loaded
|
||||||
self.__version = {'format': 1, 'file': file_version}
|
self.__version = {'format': 1, 'file': file_version}
|
||||||
@ -132,7 +134,7 @@ class Config(object):
|
|||||||
|
|
||||||
if defaults:
|
if defaults:
|
||||||
for key, value in defaults.items():
|
for key, value in defaults.items():
|
||||||
self.set_item(key, value)
|
self.set_item(key, value, default=True)
|
||||||
|
|
||||||
# Load the config from file in the config_dir
|
# Load the config from file in the config_dir
|
||||||
if config_dir:
|
if config_dir:
|
||||||
@ -142,6 +144,12 @@ class Config(object):
|
|||||||
|
|
||||||
self.load()
|
self.load()
|
||||||
|
|
||||||
|
def callLater(self, period, func, *args, **kwargs): # noqa: N802 ignore camelCase
|
||||||
|
"""Wrapper around reactor.callLater for test purpose."""
|
||||||
|
from twisted.internet import reactor
|
||||||
|
|
||||||
|
return reactor.callLater(period, func, *args, **kwargs)
|
||||||
|
|
||||||
def __contains__(self, item):
|
def __contains__(self, item):
|
||||||
return item in self.__config
|
return item in self.__config
|
||||||
|
|
||||||
@ -150,7 +158,7 @@ class Config(object):
|
|||||||
|
|
||||||
return self.set_item(key, value)
|
return self.set_item(key, value)
|
||||||
|
|
||||||
def set_item(self, key, value):
|
def set_item(self, key, value, default=False):
|
||||||
"""Sets item 'key' to 'value' in the config dictionary.
|
"""Sets item 'key' to 'value' in the config dictionary.
|
||||||
|
|
||||||
Does not allow changing the item's type unless it is None.
|
Does not allow changing the item's type unless it is None.
|
||||||
@ -162,6 +170,8 @@ class Config(object):
|
|||||||
key (str): Item to change to change.
|
key (str): Item to change to change.
|
||||||
value (any): The value to change item to, must be same type as what is
|
value (any): The value to change item to, must be same type as what is
|
||||||
currently in the config.
|
currently in the config.
|
||||||
|
default (optional, bool): When setting a default value skip func or save
|
||||||
|
callbacks.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: Raised when the type of value is not the same as what is
|
ValueError: Raised when the type of value is not the same as what is
|
||||||
@ -174,61 +184,54 @@ class Config(object):
|
|||||||
5
|
5
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if key not in self.__config:
|
if isinstance(value, bytes):
|
||||||
self.__config[key] = value
|
value = value.decode()
|
||||||
log.debug('Setting key "%s" to: %s (of type: %s)', key, value, type(value))
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.__config[key] == value:
|
if key in self.__config:
|
||||||
return
|
|
||||||
|
|
||||||
# Change the value type if it is not None and does not match.
|
|
||||||
type_match = isinstance(self.__config[key], (type(None), type(value)))
|
|
||||||
if value is not None and not type_match:
|
|
||||||
try:
|
try:
|
||||||
oldtype = type(self.__config[key])
|
value = cast_to_existing_type(value, self.__config[key])
|
||||||
# Don't convert to bytes as requires encoding and value will
|
|
||||||
# be decoded anyway.
|
|
||||||
if oldtype is not bytes:
|
|
||||||
value = oldtype(value)
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
log.warning('Value Type "%s" invalid for key: %s', type(value), key)
|
log.warning('Value Type "%s" invalid for key: %s', type(value), key)
|
||||||
raise
|
raise
|
||||||
|
else:
|
||||||
|
if self.__config[key] == value:
|
||||||
|
return
|
||||||
|
|
||||||
if isinstance(value, bytes):
|
if log.isEnabledFor(logging.DEBUG):
|
||||||
value = value.decode('utf8')
|
if key in self.__log_mask_funcs:
|
||||||
|
value = self.__log_mask_funcs[key](value)
|
||||||
log.debug('Setting key "%s" to: %s (of type: %s)', key, value, type(value))
|
log.debug(
|
||||||
|
'Setting key "%s" to: %s (of type: %s)',
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
type(value),
|
||||||
|
)
|
||||||
self.__config[key] = value
|
self.__config[key] = value
|
||||||
|
|
||||||
global callLater
|
# Skip save or func callbacks if setting default value for keys
|
||||||
if callLater is None:
|
if default:
|
||||||
# Must import here and not at the top or it will throw ReactorAlreadyInstalledError
|
return
|
||||||
from twisted.internet.reactor import ( # pylint: disable=redefined-outer-name
|
|
||||||
callLater,
|
|
||||||
)
|
|
||||||
# Run the set_function for this key if any
|
# Run the set_function for this key if any
|
||||||
try:
|
for func in self.__set_functions.get(key, []):
|
||||||
for func in self.__set_functions[key]:
|
self.callLater(0, func, key, value)
|
||||||
callLater(0, func, key, value)
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
try:
|
try:
|
||||||
|
|
||||||
def do_change_callbacks(key, value):
|
def do_change_callbacks(key, value):
|
||||||
for func in self.__change_callbacks:
|
for func in self.__change_callbacks:
|
||||||
func(key, value)
|
func(key, value)
|
||||||
|
|
||||||
callLater(0, do_change_callbacks, key, value)
|
self.callLater(0, do_change_callbacks, key, value)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# We set the save_timer for 5 seconds if not already set
|
# We set the save_timer for 5 seconds if not already set
|
||||||
if not self._save_timer or not self._save_timer.active():
|
if not self._save_timer or not self._save_timer.active():
|
||||||
self._save_timer = callLater(5, self.save)
|
self._save_timer = self.callLater(5, self.save)
|
||||||
|
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
"""See get_item """
|
"""See get_item"""
|
||||||
return self.get_item(key)
|
return self.get_item(key)
|
||||||
|
|
||||||
def get_item(self, key):
|
def get_item(self, key):
|
||||||
@ -301,16 +304,9 @@ class Config(object):
|
|||||||
|
|
||||||
del self.__config[key]
|
del self.__config[key]
|
||||||
|
|
||||||
global callLater
|
|
||||||
if callLater is None:
|
|
||||||
# Must import here and not at the top or it will throw ReactorAlreadyInstalledError
|
|
||||||
from twisted.internet.reactor import ( # pylint: disable=redefined-outer-name
|
|
||||||
callLater,
|
|
||||||
)
|
|
||||||
|
|
||||||
# We set the save_timer for 5 seconds if not already set
|
# We set the save_timer for 5 seconds if not already set
|
||||||
if not self._save_timer or not self._save_timer.active():
|
if not self._save_timer or not self._save_timer.active():
|
||||||
self._save_timer = callLater(5, self.save)
|
self._save_timer = self.callLater(5, self.save)
|
||||||
|
|
||||||
def register_change_callback(self, callback):
|
def register_change_callback(self, callback):
|
||||||
"""Registers a callback function for any changed value.
|
"""Registers a callback function for any changed value.
|
||||||
@ -356,7 +352,6 @@ class Config(object):
|
|||||||
# Run the function now if apply_now is set
|
# Run the function now if apply_now is set
|
||||||
if apply_now:
|
if apply_now:
|
||||||
function(key, self.__config[key])
|
function(key, self.__config[key])
|
||||||
return
|
|
||||||
|
|
||||||
def apply_all(self):
|
def apply_all(self):
|
||||||
"""Calls all set functions.
|
"""Calls all set functions.
|
||||||
@ -399,9 +394,9 @@ class Config(object):
|
|||||||
filename = self.__config_file
|
filename = self.__config_file
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(filename, 'r', encoding='utf8') as _file:
|
with open(filename, encoding='utf8') as _file:
|
||||||
data = _file.read()
|
data = _file.read()
|
||||||
except IOError as ex:
|
except OSError as ex:
|
||||||
log.warning('Unable to open config file %s: %s', filename, ex)
|
log.warning('Unable to open config file %s: %s', filename, ex)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -431,12 +426,24 @@ class Config(object):
|
|||||||
log.exception(ex)
|
log.exception(ex)
|
||||||
log.warning('Unable to load config file: %s', filename)
|
log.warning('Unable to load config file: %s', filename)
|
||||||
|
|
||||||
|
if not log.isEnabledFor(logging.DEBUG):
|
||||||
|
return
|
||||||
|
|
||||||
|
config = self.__config
|
||||||
|
if self.__log_mask_funcs:
|
||||||
|
config = {
|
||||||
|
key: self.__log_mask_funcs[key](config[key])
|
||||||
|
if key in self.__log_mask_funcs
|
||||||
|
else config[key]
|
||||||
|
for key in config
|
||||||
|
}
|
||||||
|
|
||||||
log.debug(
|
log.debug(
|
||||||
'Config %s version: %s.%s loaded: %s',
|
'Config %s version: %s.%s loaded: %s',
|
||||||
filename,
|
filename,
|
||||||
self.__version['format'],
|
self.__version['format'],
|
||||||
self.__version['file'],
|
self.__version['file'],
|
||||||
self.__config,
|
config,
|
||||||
)
|
)
|
||||||
|
|
||||||
def save(self, filename=None):
|
def save(self, filename=None):
|
||||||
@ -454,7 +461,7 @@ class Config(object):
|
|||||||
# Check to see if the current config differs from the one on disk
|
# Check to see if the current config differs from the one on disk
|
||||||
# We will only write a new config file if there is a difference
|
# We will only write a new config file if there is a difference
|
||||||
try:
|
try:
|
||||||
with open(filename, 'r', encoding='utf8') as _file:
|
with open(filename, encoding='utf8') as _file:
|
||||||
data = _file.read()
|
data = _file.read()
|
||||||
objects = find_json_objects(data)
|
objects = find_json_objects(data)
|
||||||
start, end = objects[0]
|
start, end = objects[0]
|
||||||
@ -466,7 +473,7 @@ class Config(object):
|
|||||||
if self._save_timer and self._save_timer.active():
|
if self._save_timer and self._save_timer.active():
|
||||||
self._save_timer.cancel()
|
self._save_timer.cancel()
|
||||||
return True
|
return True
|
||||||
except (IOError, IndexError) as ex:
|
except (OSError, IndexError) as ex:
|
||||||
log.warning('Unable to open config file: %s because: %s', filename, ex)
|
log.warning('Unable to open config file: %s because: %s', filename, ex)
|
||||||
|
|
||||||
# Save the new config and make sure it's written to disk
|
# Save the new config and make sure it's written to disk
|
||||||
@ -480,7 +487,7 @@ class Config(object):
|
|||||||
json.dump(self.__config, getwriter('utf8')(_file), **JSON_FORMAT)
|
json.dump(self.__config, getwriter('utf8')(_file), **JSON_FORMAT)
|
||||||
_file.flush()
|
_file.flush()
|
||||||
os.fsync(_file.fileno())
|
os.fsync(_file.fileno())
|
||||||
except IOError as ex:
|
except OSError as ex:
|
||||||
log.error('Error writing new config file: %s', ex)
|
log.error('Error writing new config file: %s', ex)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -491,7 +498,7 @@ class Config(object):
|
|||||||
try:
|
try:
|
||||||
log.debug('Backing up old config file to %s.bak', filename)
|
log.debug('Backing up old config file to %s.bak', filename)
|
||||||
shutil.move(filename, filename + '.bak')
|
shutil.move(filename, filename + '.bak')
|
||||||
except IOError as ex:
|
except OSError as ex:
|
||||||
log.warning('Unable to backup old config: %s', ex)
|
log.warning('Unable to backup old config: %s', ex)
|
||||||
|
|
||||||
# The new config file has been written successfully, so let's move it over
|
# The new config file has been written successfully, so let's move it over
|
||||||
@ -499,7 +506,7 @@ class Config(object):
|
|||||||
try:
|
try:
|
||||||
log.debug('Moving new config file %s to %s', filename_tmp, filename)
|
log.debug('Moving new config file %s to %s', filename_tmp, filename)
|
||||||
shutil.move(filename_tmp, filename)
|
shutil.move(filename_tmp, filename)
|
||||||
except IOError as ex:
|
except OSError as ex:
|
||||||
log.error('Error moving new config file: %s', ex)
|
log.error('Error moving new config file: %s', ex)
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
@ -551,14 +558,11 @@ class Config(object):
|
|||||||
def config_file(self):
|
def config_file(self):
|
||||||
return self.__config_file
|
return self.__config_file
|
||||||
|
|
||||||
@prop
|
@property
|
||||||
def config(): # pylint: disable=no-method-argument
|
def config(self):
|
||||||
"""The config dictionary"""
|
"""The config dictionary"""
|
||||||
|
return self.__config
|
||||||
|
|
||||||
def fget(self):
|
@config.deleter
|
||||||
return self.__config
|
def config(self):
|
||||||
|
return self.save()
|
||||||
def fdel(self):
|
|
||||||
return self.save()
|
|
||||||
|
|
||||||
return locals()
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
|
||||||
#
|
#
|
||||||
@ -7,8 +6,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@ -19,7 +16,7 @@ from deluge.config import Config
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class _ConfigManager(object):
|
class _ConfigManager:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
log.debug('ConfigManager started..')
|
log.debug('ConfigManager started..')
|
||||||
self.config_files = {}
|
self.config_files = {}
|
||||||
|
|||||||
192
deluge/conftest.py
Normal file
192
deluge/conftest.py
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
#
|
||||||
|
# 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 tempfile
|
||||||
|
import warnings
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import pytest_twisted
|
||||||
|
from twisted.internet import reactor
|
||||||
|
from twisted.internet.defer import Deferred, maybeDeferred
|
||||||
|
from twisted.internet.error import CannotListenError
|
||||||
|
from twisted.python.failure import Failure
|
||||||
|
|
||||||
|
import deluge.component as _component
|
||||||
|
import deluge.configmanager
|
||||||
|
from deluge.common import get_localhost_auth
|
||||||
|
from deluge.tests import common
|
||||||
|
from deluge.ui.client import client as _client
|
||||||
|
|
||||||
|
DEFAULT_LISTEN_PORT = 58900
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def listen_port(request):
|
||||||
|
if request and 'daemon' in request.fixturenames:
|
||||||
|
try:
|
||||||
|
return request.getfixturevalue('daemon').listen_port
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return DEFAULT_LISTEN_PORT
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_callback():
|
||||||
|
"""Returns a `Mock` object which can be registered as a callback to test against.
|
||||||
|
|
||||||
|
If callback was not called within `timeout` seconds, it will raise a TimeoutError.
|
||||||
|
The returned Mock instance will have a `deferred` attribute which will complete when the callback has been called.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def reset():
|
||||||
|
if mock.called:
|
||||||
|
original_reset_mock()
|
||||||
|
deferred = Deferred()
|
||||||
|
deferred.addTimeout(0.5, reactor)
|
||||||
|
mock.side_effect = lambda *args, **kw: deferred.callback((args, kw))
|
||||||
|
mock.deferred = deferred
|
||||||
|
|
||||||
|
mock = Mock()
|
||||||
|
original_reset_mock = mock.reset_mock
|
||||||
|
mock.reset_mock = reset
|
||||||
|
mock.reset_mock()
|
||||||
|
return mock
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def config_dir(tmp_path):
|
||||||
|
deluge.configmanager.set_config_dir(tmp_path)
|
||||||
|
yield tmp_path
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_twisted.async_yield_fixture()
|
||||||
|
async def client(request, config_dir, monkeypatch, listen_port):
|
||||||
|
# monkeypatch.setattr(
|
||||||
|
# _client, 'connect', functools.partial(_client.connect, port=listen_port)
|
||||||
|
# )
|
||||||
|
try:
|
||||||
|
username, password = get_localhost_auth()
|
||||||
|
except Exception:
|
||||||
|
username, password = '', ''
|
||||||
|
await _client.connect(
|
||||||
|
'localhost',
|
||||||
|
port=listen_port,
|
||||||
|
username=username,
|
||||||
|
password=password,
|
||||||
|
)
|
||||||
|
yield _client
|
||||||
|
if _client.connected():
|
||||||
|
await _client.disconnect()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_twisted.async_yield_fixture
|
||||||
|
async def daemon(request, config_dir):
|
||||||
|
listen_port = DEFAULT_LISTEN_PORT
|
||||||
|
logfile = f'daemon_{request.node.name}.log'
|
||||||
|
if hasattr(request.cls, 'daemon_custom_script'):
|
||||||
|
custom_script = request.cls.daemon_custom_script
|
||||||
|
else:
|
||||||
|
custom_script = ''
|
||||||
|
|
||||||
|
for dummy in range(10):
|
||||||
|
try:
|
||||||
|
d, daemon = common.start_core(
|
||||||
|
listen_port=listen_port,
|
||||||
|
logfile=logfile,
|
||||||
|
timeout=5,
|
||||||
|
timeout_msg='Timeout!',
|
||||||
|
custom_script=custom_script,
|
||||||
|
print_stdout=True,
|
||||||
|
print_stderr=True,
|
||||||
|
config_directory=config_dir,
|
||||||
|
)
|
||||||
|
await d
|
||||||
|
except CannotListenError as ex:
|
||||||
|
exception_error = ex
|
||||||
|
listen_port += 1
|
||||||
|
except (KeyboardInterrupt, SystemExit):
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise exception_error
|
||||||
|
daemon.listen_port = listen_port
|
||||||
|
yield daemon
|
||||||
|
await daemon.kill()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def common_fixture(config_dir, request, monkeypatch, listen_port):
|
||||||
|
"""Adds some instance attributes to test classes for backwards compatibility with old testing."""
|
||||||
|
|
||||||
|
def fail(self, reason):
|
||||||
|
if isinstance(reason, Failure):
|
||||||
|
reason = reason.value
|
||||||
|
return pytest.fail(str(reason))
|
||||||
|
|
||||||
|
if request.instance:
|
||||||
|
request.instance.patch = monkeypatch.setattr
|
||||||
|
request.instance.config_dir = config_dir
|
||||||
|
request.instance.listen_port = listen_port
|
||||||
|
request.instance.id = lambda: request.node.name
|
||||||
|
request.cls.fail = fail
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_twisted.async_yield_fixture(scope='function')
|
||||||
|
async def component(request):
|
||||||
|
"""Verify component registry is clean, and clean up after test."""
|
||||||
|
if len(_component._ComponentRegistry.components) != 0:
|
||||||
|
warnings.warn(
|
||||||
|
'The component._ComponentRegistry.components is not empty on test setup.\n'
|
||||||
|
'This is probably caused by another test that did not clean up after finishing!: %s'
|
||||||
|
% _component._ComponentRegistry.components
|
||||||
|
)
|
||||||
|
|
||||||
|
yield _component
|
||||||
|
|
||||||
|
await _component.shutdown()
|
||||||
|
_component._ComponentRegistry.components.clear()
|
||||||
|
_component._ComponentRegistry.dependents.clear()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_twisted.async_yield_fixture(scope='function')
|
||||||
|
async def base_fixture(common_fixture, component, request):
|
||||||
|
"""This fixture is autoused on all tests that subclass BaseTestCase"""
|
||||||
|
self = request.instance
|
||||||
|
|
||||||
|
if hasattr(self, 'set_up'):
|
||||||
|
try:
|
||||||
|
await maybeDeferred(self.set_up)
|
||||||
|
except Exception as exc:
|
||||||
|
warnings.warn('Error caught in test setup!\n%s' % exc)
|
||||||
|
pytest.fail('Error caught in test setup!\n%s' % exc)
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
if hasattr(self, 'tear_down'):
|
||||||
|
try:
|
||||||
|
await maybeDeferred(self.tear_down)
|
||||||
|
except Exception as exc:
|
||||||
|
pytest.fail('Error caught in test teardown!\n%s' % exc)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures('base_fixture')
|
||||||
|
class BaseTestCase:
|
||||||
|
"""This is the base class that should be used for all test classes
|
||||||
|
that create classes that inherit from deluge.component.Component. It
|
||||||
|
ensures that the component registry has been cleaned up when tests
|
||||||
|
have finished.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_mkstemp(tmp_path):
|
||||||
|
"""Return known tempfile location to verify file deleted"""
|
||||||
|
tmp_file = tempfile.mkstemp(dir=tmp_path)
|
||||||
|
with patch('tempfile.mkstemp', return_value=tmp_file):
|
||||||
|
yield tmp_file
|
||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
|
||||||
#
|
#
|
||||||
@ -15,10 +14,8 @@ This should typically only be used by the Core. Plugins should utilize the
|
|||||||
`:mod:EventManager` for similar functionality.
|
`:mod:EventManager` for similar functionality.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import types
|
from types import SimpleNamespace
|
||||||
|
|
||||||
from twisted.internet import reactor
|
from twisted.internet import reactor
|
||||||
|
|
||||||
@ -28,14 +25,6 @@ from deluge.common import decode_bytes
|
|||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
try:
|
|
||||||
SimpleNamespace = types.SimpleNamespace # Python 3.3+
|
|
||||||
except AttributeError:
|
|
||||||
|
|
||||||
class SimpleNamespace(object): # Python 2.7
|
|
||||||
def __init__(self, **attr):
|
|
||||||
self.__dict__.update(attr)
|
|
||||||
|
|
||||||
|
|
||||||
class AlertManager(component.Component):
|
class AlertManager(component.Component):
|
||||||
"""AlertManager fetches and processes libtorrent alerts"""
|
"""AlertManager fetches and processes libtorrent alerts"""
|
||||||
@ -57,6 +46,7 @@ class AlertManager(component.Component):
|
|||||||
| lt.alert.category_t.status_notification
|
| lt.alert.category_t.status_notification
|
||||||
| lt.alert.category_t.ip_block_notification
|
| lt.alert.category_t.ip_block_notification
|
||||||
| lt.alert.category_t.performance_warning
|
| lt.alert.category_t.performance_warning
|
||||||
|
| lt.alert.category_t.file_progress_notification
|
||||||
)
|
)
|
||||||
|
|
||||||
self.session.apply_settings({'alert_mask': alert_mask})
|
self.session.apply_settings({'alert_mask': alert_mask})
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
||||||
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
|
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
|
||||||
@ -8,12 +7,9 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
from io import open
|
|
||||||
|
|
||||||
import deluge.component as component
|
import deluge.component as component
|
||||||
import deluge.configmanager as configmanager
|
import deluge.configmanager as configmanager
|
||||||
@ -32,14 +28,14 @@ log = logging.getLogger(__name__)
|
|||||||
AUTH_LEVELS_MAPPING = {
|
AUTH_LEVELS_MAPPING = {
|
||||||
'NONE': AUTH_LEVEL_NONE,
|
'NONE': AUTH_LEVEL_NONE,
|
||||||
'READONLY': AUTH_LEVEL_READONLY,
|
'READONLY': AUTH_LEVEL_READONLY,
|
||||||
'DEFAULT': AUTH_LEVEL_NORMAL,
|
'DEFAULT': AUTH_LEVEL_DEFAULT,
|
||||||
'NORMAL': AUTH_LEVEL_DEFAULT,
|
'NORMAL': AUTH_LEVEL_NORMAL,
|
||||||
'ADMIN': AUTH_LEVEL_ADMIN,
|
'ADMIN': AUTH_LEVEL_ADMIN,
|
||||||
}
|
}
|
||||||
AUTH_LEVELS_MAPPING_REVERSE = {v: k for k, v in AUTH_LEVELS_MAPPING.items()}
|
AUTH_LEVELS_MAPPING_REVERSE = {v: k for k, v in AUTH_LEVELS_MAPPING.items()}
|
||||||
|
|
||||||
|
|
||||||
class Account(object):
|
class Account:
|
||||||
__slots__ = ('username', 'password', 'authlevel')
|
__slots__ = ('username', 'password', 'authlevel')
|
||||||
|
|
||||||
def __init__(self, username, password, authlevel):
|
def __init__(self, username, password, authlevel):
|
||||||
@ -56,10 +52,10 @@ class Account(object):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<Account username="%(username)s" authlevel=%(authlevel)s>' % {
|
return '<Account username="{username}" authlevel={authlevel}>'.format(
|
||||||
'username': self.username,
|
username=self.username,
|
||||||
'authlevel': self.authlevel,
|
authlevel=self.authlevel,
|
||||||
}
|
)
|
||||||
|
|
||||||
|
|
||||||
class AuthManager(component.Component):
|
class AuthManager(component.Component):
|
||||||
@ -184,7 +180,7 @@ class AuthManager(component.Component):
|
|||||||
if os.path.isfile(filepath):
|
if os.path.isfile(filepath):
|
||||||
log.debug('Creating backup of %s at: %s', filename, filepath_bak)
|
log.debug('Creating backup of %s at: %s', filename, filepath_bak)
|
||||||
shutil.copy2(filepath, filepath_bak)
|
shutil.copy2(filepath, filepath_bak)
|
||||||
except IOError as ex:
|
except OSError as ex:
|
||||||
log.error('Unable to backup %s to %s: %s', filepath, filepath_bak, ex)
|
log.error('Unable to backup %s to %s: %s', filepath, filepath_bak, ex)
|
||||||
else:
|
else:
|
||||||
log.info('Saving the %s at: %s', filename, filepath)
|
log.info('Saving the %s at: %s', filename, filepath)
|
||||||
@ -198,7 +194,7 @@ class AuthManager(component.Component):
|
|||||||
_file.flush()
|
_file.flush()
|
||||||
os.fsync(_file.fileno())
|
os.fsync(_file.fileno())
|
||||||
shutil.move(filepath_tmp, filepath)
|
shutil.move(filepath_tmp, filepath)
|
||||||
except IOError as ex:
|
except OSError as ex:
|
||||||
log.error('Unable to save %s: %s', filename, ex)
|
log.error('Unable to save %s: %s', filename, ex)
|
||||||
if os.path.isfile(filepath_bak):
|
if os.path.isfile(filepath_bak):
|
||||||
log.info('Restoring backup of %s from: %s', filename, filepath_bak)
|
log.info('Restoring backup of %s from: %s', filename, filepath_bak)
|
||||||
@ -227,9 +223,9 @@ class AuthManager(component.Component):
|
|||||||
for _filepath in (auth_file, auth_file_bak):
|
for _filepath in (auth_file, auth_file_bak):
|
||||||
log.info('Opening %s for load: %s', filename, _filepath)
|
log.info('Opening %s for load: %s', filename, _filepath)
|
||||||
try:
|
try:
|
||||||
with open(_filepath, 'r', encoding='utf8') as _file:
|
with open(_filepath, encoding='utf8') as _file:
|
||||||
file_data = _file.readlines()
|
file_data = _file.readlines()
|
||||||
except IOError as ex:
|
except OSError as ex:
|
||||||
log.warning('Unable to load %s: %s', _filepath, ex)
|
log.warning('Unable to load %s: %s', _filepath, ex)
|
||||||
file_data = []
|
file_data = []
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
|
||||||
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
|
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
|
||||||
@ -8,8 +7,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import division, unicode_literals
|
|
||||||
|
|
||||||
import glob
|
import glob
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@ -17,8 +14,9 @@ import shutil
|
|||||||
import tempfile
|
import tempfile
|
||||||
import threading
|
import threading
|
||||||
from base64 import b64decode, b64encode
|
from base64 import b64decode, b64encode
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||||
|
from urllib.request import URLError, urlopen
|
||||||
|
|
||||||
from six import string_types
|
|
||||||
from twisted.internet import defer, reactor, task
|
from twisted.internet import defer, reactor, task
|
||||||
from twisted.web.client import Agent, readBody
|
from twisted.web.client import Agent, readBody
|
||||||
|
|
||||||
@ -41,7 +39,7 @@ from deluge.core.pluginmanager import PluginManager
|
|||||||
from deluge.core.preferencesmanager import PreferencesManager
|
from deluge.core.preferencesmanager import PreferencesManager
|
||||||
from deluge.core.rpcserver import export
|
from deluge.core.rpcserver import export
|
||||||
from deluge.core.torrentmanager import TorrentManager
|
from deluge.core.torrentmanager import TorrentManager
|
||||||
from deluge.decorators import deprecated
|
from deluge.decorators import deprecated, maybe_coroutine
|
||||||
from deluge.error import (
|
from deluge.error import (
|
||||||
AddTorrentError,
|
AddTorrentError,
|
||||||
DelugeError,
|
DelugeError,
|
||||||
@ -56,12 +54,6 @@ from deluge.event import (
|
|||||||
)
|
)
|
||||||
from deluge.httpdownloader import download_file
|
from deluge.httpdownloader import download_file
|
||||||
|
|
||||||
try:
|
|
||||||
from urllib.request import URLError, urlopen
|
|
||||||
except ImportError:
|
|
||||||
# PY2 fallback
|
|
||||||
from urllib2 import URLError, urlopen
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEPR_SESSION_STATUS_KEYS = {
|
DEPR_SESSION_STATUS_KEYS = {
|
||||||
@ -120,7 +112,7 @@ class Core(component.Component):
|
|||||||
component.Component.__init__(self, 'Core')
|
component.Component.__init__(self, 'Core')
|
||||||
|
|
||||||
# Start the libtorrent session.
|
# Start the libtorrent session.
|
||||||
user_agent = 'Deluge/{} libtorrent/{}'.format(DELUGE_VER, LT_VERSION)
|
user_agent = f'Deluge/{DELUGE_VER} libtorrent/{LT_VERSION}'
|
||||||
peer_id = self._create_peer_id(DELUGE_VER)
|
peer_id = self._create_peer_id(DELUGE_VER)
|
||||||
log.debug('Starting session (peer_id: %s, user_agent: %s)', peer_id, user_agent)
|
log.debug('Starting session (peer_id: %s, user_agent: %s)', peer_id, user_agent)
|
||||||
settings_pack = {
|
settings_pack = {
|
||||||
@ -173,19 +165,25 @@ class Core(component.Component):
|
|||||||
# store the one in the config so we can restore it on shutdown
|
# store the one in the config so we can restore it on shutdown
|
||||||
self._old_listen_interface = None
|
self._old_listen_interface = None
|
||||||
if listen_interface:
|
if listen_interface:
|
||||||
if deluge.common.is_ip(listen_interface):
|
if deluge.common.is_interface(listen_interface):
|
||||||
self._old_listen_interface = self.config['listen_interface']
|
self._old_listen_interface = self.config['listen_interface']
|
||||||
self.config['listen_interface'] = listen_interface
|
self.config['listen_interface'] = listen_interface
|
||||||
else:
|
else:
|
||||||
log.error(
|
log.error(
|
||||||
'Invalid listen interface (must be IP Address): %s',
|
'Invalid listen interface (must be IP Address or Interface Name): %s',
|
||||||
listen_interface,
|
listen_interface,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._old_outgoing_interface = None
|
self._old_outgoing_interface = None
|
||||||
if outgoing_interface:
|
if outgoing_interface:
|
||||||
self._old_outgoing_interface = self.config['outgoing_interface']
|
if deluge.common.is_interface(outgoing_interface):
|
||||||
self.config['outgoing_interface'] = outgoing_interface
|
self._old_outgoing_interface = self.config['outgoing_interface']
|
||||||
|
self.config['outgoing_interface'] = outgoing_interface
|
||||||
|
else:
|
||||||
|
log.error(
|
||||||
|
'Invalid outgoing interface (must be IP Address or Interface Name): %s',
|
||||||
|
outgoing_interface,
|
||||||
|
)
|
||||||
|
|
||||||
# New release check information
|
# New release check information
|
||||||
self.__new_release = None
|
self.__new_release = None
|
||||||
@ -243,13 +241,12 @@ class Core(component.Component):
|
|||||||
"""Apply libtorrent session settings.
|
"""Apply libtorrent session settings.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
settings (dict): A dict of lt session settings to apply.
|
settings: A dict of lt session settings to apply.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.session.apply_settings(settings)
|
self.session.apply_settings(settings)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _create_peer_id(version):
|
def _create_peer_id(version: str) -> str:
|
||||||
"""Create a peer_id fingerprint.
|
"""Create a peer_id fingerprint.
|
||||||
|
|
||||||
This creates the peer_id and modifies the release char to identify
|
This creates the peer_id and modifies the release char to identify
|
||||||
@ -264,11 +261,10 @@ class Core(component.Component):
|
|||||||
``--DE201b--`` (beta pre-release of v2.0.1)
|
``--DE201b--`` (beta pre-release of v2.0.1)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
version (str): The version string in PEP440 dotted notation.
|
version: The version string in PEP440 dotted notation.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: The formatted peer_id with Deluge prefix e.g. '--DE200s--'
|
The formatted peer_id with Deluge prefix e.g. '--DE200s--'
|
||||||
|
|
||||||
"""
|
"""
|
||||||
split = deluge.common.VersionSplit(version)
|
split = deluge.common.VersionSplit(version)
|
||||||
# Fill list with zeros to length of 4 and use lt to create fingerprint.
|
# Fill list with zeros to length of 4 and use lt to create fingerprint.
|
||||||
@ -301,7 +297,7 @@ class Core(component.Component):
|
|||||||
if os.path.isfile(filepath):
|
if os.path.isfile(filepath):
|
||||||
log.debug('Creating backup of %s at: %s', filename, filepath_bak)
|
log.debug('Creating backup of %s at: %s', filename, filepath_bak)
|
||||||
shutil.copy2(filepath, filepath_bak)
|
shutil.copy2(filepath, filepath_bak)
|
||||||
except IOError as ex:
|
except OSError as ex:
|
||||||
log.error('Unable to backup %s to %s: %s', filepath, filepath_bak, ex)
|
log.error('Unable to backup %s to %s: %s', filepath, filepath_bak, ex)
|
||||||
else:
|
else:
|
||||||
log.info('Saving the %s at: %s', filename, filepath)
|
log.info('Saving the %s at: %s', filename, filepath)
|
||||||
@ -311,18 +307,17 @@ class Core(component.Component):
|
|||||||
_file.flush()
|
_file.flush()
|
||||||
os.fsync(_file.fileno())
|
os.fsync(_file.fileno())
|
||||||
shutil.move(filepath_tmp, filepath)
|
shutil.move(filepath_tmp, filepath)
|
||||||
except (IOError, EOFError) as ex:
|
except (OSError, EOFError) as ex:
|
||||||
log.error('Unable to save %s: %s', filename, ex)
|
log.error('Unable to save %s: %s', filename, ex)
|
||||||
if os.path.isfile(filepath_bak):
|
if os.path.isfile(filepath_bak):
|
||||||
log.info('Restoring backup of %s from: %s', filename, filepath_bak)
|
log.info('Restoring backup of %s from: %s', filename, filepath_bak)
|
||||||
shutil.move(filepath_bak, filepath)
|
shutil.move(filepath_bak, filepath)
|
||||||
|
|
||||||
def _load_session_state(self):
|
def _load_session_state(self) -> dict:
|
||||||
"""Loads the libtorrent session state
|
"""Loads the libtorrent session state
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: A libtorrent sesion state, empty dict if unable to load it.
|
A libtorrent sesion state, empty dict if unable to load it.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
filename = 'session.state'
|
filename = 'session.state'
|
||||||
filepath = get_config_dir(filename)
|
filepath = get_config_dir(filename)
|
||||||
@ -333,7 +328,7 @@ class Core(component.Component):
|
|||||||
try:
|
try:
|
||||||
with open(_filepath, 'rb') as _file:
|
with open(_filepath, 'rb') as _file:
|
||||||
state = lt.bdecode(_file.read())
|
state = lt.bdecode(_file.read())
|
||||||
except (IOError, EOFError, RuntimeError) as ex:
|
except (OSError, EOFError, RuntimeError) as ex:
|
||||||
log.warning('Unable to load %s: %s', _filepath, ex)
|
log.warning('Unable to load %s: %s', _filepath, ex)
|
||||||
else:
|
else:
|
||||||
log.info('Successfully loaded %s: %s', filename, _filepath)
|
log.info('Successfully loaded %s: %s', filename, _filepath)
|
||||||
@ -404,18 +399,19 @@ class Core(component.Component):
|
|||||||
|
|
||||||
# Exported Methods
|
# Exported Methods
|
||||||
@export
|
@export
|
||||||
def add_torrent_file_async(self, filename, filedump, options, save_state=True):
|
def add_torrent_file_async(
|
||||||
|
self, filename: str, filedump: str, options: dict, save_state: bool = True
|
||||||
|
) -> 'defer.Deferred[Optional[str]]':
|
||||||
"""Adds a torrent file to the session asynchronously.
|
"""Adds a torrent file to the session asynchronously.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
filename (str): The filename of the torrent.
|
filename: The filename of the torrent.
|
||||||
filedump (str): A base64 encoded string of torrent file contents.
|
filedump: A base64 encoded string of torrent file contents.
|
||||||
options (dict): The options to apply to the torrent upon adding.
|
options: The options to apply to the torrent upon adding.
|
||||||
save_state (bool): If the state should be saved after adding the file.
|
save_state: If the state should be saved after adding the file.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Deferred: The torrent ID or None.
|
The torrent ID or None.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
filedump = b64decode(filedump)
|
filedump = b64decode(filedump)
|
||||||
@ -436,42 +432,39 @@ class Core(component.Component):
|
|||||||
return d
|
return d
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def prefetch_magnet_metadata(self, magnet, timeout=30):
|
@maybe_coroutine
|
||||||
|
async def prefetch_magnet_metadata(
|
||||||
|
self, magnet: str, timeout: int = 30
|
||||||
|
) -> Tuple[str, bytes]:
|
||||||
"""Download magnet metadata without adding to Deluge session.
|
"""Download magnet metadata without adding to Deluge session.
|
||||||
|
|
||||||
Used by UIs to get magnet files for selection before adding to session.
|
Used by UIs to get magnet files for selection before adding to session.
|
||||||
|
|
||||||
|
The metadata is bencoded and for transfer base64 encoded.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
magnet (str): The magnet URI.
|
magnet: The magnet URI.
|
||||||
timeout (int): Number of seconds to wait before canceling request.
|
timeout: Number of seconds to wait before canceling request.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Deferred: A tuple of (torrent_id (str), metadata (dict)) for the magnet.
|
A tuple of (torrent_id, metadata) for the magnet.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
return await self.torrentmanager.prefetch_metadata(magnet, timeout)
|
||||||
def on_metadata(result, result_d):
|
|
||||||
"""Return result of torrent_id and metadata"""
|
|
||||||
result_d.callback(result)
|
|
||||||
return result
|
|
||||||
|
|
||||||
d = self.torrentmanager.prefetch_metadata(magnet, timeout)
|
|
||||||
# Use a separate callback chain to handle existing prefetching magnet.
|
|
||||||
result_d = defer.Deferred()
|
|
||||||
d.addBoth(on_metadata, result_d)
|
|
||||||
return result_d
|
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def add_torrent_file(self, filename, filedump, options):
|
def add_torrent_file(
|
||||||
|
self, filename: str, filedump: Union[str, bytes], options: dict
|
||||||
|
) -> Optional[str]:
|
||||||
"""Adds a torrent file to the session.
|
"""Adds a torrent file to the session.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
filename (str): The filename of the torrent.
|
filename: The filename of the torrent.
|
||||||
filedump (str): A base64 encoded string of the torrent file contents.
|
filedump: A base64 encoded string of the torrent file contents.
|
||||||
options (dict): The options to apply to the torrent upon adding.
|
options: The options to apply to the torrent upon adding.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: The torrent_id or None.
|
The torrent_id or None.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
filedump = b64decode(filedump)
|
filedump = b64decode(filedump)
|
||||||
@ -487,25 +480,26 @@ class Core(component.Component):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def add_torrent_files(self, torrent_files):
|
def add_torrent_files(
|
||||||
|
self, torrent_files: List[Tuple[str, Union[str, bytes], dict]]
|
||||||
|
) -> 'defer.Deferred[List[AddTorrentError]]':
|
||||||
"""Adds multiple torrent files to the session asynchronously.
|
"""Adds multiple torrent files to the session asynchronously.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
torrent_files (list of tuples): Torrent files as tuple of
|
torrent_files: Torrent files as tuple of
|
||||||
``(filename, filedump, options)``.
|
``(filename, filedump, options)``.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Deferred
|
A list of errors (if there were any)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@maybe_coroutine
|
||||||
def add_torrents():
|
async def add_torrents():
|
||||||
errors = []
|
errors = []
|
||||||
last_index = len(torrent_files) - 1
|
last_index = len(torrent_files) - 1
|
||||||
for idx, torrent in enumerate(torrent_files):
|
for idx, torrent in enumerate(torrent_files):
|
||||||
try:
|
try:
|
||||||
yield self.add_torrent_file_async(
|
await self.add_torrent_file_async(
|
||||||
torrent[0], torrent[1], torrent[2], save_state=idx == last_index
|
torrent[0], torrent[1], torrent[2], save_state=idx == last_index
|
||||||
)
|
)
|
||||||
except AddTorrentError as ex:
|
except AddTorrentError as ex:
|
||||||
@ -516,93 +510,89 @@ class Core(component.Component):
|
|||||||
return task.deferLater(reactor, 0, add_torrents)
|
return task.deferLater(reactor, 0, add_torrents)
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def add_torrent_url(self, url, options, headers=None):
|
@maybe_coroutine
|
||||||
"""
|
async def add_torrent_url(
|
||||||
Adds a torrent from a URL. Deluge will attempt to fetch the torrent
|
self, url: str, options: dict, headers: dict = None
|
||||||
|
) -> 'defer.Deferred[Optional[str]]':
|
||||||
|
"""Adds a torrent from a URL. Deluge will attempt to fetch the torrent
|
||||||
from the URL prior to adding it to the session.
|
from the URL prior to adding it to the session.
|
||||||
|
|
||||||
:param url: the URL pointing to the torrent file
|
Args:
|
||||||
:type url: string
|
url: the URL pointing to the torrent file
|
||||||
:param options: the options to apply to the torrent on add
|
options: the options to apply to the torrent on add
|
||||||
:type options: dict
|
headers: any optional headers to send
|
||||||
:param headers: any optional headers to send
|
|
||||||
:type headers: dict
|
|
||||||
|
|
||||||
:returns: a Deferred which returns the torrent_id as a str or None
|
Returns:
|
||||||
|
a Deferred which returns the torrent_id as a str or None
|
||||||
"""
|
"""
|
||||||
log.info('Attempting to add URL %s', url)
|
log.info('Attempting to add URL %s', url)
|
||||||
|
|
||||||
def on_download_success(filename):
|
tmp_fd, tmp_file = tempfile.mkstemp(prefix='deluge_url.', suffix='.torrent')
|
||||||
# We got the file, so add it to the session
|
try:
|
||||||
|
filename = await download_file(
|
||||||
|
url, tmp_file, headers=headers, force_filename=True
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
log.error('Failed to add torrent from URL %s', url)
|
||||||
|
raise
|
||||||
|
else:
|
||||||
with open(filename, 'rb') as _file:
|
with open(filename, 'rb') as _file:
|
||||||
data = _file.read()
|
data = _file.read()
|
||||||
try:
|
|
||||||
os.remove(filename)
|
|
||||||
except OSError as ex:
|
|
||||||
log.warning('Could not remove temp file: %s', ex)
|
|
||||||
return self.add_torrent_file(filename, b64encode(data), options)
|
return self.add_torrent_file(filename, b64encode(data), options)
|
||||||
|
finally:
|
||||||
def on_download_fail(failure):
|
try:
|
||||||
# Log the error and pass the failure onto the client
|
os.close(tmp_fd)
|
||||||
log.error('Failed to add torrent from URL %s', url)
|
os.remove(tmp_file)
|
||||||
return failure
|
except OSError as ex:
|
||||||
|
log.warning(f'Unable to delete temp file {tmp_file}: , {ex}')
|
||||||
tmp_fd, tmp_file = tempfile.mkstemp(prefix='deluge_url.', suffix='.torrent')
|
|
||||||
os.close(tmp_fd)
|
|
||||||
d = download_file(url, tmp_file, headers=headers, force_filename=True)
|
|
||||||
d.addCallbacks(on_download_success, on_download_fail)
|
|
||||||
return d
|
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def add_torrent_magnet(self, uri, options):
|
def add_torrent_magnet(self, uri: str, options: dict) -> str:
|
||||||
"""
|
"""Adds a torrent from a magnet link.
|
||||||
Adds a torrent from a magnet link.
|
|
||||||
|
|
||||||
:param uri: the magnet link
|
Args:
|
||||||
:type uri: string
|
uri: the magnet link
|
||||||
:param options: the options to apply to the torrent on add
|
options: the options to apply to the torrent on add
|
||||||
:type options: dict
|
|
||||||
|
|
||||||
:returns: the torrent_id
|
|
||||||
:rtype: string
|
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
the torrent_id
|
||||||
"""
|
"""
|
||||||
log.debug('Attempting to add by magnet URI: %s', uri)
|
log.debug('Attempting to add by magnet URI: %s', uri)
|
||||||
|
|
||||||
return self.torrentmanager.add(magnet=uri, options=options)
|
return self.torrentmanager.add(magnet=uri, options=options)
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def remove_torrent(self, torrent_id, remove_data):
|
def remove_torrent(self, torrent_id: str, remove_data: bool) -> bool:
|
||||||
"""Removes a single torrent from the session.
|
"""Removes a single torrent from the session.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
torrent_id (str): The torrent ID to remove.
|
torrent_id: The torrent ID to remove.
|
||||||
remove_data (bool): If True, also remove the downloaded data.
|
remove_data: If True, also remove the downloaded data.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if removed successfully.
|
True if removed successfully.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
InvalidTorrentError: If the torrent ID does not exist in the session.
|
InvalidTorrentError: If the torrent ID does not exist in the session.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
log.debug('Removing torrent %s from the core.', torrent_id)
|
log.debug('Removing torrent %s from the core.', torrent_id)
|
||||||
return self.torrentmanager.remove(torrent_id, remove_data)
|
return self.torrentmanager.remove(torrent_id, remove_data)
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def remove_torrents(self, torrent_ids, remove_data):
|
def remove_torrents(
|
||||||
|
self, torrent_ids: List[str], remove_data: bool
|
||||||
|
) -> 'defer.Deferred[List[Tuple[str, str]]]':
|
||||||
"""Remove multiple torrents from the session.
|
"""Remove multiple torrents from the session.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
torrent_ids (list): The torrent IDs to remove.
|
torrent_ids: The torrent IDs to remove.
|
||||||
remove_data (bool): If True, also remove the downloaded data.
|
remove_data: If True, also remove the downloaded data.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list: An empty list if no errors occurred otherwise the list contains
|
An empty list if no errors occurred otherwise the list contains
|
||||||
tuples of strings, a torrent ID and an error message. For example:
|
tuples of strings, a torrent ID and an error message. For example:
|
||||||
|
|
||||||
[('<torrent_id>', 'Error removing torrent')]
|
|
||||||
|
|
||||||
|
[('<torrent_id>', 'Error removing torrent')]
|
||||||
"""
|
"""
|
||||||
log.info('Removing %d torrents from core.', len(torrent_ids))
|
log.info('Removing %d torrents from core.', len(torrent_ids))
|
||||||
|
|
||||||
@ -626,17 +616,17 @@ class Core(component.Component):
|
|||||||
return task.deferLater(reactor, 0, do_remove_torrents)
|
return task.deferLater(reactor, 0, do_remove_torrents)
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def get_session_status(self, keys):
|
def get_session_status(self, keys: List[str]) -> Dict[str, Union[int, float]]:
|
||||||
"""Gets the session status values for 'keys', these keys are taking
|
"""Gets the session status values for 'keys', these keys are taking
|
||||||
from libtorrent's session status.
|
from libtorrent's session status.
|
||||||
|
|
||||||
See: http://www.rasterbar.com/products/libtorrent/manual.html#status
|
See: http://www.rasterbar.com/products/libtorrent/manual.html#status
|
||||||
|
|
||||||
:param keys: the keys for which we want values
|
Args:
|
||||||
:type keys: list
|
keys: the keys for which we want values
|
||||||
:returns: a dictionary of {key: value, ...}
|
|
||||||
:rtype: dict
|
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
a dictionary of {key: value, ...}
|
||||||
"""
|
"""
|
||||||
if not keys:
|
if not keys:
|
||||||
return self.session_status
|
return self.session_status
|
||||||
@ -657,22 +647,22 @@ class Core(component.Component):
|
|||||||
return status
|
return status
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def force_reannounce(self, torrent_ids):
|
def force_reannounce(self, torrent_ids: List[str]) -> None:
|
||||||
log.debug('Forcing reannouncment to: %s', torrent_ids)
|
log.debug('Forcing reannouncment to: %s', torrent_ids)
|
||||||
for torrent_id in torrent_ids:
|
for torrent_id in torrent_ids:
|
||||||
self.torrentmanager[torrent_id].force_reannounce()
|
self.torrentmanager[torrent_id].force_reannounce()
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def pause_torrent(self, torrent_id):
|
def pause_torrent(self, torrent_id: str) -> None:
|
||||||
"""Pauses a torrent"""
|
"""Pauses a torrent"""
|
||||||
log.debug('Pausing: %s', torrent_id)
|
log.debug('Pausing: %s', torrent_id)
|
||||||
if not isinstance(torrent_id, string_types):
|
if not isinstance(torrent_id, str):
|
||||||
self.pause_torrents(torrent_id)
|
self.pause_torrents(torrent_id)
|
||||||
else:
|
else:
|
||||||
self.torrentmanager[torrent_id].pause()
|
self.torrentmanager[torrent_id].pause()
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def pause_torrents(self, torrent_ids=None):
|
def pause_torrents(self, torrent_ids: List[str] = None) -> None:
|
||||||
"""Pauses a list of torrents"""
|
"""Pauses a list of torrents"""
|
||||||
if not torrent_ids:
|
if not torrent_ids:
|
||||||
torrent_ids = self.torrentmanager.get_torrent_list()
|
torrent_ids = self.torrentmanager.get_torrent_list()
|
||||||
@ -680,27 +670,27 @@ class Core(component.Component):
|
|||||||
self.pause_torrent(torrent_id)
|
self.pause_torrent(torrent_id)
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def connect_peer(self, torrent_id, ip, port):
|
def connect_peer(self, torrent_id: str, ip: str, port: int):
|
||||||
log.debug('adding peer %s to %s', ip, torrent_id)
|
log.debug('adding peer %s to %s', ip, torrent_id)
|
||||||
if not self.torrentmanager[torrent_id].connect_peer(ip, port):
|
if not self.torrentmanager[torrent_id].connect_peer(ip, port):
|
||||||
log.warning('Error adding peer %s:%s to %s', ip, port, torrent_id)
|
log.warning('Error adding peer %s:%s to %s', ip, port, torrent_id)
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def move_storage(self, torrent_ids, dest):
|
def move_storage(self, torrent_ids: List[str], dest: str):
|
||||||
log.debug('Moving storage %s to %s', torrent_ids, dest)
|
log.debug('Moving storage %s to %s', torrent_ids, dest)
|
||||||
for torrent_id in torrent_ids:
|
for torrent_id in torrent_ids:
|
||||||
if not self.torrentmanager[torrent_id].move_storage(dest):
|
if not self.torrentmanager[torrent_id].move_storage(dest):
|
||||||
log.warning('Error moving torrent %s to %s', torrent_id, dest)
|
log.warning('Error moving torrent %s to %s', torrent_id, dest)
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def pause_session(self):
|
def pause_session(self) -> None:
|
||||||
"""Pause the entire session"""
|
"""Pause the entire session"""
|
||||||
if not self.session.is_paused():
|
if not self.session.is_paused():
|
||||||
self.session.pause()
|
self.session.pause()
|
||||||
component.get('EventManager').emit(SessionPausedEvent())
|
component.get('EventManager').emit(SessionPausedEvent())
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def resume_session(self):
|
def resume_session(self) -> None:
|
||||||
"""Resume the entire session"""
|
"""Resume the entire session"""
|
||||||
if self.session.is_paused():
|
if self.session.is_paused():
|
||||||
self.session.resume()
|
self.session.resume()
|
||||||
@ -709,21 +699,21 @@ class Core(component.Component):
|
|||||||
component.get('EventManager').emit(SessionResumedEvent())
|
component.get('EventManager').emit(SessionResumedEvent())
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def is_session_paused(self):
|
def is_session_paused(self) -> bool:
|
||||||
"""Returns the activity of the session"""
|
"""Returns the activity of the session"""
|
||||||
return self.session.is_paused()
|
return self.session.is_paused()
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def resume_torrent(self, torrent_id):
|
def resume_torrent(self, torrent_id: str) -> None:
|
||||||
"""Resumes a torrent"""
|
"""Resumes a torrent"""
|
||||||
log.debug('Resuming: %s', torrent_id)
|
log.debug('Resuming: %s', torrent_id)
|
||||||
if not isinstance(torrent_id, string_types):
|
if not isinstance(torrent_id, str):
|
||||||
self.resume_torrents(torrent_id)
|
self.resume_torrents(torrent_id)
|
||||||
else:
|
else:
|
||||||
self.torrentmanager[torrent_id].resume()
|
self.torrentmanager[torrent_id].resume()
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def resume_torrents(self, torrent_ids=None):
|
def resume_torrents(self, torrent_ids: List[str] = None) -> None:
|
||||||
"""Resumes a list of torrents"""
|
"""Resumes a list of torrents"""
|
||||||
if not torrent_ids:
|
if not torrent_ids:
|
||||||
torrent_ids = self.torrentmanager.get_torrent_list()
|
torrent_ids = self.torrentmanager.get_torrent_list()
|
||||||
@ -756,7 +746,9 @@ class Core(component.Component):
|
|||||||
return status
|
return status
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def get_torrent_status(self, torrent_id, keys, diff=False):
|
def get_torrent_status(
|
||||||
|
self, torrent_id: str, keys: List[str], diff: bool = False
|
||||||
|
) -> dict:
|
||||||
torrent_keys, plugin_keys = self.torrentmanager.separate_keys(
|
torrent_keys, plugin_keys = self.torrentmanager.separate_keys(
|
||||||
keys, [torrent_id]
|
keys, [torrent_id]
|
||||||
)
|
)
|
||||||
@ -770,57 +762,54 @@ class Core(component.Component):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def get_torrents_status(self, filter_dict, keys, diff=False):
|
@maybe_coroutine
|
||||||
"""
|
async def get_torrents_status(
|
||||||
returns all torrents , optionally filtered by filter_dict.
|
self, filter_dict: dict, keys: List[str], diff: bool = False
|
||||||
"""
|
) -> dict:
|
||||||
|
"""returns all torrents , optionally filtered by filter_dict."""
|
||||||
|
all_keys = not keys
|
||||||
torrent_ids = self.filtermanager.filter_torrent_ids(filter_dict)
|
torrent_ids = self.filtermanager.filter_torrent_ids(filter_dict)
|
||||||
d = self.torrentmanager.torrents_status_update(torrent_ids, keys, diff=diff)
|
status_dict, plugin_keys = await self.torrentmanager.torrents_status_update(
|
||||||
|
torrent_ids, keys, diff=diff
|
||||||
def add_plugin_fields(args):
|
)
|
||||||
status_dict, plugin_keys = args
|
# Ask the plugin manager to fill in the plugin keys
|
||||||
# Ask the plugin manager to fill in the plugin keys
|
if len(plugin_keys) > 0 or all_keys:
|
||||||
if len(plugin_keys) > 0:
|
for key in status_dict:
|
||||||
for key in status_dict:
|
status_dict[key].update(self.pluginmanager.get_status(key, plugin_keys))
|
||||||
status_dict[key].update(
|
return status_dict
|
||||||
self.pluginmanager.get_status(key, plugin_keys)
|
|
||||||
)
|
|
||||||
return status_dict
|
|
||||||
|
|
||||||
d.addCallback(add_plugin_fields)
|
|
||||||
return d
|
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def get_filter_tree(self, show_zero_hits=True, hide_cat=None):
|
def get_filter_tree(
|
||||||
"""
|
self, show_zero_hits: bool = True, hide_cat: List[str] = None
|
||||||
returns {field: [(value,count)] }
|
) -> Dict:
|
||||||
|
"""returns {field: [(value,count)] }
|
||||||
for use in sidebar(s)
|
for use in sidebar(s)
|
||||||
"""
|
"""
|
||||||
return self.filtermanager.get_filter_tree(show_zero_hits, hide_cat)
|
return self.filtermanager.get_filter_tree(show_zero_hits, hide_cat)
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def get_session_state(self):
|
def get_session_state(self) -> List[str]:
|
||||||
"""Returns a list of torrent_ids in the session."""
|
"""Returns a list of torrent_ids in the session."""
|
||||||
# Get the torrent list from the TorrentManager
|
# Get the torrent list from the TorrentManager
|
||||||
return self.torrentmanager.get_torrent_list()
|
return self.torrentmanager.get_torrent_list()
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def get_config(self):
|
def get_config(self) -> dict:
|
||||||
"""Get all the preferences as a dictionary"""
|
"""Get all the preferences as a dictionary"""
|
||||||
return self.config.config
|
return self.config.config
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def get_config_value(self, key):
|
def get_config_value(self, key: str) -> Any:
|
||||||
"""Get the config value for key"""
|
"""Get the config value for key"""
|
||||||
return self.config.get(key)
|
return self.config.get(key)
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def get_config_values(self, keys):
|
def get_config_values(self, keys: List[str]) -> Dict[str, Any]:
|
||||||
"""Get the config values for the entered keys"""
|
"""Get the config values for the entered keys"""
|
||||||
return {key: self.config.get(key) for key in keys}
|
return {key: self.config.get(key) for key in keys}
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def set_config(self, config):
|
def set_config(self, config: Dict[str, Any]):
|
||||||
"""Set the config with values from dictionary"""
|
"""Set the config with values from dictionary"""
|
||||||
# Load all the values into the configuration
|
# Load all the values into the configuration
|
||||||
for key in config:
|
for key in config:
|
||||||
@ -829,21 +818,20 @@ class Core(component.Component):
|
|||||||
self.config[key] = config[key]
|
self.config[key] = config[key]
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def get_listen_port(self):
|
def get_listen_port(self) -> int:
|
||||||
"""Returns the active listen port"""
|
"""Returns the active listen port"""
|
||||||
return self.session.listen_port()
|
return self.session.listen_port()
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def get_proxy(self):
|
def get_proxy(self) -> Dict[str, Any]:
|
||||||
"""Returns the proxy settings
|
"""Returns the proxy settings
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: Contains proxy settings.
|
Proxy settings.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
Proxy type names:
|
Proxy type names:
|
||||||
0: None, 1: Socks4, 2: Socks5, 3: Socks5 w Auth, 4: HTTP, 5: HTTP w Auth, 6: I2P
|
0: None, 1: Socks4, 2: Socks5, 3: Socks5 w Auth, 4: HTTP, 5: HTTP w Auth, 6: I2P
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
settings = self.session.get_settings()
|
settings = self.session.get_settings()
|
||||||
@ -866,54 +854,58 @@ class Core(component.Component):
|
|||||||
return proxy_dict
|
return proxy_dict
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def get_available_plugins(self):
|
def get_available_plugins(self) -> List[str]:
|
||||||
"""Returns a list of plugins available in the core"""
|
"""Returns a list of plugins available in the core"""
|
||||||
return self.pluginmanager.get_available_plugins()
|
return self.pluginmanager.get_available_plugins()
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def get_enabled_plugins(self):
|
def get_enabled_plugins(self) -> List[str]:
|
||||||
"""Returns a list of enabled plugins in the core"""
|
"""Returns a list of enabled plugins in the core"""
|
||||||
return self.pluginmanager.get_enabled_plugins()
|
return self.pluginmanager.get_enabled_plugins()
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def enable_plugin(self, plugin):
|
def enable_plugin(self, plugin: str) -> 'defer.Deferred[bool]':
|
||||||
return self.pluginmanager.enable_plugin(plugin)
|
return self.pluginmanager.enable_plugin(plugin)
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def disable_plugin(self, plugin):
|
def disable_plugin(self, plugin: str) -> 'defer.Deferred[bool]':
|
||||||
return self.pluginmanager.disable_plugin(plugin)
|
return self.pluginmanager.disable_plugin(plugin)
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def force_recheck(self, torrent_ids):
|
def force_recheck(self, torrent_ids: List[str]) -> None:
|
||||||
"""Forces a data recheck on torrent_ids"""
|
"""Forces a data recheck on torrent_ids"""
|
||||||
for torrent_id in torrent_ids:
|
for torrent_id in torrent_ids:
|
||||||
self.torrentmanager[torrent_id].force_recheck()
|
self.torrentmanager[torrent_id].force_recheck()
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def set_torrent_options(self, torrent_ids, options):
|
def set_torrent_options(
|
||||||
|
self, torrent_ids: List[str], options: Dict[str, Any]
|
||||||
|
) -> None:
|
||||||
"""Sets the torrent options for torrent_ids
|
"""Sets the torrent options for torrent_ids
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
torrent_ids (list): A list of torrent_ids to set the options for.
|
torrent_ids: A list of torrent_ids to set the options for.
|
||||||
options (dict): A dict of torrent options to set. See
|
options: A dict of torrent options to set. See
|
||||||
``torrent.TorrentOptions`` class for valid keys.
|
``torrent.TorrentOptions`` class for valid keys.
|
||||||
"""
|
"""
|
||||||
if 'owner' in options and not self.authmanager.has_account(options['owner']):
|
if 'owner' in options and not self.authmanager.has_account(options['owner']):
|
||||||
raise DelugeError('Username "%s" is not known.' % options['owner'])
|
raise DelugeError('Username "%s" is not known.' % options['owner'])
|
||||||
|
|
||||||
if isinstance(torrent_ids, string_types):
|
if isinstance(torrent_ids, str):
|
||||||
torrent_ids = [torrent_ids]
|
torrent_ids = [torrent_ids]
|
||||||
|
|
||||||
for torrent_id in torrent_ids:
|
for torrent_id in torrent_ids:
|
||||||
self.torrentmanager[torrent_id].set_options(options)
|
self.torrentmanager[torrent_id].set_options(options)
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def set_torrent_trackers(self, torrent_id, trackers):
|
def set_torrent_trackers(
|
||||||
|
self, torrent_id: str, trackers: List[Dict[str, Any]]
|
||||||
|
) -> None:
|
||||||
"""Sets a torrents tracker list. trackers will be ``[{"url", "tier"}]``"""
|
"""Sets a torrents tracker list. trackers will be ``[{"url", "tier"}]``"""
|
||||||
return self.torrentmanager[torrent_id].set_trackers(trackers)
|
return self.torrentmanager[torrent_id].set_trackers(trackers)
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def get_magnet_uri(self, torrent_id):
|
def get_magnet_uri(self, torrent_id: str) -> str:
|
||||||
return self.torrentmanager[torrent_id].get_magnet_uri()
|
return self.torrentmanager[torrent_id].get_magnet_uri()
|
||||||
|
|
||||||
@deprecated
|
@deprecated
|
||||||
@ -1061,7 +1053,7 @@ class Core(component.Component):
|
|||||||
self.add_torrent_file(os.path.split(target)[1], filedump, options)
|
self.add_torrent_file(os.path.split(target)[1], filedump, options)
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def upload_plugin(self, filename, filedump):
|
def upload_plugin(self, filename: str, filedump: Union[str, bytes]) -> None:
|
||||||
"""This method is used to upload new plugins to the daemon. It is used
|
"""This method is used to upload new plugins to the daemon. It is used
|
||||||
when connecting to the daemon remotely and installing a new plugin on
|
when connecting to the daemon remotely and installing a new plugin on
|
||||||
the client side. ``plugin_data`` is a ``xmlrpc.Binary`` object of the file data,
|
the client side. ``plugin_data`` is a ``xmlrpc.Binary`` object of the file data,
|
||||||
@ -1079,26 +1071,24 @@ class Core(component.Component):
|
|||||||
component.get('CorePluginManager').scan_for_plugins()
|
component.get('CorePluginManager').scan_for_plugins()
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def rescan_plugins(self):
|
def rescan_plugins(self) -> None:
|
||||||
"""
|
"""Re-scans the plugin folders for new plugins"""
|
||||||
Re-scans the plugin folders for new plugins
|
|
||||||
"""
|
|
||||||
component.get('CorePluginManager').scan_for_plugins()
|
component.get('CorePluginManager').scan_for_plugins()
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def rename_files(self, torrent_id, filenames):
|
def rename_files(
|
||||||
"""
|
self, torrent_id: str, filenames: List[Tuple[int, str]]
|
||||||
Rename files in ``torrent_id``. Since this is an asynchronous operation by
|
) -> defer.Deferred:
|
||||||
|
"""Rename files in ``torrent_id``. Since this is an asynchronous operation by
|
||||||
libtorrent, watch for the TorrentFileRenamedEvent to know when the
|
libtorrent, watch for the TorrentFileRenamedEvent to know when the
|
||||||
files have been renamed.
|
files have been renamed.
|
||||||
|
|
||||||
:param torrent_id: the torrent_id to rename files
|
Args:
|
||||||
:type torrent_id: string
|
torrent_id: the torrent_id to rename files
|
||||||
:param filenames: a list of index, filename pairs
|
filenames: a list of index, filename pairs
|
||||||
:type filenames: ((index, filename), ...)
|
|
||||||
|
|
||||||
:raises InvalidTorrentError: if torrent_id is invalid
|
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
InvalidTorrentError: if torrent_id is invalid
|
||||||
"""
|
"""
|
||||||
if torrent_id not in self.torrentmanager.torrents:
|
if torrent_id not in self.torrentmanager.torrents:
|
||||||
raise InvalidTorrentError('torrent_id is not in session')
|
raise InvalidTorrentError('torrent_id is not in session')
|
||||||
@ -1109,21 +1099,20 @@ class Core(component.Component):
|
|||||||
return task.deferLater(reactor, 0, rename)
|
return task.deferLater(reactor, 0, rename)
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def rename_folder(self, torrent_id, folder, new_folder):
|
def rename_folder(
|
||||||
"""
|
self, torrent_id: str, folder: str, new_folder: str
|
||||||
Renames the 'folder' to 'new_folder' in 'torrent_id'. Watch for the
|
) -> defer.Deferred:
|
||||||
|
"""Renames the 'folder' to 'new_folder' in 'torrent_id'. Watch for the
|
||||||
TorrentFolderRenamedEvent which is emitted when the folder has been
|
TorrentFolderRenamedEvent which is emitted when the folder has been
|
||||||
renamed successfully.
|
renamed successfully.
|
||||||
|
|
||||||
:param torrent_id: the torrent to rename folder in
|
Args:
|
||||||
:type torrent_id: string
|
torrent_id: the torrent to rename folder in
|
||||||
:param folder: the folder to rename
|
folder: the folder to rename
|
||||||
:type folder: string
|
new_folder: the new folder name
|
||||||
:param new_folder: the new folder name
|
|
||||||
:type new_folder: string
|
|
||||||
|
|
||||||
:raises InvalidTorrentError: if the torrent_id is invalid
|
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
InvalidTorrentError: if the torrent_id is invalid
|
||||||
"""
|
"""
|
||||||
if torrent_id not in self.torrentmanager.torrents:
|
if torrent_id not in self.torrentmanager.torrents:
|
||||||
raise InvalidTorrentError('torrent_id is not in session')
|
raise InvalidTorrentError('torrent_id is not in session')
|
||||||
@ -1131,7 +1120,7 @@ class Core(component.Component):
|
|||||||
return self.torrentmanager[torrent_id].rename_folder(folder, new_folder)
|
return self.torrentmanager[torrent_id].rename_folder(folder, new_folder)
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def queue_top(self, torrent_ids):
|
def queue_top(self, torrent_ids: List[str]) -> None:
|
||||||
log.debug('Attempting to queue %s to top', torrent_ids)
|
log.debug('Attempting to queue %s to top', torrent_ids)
|
||||||
# torrent_ids must be sorted in reverse before moving to preserve order
|
# torrent_ids must be sorted in reverse before moving to preserve order
|
||||||
for torrent_id in sorted(
|
for torrent_id in sorted(
|
||||||
@ -1145,7 +1134,7 @@ class Core(component.Component):
|
|||||||
log.warning('torrent_id: %s does not exist in the queue', torrent_id)
|
log.warning('torrent_id: %s does not exist in the queue', torrent_id)
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def queue_up(self, torrent_ids):
|
def queue_up(self, torrent_ids: List[str]) -> None:
|
||||||
log.debug('Attempting to queue %s to up', torrent_ids)
|
log.debug('Attempting to queue %s to up', torrent_ids)
|
||||||
torrents = (
|
torrents = (
|
||||||
(self.torrentmanager.get_queue_position(torrent_id), torrent_id)
|
(self.torrentmanager.get_queue_position(torrent_id), torrent_id)
|
||||||
@ -1170,7 +1159,7 @@ class Core(component.Component):
|
|||||||
prev_queue_position = queue_position
|
prev_queue_position = queue_position
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def queue_down(self, torrent_ids):
|
def queue_down(self, torrent_ids: List[str]) -> None:
|
||||||
log.debug('Attempting to queue %s to down', torrent_ids)
|
log.debug('Attempting to queue %s to down', torrent_ids)
|
||||||
torrents = (
|
torrents = (
|
||||||
(self.torrentmanager.get_queue_position(torrent_id), torrent_id)
|
(self.torrentmanager.get_queue_position(torrent_id), torrent_id)
|
||||||
@ -1195,7 +1184,7 @@ class Core(component.Component):
|
|||||||
prev_queue_position = queue_position
|
prev_queue_position = queue_position
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def queue_bottom(self, torrent_ids):
|
def queue_bottom(self, torrent_ids: List[str]) -> None:
|
||||||
log.debug('Attempting to queue %s to bottom', torrent_ids)
|
log.debug('Attempting to queue %s to bottom', torrent_ids)
|
||||||
# torrent_ids must be sorted before moving to preserve order
|
# torrent_ids must be sorted before moving to preserve order
|
||||||
for torrent_id in sorted(
|
for torrent_id in sorted(
|
||||||
@ -1209,17 +1198,15 @@ class Core(component.Component):
|
|||||||
log.warning('torrent_id: %s does not exist in the queue', torrent_id)
|
log.warning('torrent_id: %s does not exist in the queue', torrent_id)
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def glob(self, path):
|
def glob(self, path: str) -> List[str]:
|
||||||
return glob.glob(path)
|
return glob.glob(path)
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def test_listen_port(self):
|
def test_listen_port(self) -> 'defer.Deferred[Optional[bool]]':
|
||||||
"""
|
"""Checks if the active port is open
|
||||||
Checks if the active port is open
|
|
||||||
|
|
||||||
:returns: True if the port is open, False if not
|
|
||||||
:rtype: bool
|
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the port is open, False if not
|
||||||
"""
|
"""
|
||||||
port = self.get_listen_port()
|
port = self.get_listen_port()
|
||||||
url = 'https://deluge-torrent.org/test_port.php?port=%s' % port
|
url = 'https://deluge-torrent.org/test_port.php?port=%s' % port
|
||||||
@ -1238,18 +1225,17 @@ class Core(component.Component):
|
|||||||
return d
|
return d
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def get_free_space(self, path=None):
|
def get_free_space(self, path: str = None) -> int:
|
||||||
"""
|
"""Returns the number of free bytes at path
|
||||||
Returns the number of free bytes at path
|
|
||||||
|
|
||||||
:param path: the path to check free space at, if None, use the default download location
|
Args:
|
||||||
:type path: string
|
path: the path to check free space at, if None, use the default download location
|
||||||
|
|
||||||
:returns: the number of free bytes at path
|
Returns:
|
||||||
:rtype: int
|
the number of free bytes at path
|
||||||
|
|
||||||
:raises InvalidPathError: if the path is invalid
|
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
InvalidPathError: if the path is invalid
|
||||||
"""
|
"""
|
||||||
if not path:
|
if not path:
|
||||||
path = self.config['download_location']
|
path = self.config['download_location']
|
||||||
@ -1262,46 +1248,40 @@ class Core(component.Component):
|
|||||||
self.external_ip = external_ip
|
self.external_ip = external_ip
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def get_external_ip(self):
|
def get_external_ip(self) -> str:
|
||||||
"""
|
"""Returns the external IP address received from libtorrent."""
|
||||||
Returns the external IP address received from libtorrent.
|
|
||||||
"""
|
|
||||||
return self.external_ip
|
return self.external_ip
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def get_libtorrent_version(self):
|
def get_libtorrent_version(self) -> str:
|
||||||
"""
|
"""Returns the libtorrent version.
|
||||||
Returns the libtorrent version.
|
|
||||||
|
|
||||||
:returns: the version
|
|
||||||
:rtype: string
|
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
the version
|
||||||
"""
|
"""
|
||||||
return LT_VERSION
|
return LT_VERSION
|
||||||
|
|
||||||
@export
|
@export
|
||||||
def get_completion_paths(self, args):
|
def get_completion_paths(self, args: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""
|
"""Returns the available path completions for the input value."""
|
||||||
Returns the available path completions for the input value.
|
|
||||||
"""
|
|
||||||
return path_chooser_common.get_completion_paths(args)
|
return path_chooser_common.get_completion_paths(args)
|
||||||
|
|
||||||
@export(AUTH_LEVEL_ADMIN)
|
@export(AUTH_LEVEL_ADMIN)
|
||||||
def get_known_accounts(self):
|
def get_known_accounts(self) -> List[Dict[str, Any]]:
|
||||||
return self.authmanager.get_known_accounts()
|
return self.authmanager.get_known_accounts()
|
||||||
|
|
||||||
@export(AUTH_LEVEL_NONE)
|
@export(AUTH_LEVEL_NONE)
|
||||||
def get_auth_levels_mappings(self):
|
def get_auth_levels_mappings(self) -> Tuple[Dict[str, int], Dict[int, str]]:
|
||||||
return (AUTH_LEVELS_MAPPING, AUTH_LEVELS_MAPPING_REVERSE)
|
return (AUTH_LEVELS_MAPPING, AUTH_LEVELS_MAPPING_REVERSE)
|
||||||
|
|
||||||
@export(AUTH_LEVEL_ADMIN)
|
@export(AUTH_LEVEL_ADMIN)
|
||||||
def create_account(self, username, password, authlevel):
|
def create_account(self, username: str, password: str, authlevel: str) -> bool:
|
||||||
return self.authmanager.create_account(username, password, authlevel)
|
return self.authmanager.create_account(username, password, authlevel)
|
||||||
|
|
||||||
@export(AUTH_LEVEL_ADMIN)
|
@export(AUTH_LEVEL_ADMIN)
|
||||||
def update_account(self, username, password, authlevel):
|
def update_account(self, username: str, password: str, authlevel: str) -> bool:
|
||||||
return self.authmanager.update_account(username, password, authlevel)
|
return self.authmanager.update_account(username, password, authlevel)
|
||||||
|
|
||||||
@export(AUTH_LEVEL_ADMIN)
|
@export(AUTH_LEVEL_ADMIN)
|
||||||
def remove_account(self, username):
|
def remove_account(self, username: str) -> bool:
|
||||||
return self.authmanager.remove_account(username)
|
return self.authmanager.remove_account(username)
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
|
||||||
#
|
#
|
||||||
@ -8,8 +7,6 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
"""The Deluge daemon"""
|
"""The Deluge daemon"""
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import socket
|
import socket
|
||||||
@ -44,8 +41,8 @@ def is_daemon_running(pid_file):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
with open(pid_file) as _file:
|
with open(pid_file) as _file:
|
||||||
pid, port = [int(x) for x in _file.readline().strip().split(';')]
|
pid, port = (int(x) for x in _file.readline().strip().split(';'))
|
||||||
except (EnvironmentError, ValueError):
|
except (OSError, ValueError):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if is_process_running(pid):
|
if is_process_running(pid):
|
||||||
@ -53,7 +50,7 @@ def is_daemon_running(pid_file):
|
|||||||
_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
try:
|
try:
|
||||||
_socket.connect(('127.0.0.1', port))
|
_socket.connect(('127.0.0.1', port))
|
||||||
except socket.error:
|
except OSError:
|
||||||
# Can't connect, so pid is not a deluged process.
|
# Can't connect, so pid is not a deluged process.
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
@ -62,7 +59,7 @@ def is_daemon_running(pid_file):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class Daemon(object):
|
class Daemon:
|
||||||
"""The Deluge Daemon class"""
|
"""The Deluge Daemon class"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -156,7 +153,7 @@ class Daemon(object):
|
|||||||
pid = os.getpid()
|
pid = os.getpid()
|
||||||
log.debug('Storing pid %s & port %s in: %s', pid, self.port, self.pid_file)
|
log.debug('Storing pid %s & port %s in: %s', pid, self.port, self.pid_file)
|
||||||
with open(self.pid_file, 'w') as _file:
|
with open(self.pid_file, 'w') as _file:
|
||||||
_file.write('%s;%s\n' % (pid, self.port))
|
_file.write(f'{pid};{self.port}\n')
|
||||||
|
|
||||||
component.start()
|
component.start()
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
|
||||||
# Copyright (C) 2010 Pedro Algarvio <pedro@algarvio.me>
|
# Copyright (C) 2010 Pedro Algarvio <pedro@algarvio.me>
|
||||||
@ -7,8 +6,6 @@
|
|||||||
# the additional special exception to link portions of this program with the OpenSSL library.
|
# the additional special exception to link portions of this program with the OpenSSL library.
|
||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
from __future__ import print_function, unicode_literals
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from logging import DEBUG, FileHandler, getLogger
|
from logging import DEBUG, FileHandler, getLogger
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
||||||
#
|
#
|
||||||
@ -7,8 +6,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import deluge.component as component
|
import deluge.component as component
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
||||||
#
|
#
|
||||||
@ -7,12 +6,8 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from six import string_types
|
|
||||||
|
|
||||||
import deluge.component as component
|
import deluge.component as component
|
||||||
from deluge.common import TORRENT_STATE
|
from deluge.common import TORRENT_STATE
|
||||||
|
|
||||||
@ -136,7 +131,7 @@ class FilterManager(component.Component):
|
|||||||
|
|
||||||
# Sanitize input: filter-value must be a list of strings
|
# Sanitize input: filter-value must be a list of strings
|
||||||
for key, value in filter_dict.items():
|
for key, value in filter_dict.items():
|
||||||
if isinstance(value, string_types):
|
if isinstance(value, str):
|
||||||
filter_dict[key] = [value]
|
filter_dict[key] = [value]
|
||||||
|
|
||||||
# Optimized filter for id
|
# Optimized filter for id
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
|
||||||
#
|
#
|
||||||
@ -9,8 +8,6 @@
|
|||||||
|
|
||||||
|
|
||||||
"""PluginManager for Core"""
|
"""PluginManager for Core"""
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2008-2010 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2008-2010 Andrew Resch <andrewresch@gmail.com>
|
||||||
#
|
#
|
||||||
@ -8,13 +7,13 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import random
|
import random
|
||||||
import threading
|
import threading
|
||||||
|
from urllib.parse import quote_plus
|
||||||
|
from urllib.request import urlopen
|
||||||
|
|
||||||
from twisted.internet.task import LoopingCall
|
from twisted.internet.task import LoopingCall
|
||||||
|
|
||||||
@ -24,17 +23,14 @@ import deluge.configmanager
|
|||||||
from deluge._libtorrent import lt
|
from deluge._libtorrent import lt
|
||||||
from deluge.event import ConfigValueChangedEvent
|
from deluge.event import ConfigValueChangedEvent
|
||||||
|
|
||||||
|
GeoIP = None
|
||||||
try:
|
try:
|
||||||
import GeoIP
|
from GeoIP import GeoIP
|
||||||
except ImportError:
|
except ImportError:
|
||||||
GeoIP = None
|
try:
|
||||||
|
from pygeoip import GeoIP
|
||||||
try:
|
except ImportError:
|
||||||
from urllib.parse import quote_plus
|
pass
|
||||||
from urllib.request import urlopen
|
|
||||||
except ImportError:
|
|
||||||
from urllib import quote_plus
|
|
||||||
from urllib2 import urlopen
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -202,7 +198,7 @@ class PreferencesManager(component.Component):
|
|||||||
self.__set_listen_on()
|
self.__set_listen_on()
|
||||||
|
|
||||||
def __set_listen_on(self):
|
def __set_listen_on(self):
|
||||||
""" Set the ports and interface address to listen for incoming connections on."""
|
"""Set the ports and interface address to listen for incoming connections on."""
|
||||||
if self.config['random_port']:
|
if self.config['random_port']:
|
||||||
if not self.config['listen_random_port']:
|
if not self.config['listen_random_port']:
|
||||||
self.config['listen_random_port'] = random.randrange(49152, 65525)
|
self.config['listen_random_port'] = random.randrange(49152, 65525)
|
||||||
@ -225,7 +221,7 @@ class PreferencesManager(component.Component):
|
|||||||
self.config['listen_use_sys_port'],
|
self.config['listen_use_sys_port'],
|
||||||
)
|
)
|
||||||
interfaces = [
|
interfaces = [
|
||||||
'%s:%s' % (interface, port)
|
f'{interface}:{port}'
|
||||||
for port in range(listen_ports[0], listen_ports[1] + 1)
|
for port in range(listen_ports[0], listen_ports[1] + 1)
|
||||||
]
|
]
|
||||||
self.core.apply_session_settings(
|
self.core.apply_session_settings(
|
||||||
@ -400,7 +396,7 @@ class PreferencesManager(component.Component):
|
|||||||
+ quote_plus(':'.join(self.config['enabled_plugins']))
|
+ quote_plus(':'.join(self.config['enabled_plugins']))
|
||||||
)
|
)
|
||||||
urlopen(url)
|
urlopen(url)
|
||||||
except IOError as ex:
|
except OSError as ex:
|
||||||
log.debug('Network error while trying to send info: %s', ex)
|
log.debug('Network error while trying to send info: %s', ex)
|
||||||
else:
|
else:
|
||||||
self.config['info_sent'] = now
|
self.config['info_sent'] = now
|
||||||
@ -464,11 +460,9 @@ class PreferencesManager(component.Component):
|
|||||||
# Load the GeoIP DB for country look-ups if available
|
# Load the GeoIP DB for country look-ups if available
|
||||||
if os.path.exists(geoipdb_path):
|
if os.path.exists(geoipdb_path):
|
||||||
try:
|
try:
|
||||||
self.core.geoip_instance = GeoIP.open(
|
self.core.geoip_instance = GeoIP(geoipdb_path, 0)
|
||||||
geoipdb_path, GeoIP.GEOIP_STANDARD
|
except Exception as ex:
|
||||||
)
|
log.warning('GeoIP Unavailable: %s', ex)
|
||||||
except AttributeError:
|
|
||||||
log.warning('GeoIP Unavailable')
|
|
||||||
else:
|
else:
|
||||||
log.warning('Unable to find GeoIP database file: %s', geoipdb_path)
|
log.warning('Unable to find GeoIP database file: %s', geoipdb_path)
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2008,2009 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2008,2009 Andrew Resch <andrewresch@gmail.com>
|
||||||
#
|
#
|
||||||
@ -8,17 +7,14 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
"""RPCServer Module"""
|
"""RPCServer Module"""
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import stat
|
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from types import FunctionType
|
from types import FunctionType
|
||||||
|
from typing import Callable, TypeVar, overload
|
||||||
|
|
||||||
from OpenSSL import crypto
|
|
||||||
from twisted.internet import defer, reactor
|
from twisted.internet import defer, reactor
|
||||||
from twisted.internet.protocol import Factory, connectionDone
|
from twisted.internet.protocol import Factory, connectionDone
|
||||||
|
|
||||||
@ -29,7 +25,7 @@ from deluge.core.authmanager import (
|
|||||||
AUTH_LEVEL_DEFAULT,
|
AUTH_LEVEL_DEFAULT,
|
||||||
AUTH_LEVEL_NONE,
|
AUTH_LEVEL_NONE,
|
||||||
)
|
)
|
||||||
from deluge.crypto_utils import get_context_factory
|
from deluge.crypto_utils import check_ssl_keys, get_context_factory
|
||||||
from deluge.error import (
|
from deluge.error import (
|
||||||
DelugeError,
|
DelugeError,
|
||||||
IncompatibleClient,
|
IncompatibleClient,
|
||||||
@ -46,6 +42,18 @@ RPC_EVENT = 3
|
|||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
TCallable = TypeVar('TCallable', bound=Callable)
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def export(func: TCallable) -> TCallable:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def export(auth_level: int) -> Callable[[TCallable], TCallable]:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
def export(auth_level=AUTH_LEVEL_DEFAULT):
|
def export(auth_level=AUTH_LEVEL_DEFAULT):
|
||||||
"""
|
"""
|
||||||
@ -69,7 +77,7 @@ def export(auth_level=AUTH_LEVEL_DEFAULT):
|
|||||||
if func.__doc__:
|
if func.__doc__:
|
||||||
if func.__doc__.endswith(' '):
|
if func.__doc__.endswith(' '):
|
||||||
indent = func.__doc__.split('\n')[-1]
|
indent = func.__doc__.split('\n')[-1]
|
||||||
func.__doc__ += '\n{}'.format(indent)
|
func.__doc__ += f'\n{indent}'
|
||||||
else:
|
else:
|
||||||
func.__doc__ += '\n\n'
|
func.__doc__ += '\n\n'
|
||||||
func.__doc__ += rpc_text
|
func.__doc__ += rpc_text
|
||||||
@ -114,7 +122,7 @@ def format_request(call):
|
|||||||
|
|
||||||
class DelugeRPCProtocol(DelugeTransferProtocol):
|
class DelugeRPCProtocol(DelugeTransferProtocol):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(DelugeRPCProtocol, self).__init__()
|
super().__init__()
|
||||||
# namedtuple subclass with auth_level, username for the connected session.
|
# namedtuple subclass with auth_level, username for the connected session.
|
||||||
self.AuthLevel = namedtuple('SessionAuthlevel', 'auth_level, username')
|
self.AuthLevel = namedtuple('SessionAuthlevel', 'auth_level, username')
|
||||||
|
|
||||||
@ -588,59 +596,3 @@ class RPCServer(component.Component):
|
|||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.factory.state = 'stopping'
|
self.factory.state = 'stopping'
|
||||||
|
|
||||||
|
|
||||||
def check_ssl_keys():
|
|
||||||
"""
|
|
||||||
Check for SSL cert/key and create them if necessary
|
|
||||||
"""
|
|
||||||
ssl_dir = deluge.configmanager.get_config_dir('ssl')
|
|
||||||
if not os.path.exists(ssl_dir):
|
|
||||||
# The ssl folder doesn't exist so we need to create it
|
|
||||||
os.makedirs(ssl_dir)
|
|
||||||
generate_ssl_keys()
|
|
||||||
else:
|
|
||||||
for f in ('daemon.pkey', 'daemon.cert'):
|
|
||||||
if not os.path.exists(os.path.join(ssl_dir, f)):
|
|
||||||
generate_ssl_keys()
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
def generate_ssl_keys():
|
|
||||||
"""
|
|
||||||
This method generates a new SSL key/cert.
|
|
||||||
"""
|
|
||||||
from deluge.common import PY2
|
|
||||||
|
|
||||||
digest = 'sha256' if not PY2 else b'sha256'
|
|
||||||
|
|
||||||
# Generate key pair
|
|
||||||
pkey = crypto.PKey()
|
|
||||||
pkey.generate_key(crypto.TYPE_RSA, 2048)
|
|
||||||
|
|
||||||
# Generate cert request
|
|
||||||
req = crypto.X509Req()
|
|
||||||
subj = req.get_subject()
|
|
||||||
setattr(subj, 'CN', 'Deluge Daemon')
|
|
||||||
req.set_pubkey(pkey)
|
|
||||||
req.sign(pkey, digest)
|
|
||||||
|
|
||||||
# Generate certificate
|
|
||||||
cert = crypto.X509()
|
|
||||||
cert.set_serial_number(0)
|
|
||||||
cert.gmtime_adj_notBefore(0)
|
|
||||||
cert.gmtime_adj_notAfter(60 * 60 * 24 * 365 * 3) # Three Years
|
|
||||||
cert.set_issuer(req.get_subject())
|
|
||||||
cert.set_subject(req.get_subject())
|
|
||||||
cert.set_pubkey(req.get_pubkey())
|
|
||||||
cert.sign(pkey, digest)
|
|
||||||
|
|
||||||
# Write out files
|
|
||||||
ssl_dir = deluge.configmanager.get_config_dir('ssl')
|
|
||||||
with open(os.path.join(ssl_dir, 'daemon.pkey'), 'wb') as _file:
|
|
||||||
_file.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
|
|
||||||
with open(os.path.join(ssl_dir, 'daemon.cert'), 'wb') as _file:
|
|
||||||
_file.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
|
|
||||||
# Make the files only readable by this user
|
|
||||||
for f in ('daemon.pkey', 'daemon.cert'):
|
|
||||||
os.chmod(os.path.join(ssl_dir, f), stat.S_IREAD | stat.S_IWRITE)
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
|
||||||
#
|
#
|
||||||
@ -14,11 +13,12 @@ Attributes:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import division, unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import socket
|
import socket
|
||||||
|
import time
|
||||||
|
from typing import Optional
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from twisted.internet.defer import Deferred, DeferredList
|
from twisted.internet.defer import Deferred, DeferredList
|
||||||
|
|
||||||
@ -34,18 +34,6 @@ from deluge.event import (
|
|||||||
TorrentTrackerStatusEvent,
|
TorrentTrackerStatusEvent,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
except ImportError:
|
|
||||||
# PY2 fallback
|
|
||||||
from urlparse import urlparse # pylint: disable=ungrouped-imports
|
|
||||||
|
|
||||||
try:
|
|
||||||
from future_builtins import zip
|
|
||||||
except ImportError:
|
|
||||||
# Ignore on Py3.
|
|
||||||
pass
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
LT_TORRENT_STATE_MAP = {
|
LT_TORRENT_STATE_MAP = {
|
||||||
@ -94,7 +82,7 @@ def convert_lt_files(files):
|
|||||||
"""Indexes and decodes files from libtorrent get_files().
|
"""Indexes and decodes files from libtorrent get_files().
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
files (list): The libtorrent torrent files.
|
files (file_storage): The libtorrent torrent files.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list of dict: The files.
|
list of dict: The files.
|
||||||
@ -109,18 +97,18 @@ def convert_lt_files(files):
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
filelist = []
|
filelist = []
|
||||||
for index, _file in enumerate(files):
|
for index in range(files.num_files()):
|
||||||
try:
|
try:
|
||||||
file_path = _file.path.decode('utf8')
|
file_path = files.file_path(index).decode('utf8')
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
file_path = _file.path
|
file_path = files.file_path(index)
|
||||||
|
|
||||||
filelist.append(
|
filelist.append(
|
||||||
{
|
{
|
||||||
'index': index,
|
'index': index,
|
||||||
'path': file_path.replace('\\', '/'),
|
'path': file_path.replace('\\', '/'),
|
||||||
'size': _file.size,
|
'size': files.file_size(index),
|
||||||
'offset': _file.offset,
|
'offset': files.file_offset(index),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -161,7 +149,7 @@ class TorrentOptions(dict):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(TorrentOptions, self).__init__()
|
super().__init__()
|
||||||
config = ConfigManager('core.conf').config
|
config = ConfigManager('core.conf').config
|
||||||
options_conf_map = {
|
options_conf_map = {
|
||||||
'add_paused': 'add_paused',
|
'add_paused': 'add_paused',
|
||||||
@ -191,14 +179,14 @@ class TorrentOptions(dict):
|
|||||||
self['seed_mode'] = False
|
self['seed_mode'] = False
|
||||||
|
|
||||||
|
|
||||||
class TorrentError(object):
|
class TorrentError:
|
||||||
def __init__(self, error_message, was_paused=False, restart_to_resume=False):
|
def __init__(self, error_message, was_paused=False, restart_to_resume=False):
|
||||||
self.error_message = error_message
|
self.error_message = error_message
|
||||||
self.was_paused = was_paused
|
self.was_paused = was_paused
|
||||||
self.restart_to_resume = restart_to_resume
|
self.restart_to_resume = restart_to_resume
|
||||||
|
|
||||||
|
|
||||||
class Torrent(object):
|
class Torrent:
|
||||||
"""Torrent holds information about torrents added to the libtorrent session.
|
"""Torrent holds information about torrents added to the libtorrent session.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -248,9 +236,10 @@ class Torrent(object):
|
|||||||
self.handle = handle
|
self.handle = handle
|
||||||
|
|
||||||
self.magnet = magnet
|
self.magnet = magnet
|
||||||
self.status = self.handle.status()
|
self._status: Optional['lt.torrent_status'] = None
|
||||||
|
self._status_last_update: float = 0.0
|
||||||
|
|
||||||
self.torrent_info = self.handle.get_torrent_info()
|
self.torrent_info = self.handle.torrent_file()
|
||||||
self.has_metadata = self.status.has_metadata
|
self.has_metadata = self.status.has_metadata
|
||||||
|
|
||||||
self.options = TorrentOptions()
|
self.options = TorrentOptions()
|
||||||
@ -281,7 +270,6 @@ class Torrent(object):
|
|||||||
self.prev_status = {}
|
self.prev_status = {}
|
||||||
self.waiting_on_folder_rename = []
|
self.waiting_on_folder_rename = []
|
||||||
|
|
||||||
self.update_status(self.handle.status())
|
|
||||||
self._create_status_funcs()
|
self._create_status_funcs()
|
||||||
self.set_options(self.options)
|
self.set_options(self.options)
|
||||||
self.update_state()
|
self.update_state()
|
||||||
@ -289,6 +277,18 @@ class Torrent(object):
|
|||||||
if log.isEnabledFor(logging.DEBUG):
|
if log.isEnabledFor(logging.DEBUG):
|
||||||
log.debug('Torrent object created.')
|
log.debug('Torrent object created.')
|
||||||
|
|
||||||
|
def _set_handle_flags(self, flag: lt.torrent_flags, set_flag: bool):
|
||||||
|
"""set or unset a flag to the lt handle
|
||||||
|
|
||||||
|
Args:
|
||||||
|
flag (lt.torrent_flags): the flag to set/unset
|
||||||
|
set_flag (bool): True for setting the flag, False for unsetting it
|
||||||
|
"""
|
||||||
|
if set_flag:
|
||||||
|
self.handle.set_flags(flag)
|
||||||
|
else:
|
||||||
|
self.handle.unset_flags(flag)
|
||||||
|
|
||||||
def on_metadata_received(self):
|
def on_metadata_received(self):
|
||||||
"""Process the metadata received alert for this torrent"""
|
"""Process the metadata received alert for this torrent"""
|
||||||
self.has_metadata = True
|
self.has_metadata = True
|
||||||
@ -373,7 +373,7 @@ class Torrent(object):
|
|||||||
"""Sets maximum download speed for this torrent.
|
"""Sets maximum download speed for this torrent.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
m_up_speed (float): Maximum download speed in KiB/s.
|
m_down_speed (float): Maximum download speed in KiB/s.
|
||||||
"""
|
"""
|
||||||
self.options['max_download_speed'] = m_down_speed
|
self.options['max_download_speed'] = m_down_speed
|
||||||
if m_down_speed < 0:
|
if m_down_speed < 0:
|
||||||
@ -405,7 +405,7 @@ class Torrent(object):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# A list of priorities for each piece in the torrent
|
# A list of priorities for each piece in the torrent
|
||||||
priorities = self.handle.piece_priorities()
|
priorities = self.handle.get_piece_priorities()
|
||||||
|
|
||||||
def get_file_piece(idx, byte_offset):
|
def get_file_piece(idx, byte_offset):
|
||||||
return self.torrent_info.map_file(idx, byte_offset, 0).piece
|
return self.torrent_info.map_file(idx, byte_offset, 0).piece
|
||||||
@ -438,7 +438,10 @@ class Torrent(object):
|
|||||||
sequential (bool): Enable sequential downloading.
|
sequential (bool): Enable sequential downloading.
|
||||||
"""
|
"""
|
||||||
self.options['sequential_download'] = sequential
|
self.options['sequential_download'] = sequential
|
||||||
self.handle.set_sequential_download(sequential)
|
self._set_handle_flags(
|
||||||
|
flag=lt.torrent_flags.sequential_download,
|
||||||
|
set_flag=sequential,
|
||||||
|
)
|
||||||
|
|
||||||
def set_auto_managed(self, auto_managed):
|
def set_auto_managed(self, auto_managed):
|
||||||
"""Set auto managed mode, i.e. will be started or queued automatically.
|
"""Set auto managed mode, i.e. will be started or queued automatically.
|
||||||
@ -448,7 +451,10 @@ class Torrent(object):
|
|||||||
"""
|
"""
|
||||||
self.options['auto_managed'] = auto_managed
|
self.options['auto_managed'] = auto_managed
|
||||||
if not (self.status.paused and not self.status.auto_managed):
|
if not (self.status.paused and not self.status.auto_managed):
|
||||||
self.handle.auto_managed(auto_managed)
|
self._set_handle_flags(
|
||||||
|
flag=lt.torrent_flags.auto_managed,
|
||||||
|
set_flag=auto_managed,
|
||||||
|
)
|
||||||
self.update_state()
|
self.update_state()
|
||||||
|
|
||||||
def set_super_seeding(self, super_seeding):
|
def set_super_seeding(self, super_seeding):
|
||||||
@ -458,7 +464,10 @@ class Torrent(object):
|
|||||||
super_seeding (bool): Enable super seeding.
|
super_seeding (bool): Enable super seeding.
|
||||||
"""
|
"""
|
||||||
self.options['super_seeding'] = super_seeding
|
self.options['super_seeding'] = super_seeding
|
||||||
self.handle.super_seeding(super_seeding)
|
self._set_handle_flags(
|
||||||
|
flag=lt.torrent_flags.super_seeding,
|
||||||
|
set_flag=super_seeding,
|
||||||
|
)
|
||||||
|
|
||||||
def set_stop_ratio(self, stop_ratio):
|
def set_stop_ratio(self, stop_ratio):
|
||||||
"""The seeding ratio to stop (or remove) the torrent at.
|
"""The seeding ratio to stop (or remove) the torrent at.
|
||||||
@ -519,7 +528,7 @@ class Torrent(object):
|
|||||||
self.handle.prioritize_files(file_priorities)
|
self.handle.prioritize_files(file_priorities)
|
||||||
else:
|
else:
|
||||||
log.debug('Unable to set new file priorities.')
|
log.debug('Unable to set new file priorities.')
|
||||||
file_priorities = self.handle.file_priorities()
|
file_priorities = self.handle.get_file_priorities()
|
||||||
|
|
||||||
if 0 in self.options['file_priorities']:
|
if 0 in self.options['file_priorities']:
|
||||||
# Previously marked a file 'skip' so check for any 0's now >0.
|
# Previously marked a file 'skip' so check for any 0's now >0.
|
||||||
@ -569,7 +578,7 @@ class Torrent(object):
|
|||||||
trackers (list of dicts): A list of trackers.
|
trackers (list of dicts): A list of trackers.
|
||||||
"""
|
"""
|
||||||
if trackers is None:
|
if trackers is None:
|
||||||
self.trackers = [tracker for tracker in self.handle.trackers()]
|
self.trackers = list(self.handle.trackers())
|
||||||
self.tracker_host = None
|
self.tracker_host = None
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -634,7 +643,7 @@ class Torrent(object):
|
|||||||
|
|
||||||
def update_state(self):
|
def update_state(self):
|
||||||
"""Updates the state, based on libtorrent's torrent state"""
|
"""Updates the state, based on libtorrent's torrent state"""
|
||||||
status = self.handle.status()
|
status = self.get_lt_status()
|
||||||
session_paused = component.get('Core').session.is_paused()
|
session_paused = component.get('Core').session.is_paused()
|
||||||
old_state = self.state
|
old_state = self.state
|
||||||
self.set_status_message()
|
self.set_status_message()
|
||||||
@ -646,7 +655,10 @@ class Torrent(object):
|
|||||||
elif status_error:
|
elif status_error:
|
||||||
self.state = 'Error'
|
self.state = 'Error'
|
||||||
# auto-manage status will be reverted upon resuming.
|
# auto-manage status will be reverted upon resuming.
|
||||||
self.handle.auto_managed(False)
|
self._set_handle_flags(
|
||||||
|
flag=lt.torrent_flags.auto_managed,
|
||||||
|
set_flag=False,
|
||||||
|
)
|
||||||
self.set_status_message(decode_bytes(status_error))
|
self.set_status_message(decode_bytes(status_error))
|
||||||
elif status.moving_storage:
|
elif status.moving_storage:
|
||||||
self.state = 'Moving'
|
self.state = 'Moving'
|
||||||
@ -699,8 +711,11 @@ class Torrent(object):
|
|||||||
restart_to_resume (bool, optional): Prevent resuming clearing the error, only restarting
|
restart_to_resume (bool, optional): Prevent resuming clearing the error, only restarting
|
||||||
session can resume.
|
session can resume.
|
||||||
"""
|
"""
|
||||||
status = self.handle.status()
|
status = self.get_lt_status()
|
||||||
self.handle.auto_managed(False)
|
self._set_handle_flags(
|
||||||
|
flag=lt.torrent_flags.auto_managed,
|
||||||
|
set_flag=False,
|
||||||
|
)
|
||||||
self.forced_error = TorrentError(message, status.paused, restart_to_resume)
|
self.forced_error = TorrentError(message, status.paused, restart_to_resume)
|
||||||
if not status.paused:
|
if not status.paused:
|
||||||
self.handle.pause()
|
self.handle.pause()
|
||||||
@ -714,7 +729,10 @@ class Torrent(object):
|
|||||||
log.error('Restart deluge to clear this torrent error')
|
log.error('Restart deluge to clear this torrent error')
|
||||||
|
|
||||||
if not self.forced_error.was_paused and self.options['auto_managed']:
|
if not self.forced_error.was_paused and self.options['auto_managed']:
|
||||||
self.handle.auto_managed(True)
|
self._set_handle_flags(
|
||||||
|
flag=lt.torrent_flags.auto_managed,
|
||||||
|
set_flag=True,
|
||||||
|
)
|
||||||
self.forced_error = None
|
self.forced_error = None
|
||||||
self.set_status_message('OK')
|
self.set_status_message('OK')
|
||||||
if update_state:
|
if update_state:
|
||||||
@ -838,7 +856,7 @@ class Torrent(object):
|
|||||||
'client': client,
|
'client': client,
|
||||||
'country': country,
|
'country': country,
|
||||||
'down_speed': peer.payload_down_speed,
|
'down_speed': peer.payload_down_speed,
|
||||||
'ip': '%s:%s' % (peer.ip[0], peer.ip[1]),
|
'ip': f'{peer.ip[0]}:{peer.ip[1]}',
|
||||||
'progress': peer.progress,
|
'progress': peer.progress,
|
||||||
'seed': peer.flags & peer.seed,
|
'seed': peer.flags & peer.seed,
|
||||||
'up_speed': peer.payload_up_speed,
|
'up_speed': peer.payload_up_speed,
|
||||||
@ -857,7 +875,7 @@ class Torrent(object):
|
|||||||
|
|
||||||
def get_file_priorities(self):
|
def get_file_priorities(self):
|
||||||
"""Return the file priorities"""
|
"""Return the file priorities"""
|
||||||
if not self.handle.has_metadata():
|
if not self.handle.status().has_metadata:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
if not self.options['file_priorities']:
|
if not self.options['file_priorities']:
|
||||||
@ -910,7 +928,7 @@ class Torrent(object):
|
|||||||
# Check if hostname is an IP address and just return it if that's the case
|
# Check if hostname is an IP address and just return it if that's the case
|
||||||
try:
|
try:
|
||||||
socket.inet_aton(host)
|
socket.inet_aton(host)
|
||||||
except socket.error:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
# This is an IP address because an exception wasn't raised
|
# This is an IP address because an exception wasn't raised
|
||||||
@ -946,10 +964,10 @@ class Torrent(object):
|
|||||||
|
|
||||||
if self.has_metadata:
|
if self.has_metadata:
|
||||||
# Use the top-level folder as torrent name.
|
# Use the top-level folder as torrent name.
|
||||||
filename = decode_bytes(self.torrent_info.file_at(0).path)
|
filename = decode_bytes(self.torrent_info.files().file_path(0))
|
||||||
name = filename.replace('\\', '/', 1).split('/', 1)[0]
|
name = filename.replace('\\', '/', 1).split('/', 1)[0]
|
||||||
else:
|
else:
|
||||||
name = decode_bytes(self.handle.name())
|
name = decode_bytes(self.handle.status().name)
|
||||||
|
|
||||||
if not name:
|
if not name:
|
||||||
name = self.torrent_id
|
name = self.torrent_id
|
||||||
@ -1008,7 +1026,7 @@ class Torrent(object):
|
|||||||
dict: a dictionary of the status keys and their values
|
dict: a dictionary of the status keys and their values
|
||||||
"""
|
"""
|
||||||
if update:
|
if update:
|
||||||
self.update_status(self.handle.status())
|
self.get_lt_status()
|
||||||
|
|
||||||
if all_keys:
|
if all_keys:
|
||||||
keys = list(self.status_funcs)
|
keys = list(self.status_funcs)
|
||||||
@ -1038,13 +1056,35 @@ class Torrent(object):
|
|||||||
|
|
||||||
return status_dict
|
return status_dict
|
||||||
|
|
||||||
def update_status(self, status):
|
def get_lt_status(self) -> 'lt.torrent_status':
|
||||||
|
"""Get the torrent status fresh, not from cache.
|
||||||
|
|
||||||
|
This should be used when a guaranteed fresh status is needed rather than
|
||||||
|
`torrent.handle.status()` because it will update the cache as well.
|
||||||
|
"""
|
||||||
|
self.status = self.handle.status()
|
||||||
|
return self.status
|
||||||
|
|
||||||
|
@property
|
||||||
|
def status(self) -> 'lt.torrent_status':
|
||||||
|
"""Cached copy of the libtorrent status for this torrent.
|
||||||
|
|
||||||
|
If it has not been updated within the last five seconds, it will be
|
||||||
|
automatically refreshed.
|
||||||
|
"""
|
||||||
|
if self._status_last_update < (time.time() - 5):
|
||||||
|
self.status = self.handle.status()
|
||||||
|
return self._status
|
||||||
|
|
||||||
|
@status.setter
|
||||||
|
def status(self, status: 'lt.torrent_status') -> None:
|
||||||
"""Updates the cached status.
|
"""Updates the cached status.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
status (libtorrent.torrent_status): a libtorrent torrent status
|
status: a libtorrent torrent status
|
||||||
"""
|
"""
|
||||||
self.status = status
|
self._status = status
|
||||||
|
self._status_last_update = time.time()
|
||||||
|
|
||||||
def _create_status_funcs(self):
|
def _create_status_funcs(self):
|
||||||
"""Creates the functions for getting torrent status"""
|
"""Creates the functions for getting torrent status"""
|
||||||
@ -1166,7 +1206,10 @@ class Torrent(object):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
# Turn off auto-management so the torrent will not be unpaused by lt queueing
|
# Turn off auto-management so the torrent will not be unpaused by lt queueing
|
||||||
self.handle.auto_managed(False)
|
self._set_handle_flags(
|
||||||
|
flag=lt.torrent_flags.auto_managed,
|
||||||
|
set_flag=False,
|
||||||
|
)
|
||||||
if self.state == 'Error':
|
if self.state == 'Error':
|
||||||
log.debug('Unable to pause torrent while in Error state')
|
log.debug('Unable to pause torrent while in Error state')
|
||||||
elif self.status.paused:
|
elif self.status.paused:
|
||||||
@ -1201,7 +1244,10 @@ class Torrent(object):
|
|||||||
else:
|
else:
|
||||||
# Check if torrent was originally being auto-managed.
|
# Check if torrent was originally being auto-managed.
|
||||||
if self.options['auto_managed']:
|
if self.options['auto_managed']:
|
||||||
self.handle.auto_managed(True)
|
self._set_handle_flags(
|
||||||
|
flag=lt.torrent_flags.auto_managed,
|
||||||
|
set_flag=True,
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
self.handle.resume()
|
self.handle.resume()
|
||||||
except RuntimeError as ex:
|
except RuntimeError as ex:
|
||||||
@ -1305,7 +1351,7 @@ class Torrent(object):
|
|||||||
try:
|
try:
|
||||||
with open(filepath, 'wb') as save_file:
|
with open(filepath, 'wb') as save_file:
|
||||||
save_file.write(filedump)
|
save_file.write(filedump)
|
||||||
except IOError as ex:
|
except OSError as ex:
|
||||||
log.error('Unable to save torrent file to: %s', ex)
|
log.error('Unable to save torrent file to: %s', ex)
|
||||||
|
|
||||||
filepath = os.path.join(get_config_dir(), 'state', self.torrent_id + '.torrent')
|
filepath = os.path.join(get_config_dir(), 'state', self.torrent_id + '.torrent')
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
|
||||||
#
|
#
|
||||||
@ -8,25 +7,23 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
"""TorrentManager handles Torrent objects"""
|
"""TorrentManager handles Torrent objects"""
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import operator
|
import operator
|
||||||
import os
|
import os
|
||||||
|
import pickle
|
||||||
import time
|
import time
|
||||||
from collections import namedtuple
|
from base64 import b64encode
|
||||||
from tempfile import gettempdir
|
from tempfile import gettempdir
|
||||||
|
from typing import Dict, List, NamedTuple, Tuple
|
||||||
|
|
||||||
import six.moves.cPickle as pickle # noqa: N813
|
from twisted.internet import defer, reactor, threads
|
||||||
from twisted.internet import defer, error, reactor, threads
|
|
||||||
from twisted.internet.defer import Deferred, DeferredList
|
from twisted.internet.defer import Deferred, DeferredList
|
||||||
from twisted.internet.task import LoopingCall
|
from twisted.internet.task import LoopingCall
|
||||||
|
|
||||||
import deluge.component as component
|
import deluge.component as component
|
||||||
from deluge._libtorrent import LT_VERSION, lt
|
from deluge._libtorrent import LT_VERSION, lt
|
||||||
from deluge.common import (
|
from deluge.common import (
|
||||||
PY2,
|
|
||||||
VersionSplit,
|
VersionSplit,
|
||||||
archive_files,
|
archive_files,
|
||||||
decode_bytes,
|
decode_bytes,
|
||||||
@ -36,6 +33,7 @@ from deluge.common import (
|
|||||||
from deluge.configmanager import ConfigManager, get_config_dir
|
from deluge.configmanager import ConfigManager, get_config_dir
|
||||||
from deluge.core.authmanager import AUTH_LEVEL_ADMIN
|
from deluge.core.authmanager import AUTH_LEVEL_ADMIN
|
||||||
from deluge.core.torrent import Torrent, TorrentOptions, sanitize_filepath
|
from deluge.core.torrent import Torrent, TorrentOptions, sanitize_filepath
|
||||||
|
from deluge.decorators import maybe_coroutine
|
||||||
from deluge.error import AddTorrentError, InvalidTorrentError
|
from deluge.error import AddTorrentError, InvalidTorrentError
|
||||||
from deluge.event import (
|
from deluge.event import (
|
||||||
ExternalIPEvent,
|
ExternalIPEvent,
|
||||||
@ -59,6 +57,11 @@ LT_DEFAULT_ADD_TORRENT_FLAGS = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PrefetchQueueItem(NamedTuple):
|
||||||
|
alert_deferred: Deferred
|
||||||
|
result_queue: List[Deferred]
|
||||||
|
|
||||||
|
|
||||||
class TorrentState: # pylint: disable=old-style-class
|
class TorrentState: # pylint: disable=old-style-class
|
||||||
"""Create a torrent state.
|
"""Create a torrent state.
|
||||||
|
|
||||||
@ -136,7 +139,8 @@ class TorrentManager(component.Component):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
callLater = reactor.callLater # noqa: N815
|
# This is used in the test to mock out timeouts
|
||||||
|
clock = reactor
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
component.Component.__init__(
|
component.Component.__init__(
|
||||||
@ -165,7 +169,7 @@ class TorrentManager(component.Component):
|
|||||||
self.is_saving_state = False
|
self.is_saving_state = False
|
||||||
self.save_resume_data_file_lock = defer.DeferredLock()
|
self.save_resume_data_file_lock = defer.DeferredLock()
|
||||||
self.torrents_loading = {}
|
self.torrents_loading = {}
|
||||||
self.prefetching_metadata = {}
|
self.prefetching_metadata: Dict[str, PrefetchQueueItem] = {}
|
||||||
|
|
||||||
# This is a map of torrent_ids to Deferreds used to track needed resume data.
|
# This is a map of torrent_ids to Deferreds used to track needed resume data.
|
||||||
# The Deferreds will be completed when resume data has been saved.
|
# The Deferreds will be completed when resume data has been saved.
|
||||||
@ -250,8 +254,8 @@ class TorrentManager(component.Component):
|
|||||||
self.save_resume_data_timer.start(190, False)
|
self.save_resume_data_timer.start(190, False)
|
||||||
self.prev_status_cleanup_loop.start(10)
|
self.prev_status_cleanup_loop.start(10)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@maybe_coroutine
|
||||||
def stop(self):
|
async def stop(self):
|
||||||
# Stop timers
|
# Stop timers
|
||||||
if self.save_state_timer.running:
|
if self.save_state_timer.running:
|
||||||
self.save_state_timer.stop()
|
self.save_state_timer.stop()
|
||||||
@ -263,11 +267,11 @@ class TorrentManager(component.Component):
|
|||||||
self.prev_status_cleanup_loop.stop()
|
self.prev_status_cleanup_loop.stop()
|
||||||
|
|
||||||
# Save state on shutdown
|
# Save state on shutdown
|
||||||
yield self.save_state()
|
await self.save_state()
|
||||||
|
|
||||||
self.session.pause()
|
self.session.pause()
|
||||||
|
|
||||||
result = yield self.save_resume_data(flush_disk_cache=True)
|
result = await self.save_resume_data(flush_disk_cache=True)
|
||||||
# Remove the temp_file to signify successfully saved state
|
# Remove the temp_file to signify successfully saved state
|
||||||
if result and os.path.isfile(self.temp_file):
|
if result and os.path.isfile(self.temp_file):
|
||||||
os.remove(self.temp_file)
|
os.remove(self.temp_file)
|
||||||
@ -281,11 +285,6 @@ class TorrentManager(component.Component):
|
|||||||
'Paused',
|
'Paused',
|
||||||
'Queued',
|
'Queued',
|
||||||
):
|
):
|
||||||
# If the global setting is set, but the per-torrent isn't...
|
|
||||||
# Just skip to the next torrent.
|
|
||||||
# This is so that a user can turn-off the stop at ratio option on a per-torrent basis
|
|
||||||
if not torrent.options['stop_at_ratio']:
|
|
||||||
continue
|
|
||||||
if (
|
if (
|
||||||
torrent.get_ratio() >= torrent.options['stop_ratio']
|
torrent.get_ratio() >= torrent.options['stop_ratio']
|
||||||
and torrent.is_finished
|
and torrent.is_finished
|
||||||
@ -293,7 +292,7 @@ class TorrentManager(component.Component):
|
|||||||
if torrent.options['remove_at_ratio']:
|
if torrent.options['remove_at_ratio']:
|
||||||
self.remove(torrent_id)
|
self.remove(torrent_id)
|
||||||
break
|
break
|
||||||
if not torrent.handle.status().paused:
|
if not torrent.status.paused:
|
||||||
torrent.pause()
|
torrent.pause()
|
||||||
|
|
||||||
def __getitem__(self, torrent_id):
|
def __getitem__(self, torrent_id):
|
||||||
@ -346,26 +345,28 @@ class TorrentManager(component.Component):
|
|||||||
else:
|
else:
|
||||||
return torrent_info
|
return torrent_info
|
||||||
|
|
||||||
def prefetch_metadata(self, magnet, timeout):
|
@maybe_coroutine
|
||||||
|
async def prefetch_metadata(self, magnet: str, timeout: int) -> Tuple[str, bytes]:
|
||||||
"""Download the metadata for a magnet URI.
|
"""Download the metadata for a magnet URI.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
magnet (str): A magnet URI to download the metadata for.
|
magnet: A magnet URI to download the metadata for.
|
||||||
timeout (int): Number of seconds to wait before canceling.
|
timeout: Number of seconds to wait before canceling.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Deferred: A tuple of (torrent_id (str), metadata (dict))
|
A tuple of (torrent_id, metadata)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
torrent_id = get_magnet_info(magnet)['info_hash']
|
torrent_id = get_magnet_info(magnet)['info_hash']
|
||||||
if torrent_id in self.prefetching_metadata:
|
if torrent_id in self.prefetching_metadata:
|
||||||
return self.prefetching_metadata[torrent_id].defer
|
d = Deferred()
|
||||||
|
self.prefetching_metadata[torrent_id].result_queue.append(d)
|
||||||
|
return await d
|
||||||
|
|
||||||
add_torrent_params = {}
|
add_torrent_params = lt.parse_magnet_uri(magnet)
|
||||||
add_torrent_params['save_path'] = gettempdir()
|
add_torrent_params.save_path = gettempdir()
|
||||||
add_torrent_params['url'] = magnet.strip().encode('utf8')
|
add_torrent_params.flags = (
|
||||||
add_torrent_params['flags'] = (
|
|
||||||
(
|
(
|
||||||
LT_DEFAULT_ADD_TORRENT_FLAGS
|
LT_DEFAULT_ADD_TORRENT_FLAGS
|
||||||
| lt.add_torrent_params_flags_t.flag_duplicate_is_error
|
| lt.add_torrent_params_flags_t.flag_duplicate_is_error
|
||||||
@ -379,33 +380,29 @@ class TorrentManager(component.Component):
|
|||||||
|
|
||||||
d = Deferred()
|
d = Deferred()
|
||||||
# Cancel the defer if timeout reached.
|
# Cancel the defer if timeout reached.
|
||||||
defer_timeout = self.callLater(timeout, d.cancel)
|
d.addTimeout(timeout, self.clock)
|
||||||
d.addBoth(self.on_prefetch_metadata, torrent_id, defer_timeout)
|
self.prefetching_metadata[torrent_id] = PrefetchQueueItem(d, [])
|
||||||
Prefetch = namedtuple('Prefetch', 'defer handle')
|
|
||||||
self.prefetching_metadata[torrent_id] = Prefetch(defer=d, handle=torrent_handle)
|
|
||||||
return d
|
|
||||||
|
|
||||||
def on_prefetch_metadata(self, torrent_info, torrent_id, defer_timeout):
|
|
||||||
# Cancel reactor.callLater.
|
|
||||||
try:
|
try:
|
||||||
defer_timeout.cancel()
|
torrent_info = await d
|
||||||
except error.AlreadyCalled:
|
except (defer.TimeoutError, defer.CancelledError):
|
||||||
pass
|
log.debug(f'Prefetching metadata for {torrent_id} timed out or cancelled.')
|
||||||
|
metadata = b''
|
||||||
|
else:
|
||||||
|
log.debug('prefetch metadata received')
|
||||||
|
if VersionSplit(LT_VERSION) < VersionSplit('2.0.0.0'):
|
||||||
|
metadata = torrent_info.metadata()
|
||||||
|
else:
|
||||||
|
metadata = torrent_info.info_section()
|
||||||
|
|
||||||
log.debug('remove prefetch magnet from session')
|
log.debug('remove prefetch magnet from session')
|
||||||
try:
|
result_queue = self.prefetching_metadata.pop(torrent_id).result_queue
|
||||||
torrent_handle = self.prefetching_metadata.pop(torrent_id).handle
|
self.session.remove_torrent(torrent_handle, 1)
|
||||||
except KeyError:
|
result = torrent_id, b64encode(metadata)
|
||||||
pass
|
|
||||||
else:
|
|
||||||
self.session.remove_torrent(torrent_handle, 1)
|
|
||||||
|
|
||||||
metadata = None
|
for d in result_queue:
|
||||||
if isinstance(torrent_info, lt.torrent_info):
|
d.callback(result)
|
||||||
log.debug('prefetch metadata received')
|
return result
|
||||||
metadata = lt.bdecode(torrent_info.metadata())
|
|
||||||
|
|
||||||
return torrent_id, metadata
|
|
||||||
|
|
||||||
def _build_torrent_options(self, options):
|
def _build_torrent_options(self, options):
|
||||||
"""Load default options and update if needed."""
|
"""Load default options and update if needed."""
|
||||||
@ -438,14 +435,10 @@ class TorrentManager(component.Component):
|
|||||||
elif magnet:
|
elif magnet:
|
||||||
magnet_info = get_magnet_info(magnet)
|
magnet_info = get_magnet_info(magnet)
|
||||||
if magnet_info:
|
if magnet_info:
|
||||||
add_torrent_params['url'] = magnet.strip().encode('utf8')
|
|
||||||
add_torrent_params['name'] = magnet_info['name']
|
add_torrent_params['name'] = magnet_info['name']
|
||||||
|
add_torrent_params['trackers'] = list(magnet_info['trackers'])
|
||||||
torrent_id = magnet_info['info_hash']
|
torrent_id = magnet_info['info_hash']
|
||||||
# Workaround lt 1.2 bug for magnet resume data with no metadata
|
add_torrent_params['info_hash'] = bytes(bytearray.fromhex(torrent_id))
|
||||||
if resume_data and VersionSplit(LT_VERSION) >= VersionSplit('1.2.10.0'):
|
|
||||||
add_torrent_params['info_hash'] = bytes(
|
|
||||||
bytearray.fromhex(torrent_id)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
raise AddTorrentError(
|
raise AddTorrentError(
|
||||||
'Unable to add magnet, invalid magnet info: %s' % magnet
|
'Unable to add magnet, invalid magnet info: %s' % magnet
|
||||||
@ -460,7 +453,7 @@ class TorrentManager(component.Component):
|
|||||||
raise AddTorrentError('Torrent already being added (%s).' % torrent_id)
|
raise AddTorrentError('Torrent already being added (%s).' % torrent_id)
|
||||||
elif torrent_id in self.prefetching_metadata:
|
elif torrent_id in self.prefetching_metadata:
|
||||||
# Cancel and remove metadata fetching torrent.
|
# Cancel and remove metadata fetching torrent.
|
||||||
self.prefetching_metadata[torrent_id].defer.cancel()
|
self.prefetching_metadata[torrent_id].alert_deferred.cancel()
|
||||||
|
|
||||||
# Check for renamed files and if so, rename them in the torrent_info before adding.
|
# Check for renamed files and if so, rename them in the torrent_info before adding.
|
||||||
if options['mapped_files'] and torrent_info:
|
if options['mapped_files'] and torrent_info:
|
||||||
@ -821,12 +814,9 @@ class TorrentManager(component.Component):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
with open(filepath, 'rb') as _file:
|
with open(filepath, 'rb') as _file:
|
||||||
if PY2:
|
state = pickle.load(_file, encoding='utf8')
|
||||||
state = pickle.load(_file)
|
except (OSError, EOFError, pickle.UnpicklingError) as ex:
|
||||||
else:
|
message = f'Unable to load {filepath}: {ex}'
|
||||||
state = pickle.load(_file, encoding='utf8')
|
|
||||||
except (IOError, EOFError, pickle.UnpicklingError) as ex:
|
|
||||||
message = 'Unable to load {}: {}'.format(filepath, ex)
|
|
||||||
log.error(message)
|
log.error(message)
|
||||||
if not filepath.endswith('.bak'):
|
if not filepath.endswith('.bak'):
|
||||||
self.archive_state(message)
|
self.archive_state(message)
|
||||||
@ -1082,7 +1072,7 @@ class TorrentManager(component.Component):
|
|||||||
try:
|
try:
|
||||||
with open(_filepath, 'rb') as _file:
|
with open(_filepath, 'rb') as _file:
|
||||||
resume_data = lt.bdecode(_file.read())
|
resume_data = lt.bdecode(_file.read())
|
||||||
except (IOError, EOFError, RuntimeError) as ex:
|
except (OSError, EOFError, RuntimeError) as ex:
|
||||||
if self.torrents:
|
if self.torrents:
|
||||||
log.warning('Unable to load %s: %s', _filepath, ex)
|
log.warning('Unable to load %s: %s', _filepath, ex)
|
||||||
resume_data = None
|
resume_data = None
|
||||||
@ -1366,10 +1356,8 @@ class TorrentManager(component.Component):
|
|||||||
torrent.set_tracker_status('Announce OK')
|
torrent.set_tracker_status('Announce OK')
|
||||||
|
|
||||||
# Check for peer information from the tracker, if none then send a scrape request.
|
# Check for peer information from the tracker, if none then send a scrape request.
|
||||||
if (
|
torrent.get_lt_status()
|
||||||
alert.handle.status().num_complete == -1
|
if torrent.status.num_complete == -1 or torrent.status.num_incomplete == -1:
|
||||||
or alert.handle.status().num_incomplete == -1
|
|
||||||
):
|
|
||||||
torrent.scrape_tracker()
|
torrent.scrape_tracker()
|
||||||
|
|
||||||
def on_alert_tracker_announce(self, alert):
|
def on_alert_tracker_announce(self, alert):
|
||||||
@ -1404,22 +1392,18 @@ class TorrentManager(component.Component):
|
|||||||
log.debug(
|
log.debug(
|
||||||
'Tracker Error Alert: %s [%s]', decode_bytes(alert.message()), error_message
|
'Tracker Error Alert: %s [%s]', decode_bytes(alert.message()), error_message
|
||||||
)
|
)
|
||||||
if VersionSplit(LT_VERSION) >= VersionSplit('1.2.0.0'):
|
# libtorrent 1.2 added endpoint struct to each tracker. to prevent false updates
|
||||||
# libtorrent 1.2 added endpoint struct to each tracker. to prevent false updates
|
# we will need to verify that at least one endpoint to the errored tracker is working
|
||||||
# we will need to verify that at least one endpoint to the errored tracker is working
|
for tracker in torrent.handle.trackers():
|
||||||
for tracker in torrent.handle.trackers():
|
if tracker['url'] == alert.url:
|
||||||
if tracker['url'] == alert.url:
|
if any(
|
||||||
if any(
|
endpoint['last_error']['value'] == 0
|
||||||
endpoint['last_error']['value'] == 0
|
for endpoint in tracker['endpoints']
|
||||||
for endpoint in tracker['endpoints']
|
):
|
||||||
):
|
torrent.set_tracker_status('Announce OK')
|
||||||
torrent.set_tracker_status('Announce OK')
|
else:
|
||||||
else:
|
torrent.set_tracker_status('Error: ' + error_message)
|
||||||
torrent.set_tracker_status('Error: ' + error_message)
|
break
|
||||||
break
|
|
||||||
else:
|
|
||||||
# preserve old functionality for libtorrent < 1.2
|
|
||||||
torrent.set_tracker_status('Error: ' + error_message)
|
|
||||||
|
|
||||||
def on_alert_storage_moved(self, alert):
|
def on_alert_storage_moved(self, alert):
|
||||||
"""Alert handler for libtorrent storage_moved_alert"""
|
"""Alert handler for libtorrent storage_moved_alert"""
|
||||||
@ -1493,7 +1477,9 @@ class TorrentManager(component.Component):
|
|||||||
return
|
return
|
||||||
if torrent_id in self.torrents:
|
if torrent_id in self.torrents:
|
||||||
# libtorrent add_torrent expects bencoded resume_data.
|
# libtorrent add_torrent expects bencoded resume_data.
|
||||||
self.resume_data[torrent_id] = lt.bencode(alert.resume_data)
|
self.resume_data[torrent_id] = lt.bencode(
|
||||||
|
lt.write_resume_data(alert.params)
|
||||||
|
)
|
||||||
|
|
||||||
if torrent_id in self.waiting_on_resume_data:
|
if torrent_id in self.waiting_on_resume_data:
|
||||||
self.waiting_on_resume_data[torrent_id].callback(None)
|
self.waiting_on_resume_data[torrent_id].callback(None)
|
||||||
@ -1575,7 +1561,7 @@ class TorrentManager(component.Component):
|
|||||||
|
|
||||||
# Try callback to prefetch_metadata method.
|
# Try callback to prefetch_metadata method.
|
||||||
try:
|
try:
|
||||||
d = self.prefetching_metadata[torrent_id].defer
|
d = self.prefetching_metadata[torrent_id].alert_deferred
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
@ -1621,7 +1607,7 @@ class TorrentManager(component.Component):
|
|||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
continue
|
continue
|
||||||
if torrent_id in self.torrents:
|
if torrent_id in self.torrents:
|
||||||
self.torrents[torrent_id].update_status(t_status)
|
self.torrents[torrent_id].status = t_status
|
||||||
|
|
||||||
self.handle_torrents_status_callback(self.torrents_status_requests.pop())
|
self.handle_torrents_status_callback(self.torrents_status_requests.pop())
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2007,2008 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2007,2008 Andrew Resch <andrewresch@gmail.com>
|
||||||
#
|
#
|
||||||
@ -7,8 +6,10 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import division, print_function, unicode_literals
|
import os
|
||||||
|
import stat
|
||||||
|
|
||||||
|
from OpenSSL import crypto
|
||||||
from OpenSSL.crypto import FILETYPE_PEM
|
from OpenSSL.crypto import FILETYPE_PEM
|
||||||
from twisted.internet.ssl import (
|
from twisted.internet.ssl import (
|
||||||
AcceptableCiphers,
|
AcceptableCiphers,
|
||||||
@ -18,6 +19,8 @@ from twisted.internet.ssl import (
|
|||||||
TLSVersion,
|
TLSVersion,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
import deluge.configmanager
|
||||||
|
|
||||||
# A TLS ciphers list.
|
# A TLS ciphers list.
|
||||||
# Sources for more information on TLS ciphers:
|
# Sources for more information on TLS ciphers:
|
||||||
# - https://wiki.mozilla.org/Security/Server_Side_TLS
|
# - https://wiki.mozilla.org/Security/Server_Side_TLS
|
||||||
@ -77,3 +80,57 @@ def get_context_factory(cert_path, pkey_path):
|
|||||||
ctx.set_options(SSL_OP_NO_RENEGOTIATION)
|
ctx.set_options(SSL_OP_NO_RENEGOTIATION)
|
||||||
|
|
||||||
return cert_options
|
return cert_options
|
||||||
|
|
||||||
|
|
||||||
|
def check_ssl_keys():
|
||||||
|
"""
|
||||||
|
Check for SSL cert/key and create them if necessary
|
||||||
|
"""
|
||||||
|
ssl_dir = deluge.configmanager.get_config_dir('ssl')
|
||||||
|
if not os.path.exists(ssl_dir):
|
||||||
|
# The ssl folder doesn't exist so we need to create it
|
||||||
|
os.makedirs(ssl_dir)
|
||||||
|
generate_ssl_keys()
|
||||||
|
else:
|
||||||
|
for f in ('daemon.pkey', 'daemon.cert'):
|
||||||
|
if not os.path.exists(os.path.join(ssl_dir, f)):
|
||||||
|
generate_ssl_keys()
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def generate_ssl_keys():
|
||||||
|
"""
|
||||||
|
This method generates a new SSL key/cert.
|
||||||
|
"""
|
||||||
|
digest = 'sha256'
|
||||||
|
|
||||||
|
# Generate key pair
|
||||||
|
pkey = crypto.PKey()
|
||||||
|
pkey.generate_key(crypto.TYPE_RSA, 2048)
|
||||||
|
|
||||||
|
# Generate cert request
|
||||||
|
req = crypto.X509Req()
|
||||||
|
subj = req.get_subject()
|
||||||
|
setattr(subj, 'CN', 'Deluge Daemon')
|
||||||
|
req.set_pubkey(pkey)
|
||||||
|
req.sign(pkey, digest)
|
||||||
|
|
||||||
|
# Generate certificate
|
||||||
|
cert = crypto.X509()
|
||||||
|
cert.set_serial_number(0)
|
||||||
|
cert.gmtime_adj_notBefore(0)
|
||||||
|
cert.gmtime_adj_notAfter(60 * 60 * 24 * 365 * 3) # Three Years
|
||||||
|
cert.set_issuer(req.get_subject())
|
||||||
|
cert.set_subject(req.get_subject())
|
||||||
|
cert.set_pubkey(req.get_pubkey())
|
||||||
|
cert.sign(pkey, digest)
|
||||||
|
|
||||||
|
# Write out files
|
||||||
|
ssl_dir = deluge.configmanager.get_config_dir('ssl')
|
||||||
|
with open(os.path.join(ssl_dir, 'daemon.pkey'), 'wb') as _file:
|
||||||
|
_file.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
|
||||||
|
with open(os.path.join(ssl_dir, 'daemon.cert'), 'wb') as _file:
|
||||||
|
_file.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
|
||||||
|
# Make the files only readable by this user
|
||||||
|
for f in ('daemon.pkey', 'daemon.cert'):
|
||||||
|
os.chmod(os.path.join(ssl_dir, f), stat.S_IREAD | stat.S_IWRITE)
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2010 John Garland <johnnybg+deluge@gmail.com>
|
# Copyright (C) 2010 John Garland <johnnybg+deluge@gmail.com>
|
||||||
#
|
#
|
||||||
@ -7,12 +6,13 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
import re
|
import re
|
||||||
import warnings
|
import warnings
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
from typing import Any, Callable, Coroutine, TypeVar
|
||||||
|
|
||||||
|
from twisted.internet import defer
|
||||||
|
|
||||||
|
|
||||||
def proxy(proxy_func):
|
def proxy(proxy_func):
|
||||||
@ -127,7 +127,7 @@ def _overrides(stack, method, explicit_base_classes=None):
|
|||||||
% (
|
% (
|
||||||
method.__name__,
|
method.__name__,
|
||||||
cls,
|
cls,
|
||||||
'File: %s:%s' % (stack[1][1], stack[1][2]),
|
f'File: {stack[1][1]}:{stack[1][2]}',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -137,7 +137,7 @@ def _overrides(stack, method, explicit_base_classes=None):
|
|||||||
% (
|
% (
|
||||||
method.__name__,
|
method.__name__,
|
||||||
check_classes,
|
check_classes,
|
||||||
'File: %s:%s' % (stack[1][1], stack[1][2]),
|
f'File: {stack[1][1]}:{stack[1][2]}',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return method
|
return method
|
||||||
@ -154,7 +154,7 @@ def deprecated(func):
|
|||||||
def depr_func(*args, **kwargs):
|
def depr_func(*args, **kwargs):
|
||||||
warnings.simplefilter('always', DeprecationWarning) # Turn off filter
|
warnings.simplefilter('always', DeprecationWarning) # Turn off filter
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
'Call to deprecated function {}.'.format(func.__name__),
|
f'Call to deprecated function {func.__name__}.',
|
||||||
category=DeprecationWarning,
|
category=DeprecationWarning,
|
||||||
stacklevel=2,
|
stacklevel=2,
|
||||||
)
|
)
|
||||||
@ -162,3 +162,57 @@ def deprecated(func):
|
|||||||
return func(*args, **kwargs)
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
return depr_func
|
return depr_func
|
||||||
|
|
||||||
|
|
||||||
|
class CoroutineDeferred(defer.Deferred):
|
||||||
|
"""Wraps a coroutine in a Deferred.
|
||||||
|
It will dynamically pass through the underlying coroutine without wrapping where apporpriate."""
|
||||||
|
|
||||||
|
def __init__(self, coro: Coroutine):
|
||||||
|
# Delay this import to make sure a reactor was installed first
|
||||||
|
from twisted.internet import reactor
|
||||||
|
|
||||||
|
super().__init__()
|
||||||
|
self.coro = coro
|
||||||
|
self.awaited = None
|
||||||
|
self.activate_deferred = reactor.callLater(0, self.activate)
|
||||||
|
|
||||||
|
def __await__(self):
|
||||||
|
if self.awaited in [None, True]:
|
||||||
|
self.awaited = True
|
||||||
|
return self.coro.__await__()
|
||||||
|
# Already in deferred mode
|
||||||
|
return super().__await__()
|
||||||
|
|
||||||
|
def activate(self):
|
||||||
|
"""If the result wasn't awaited before the next context switch, we turn it into a deferred."""
|
||||||
|
if self.awaited is None:
|
||||||
|
self.awaited = False
|
||||||
|
try:
|
||||||
|
d = defer.Deferred.fromCoroutine(self.coro)
|
||||||
|
except AttributeError:
|
||||||
|
# Fallback for Twisted <= 21.2 without fromCoroutine
|
||||||
|
d = defer.ensureDeferred(self.coro)
|
||||||
|
d.chainDeferred(self)
|
||||||
|
|
||||||
|
def addCallbacks(self, *args, **kwargs): # noqa: N802
|
||||||
|
assert not self.awaited, 'Cannot add callbacks to an already awaited coroutine.'
|
||||||
|
self.activate()
|
||||||
|
return super().addCallbacks(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
_RetT = TypeVar('_RetT')
|
||||||
|
|
||||||
|
|
||||||
|
def maybe_coroutine(
|
||||||
|
f: Callable[..., Coroutine[Any, Any, _RetT]]
|
||||||
|
) -> 'Callable[..., defer.Deferred[_RetT]]':
|
||||||
|
"""Wraps a coroutine function to make it usable as a normal function that returns a Deferred."""
|
||||||
|
|
||||||
|
@wraps(f)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
# Uncomment for quick testing to make sure CoroutineDeferred magic isn't at fault
|
||||||
|
# return defer.ensureDeferred(f(*args, **kwargs))
|
||||||
|
return CoroutineDeferred(f(*args, **kwargs))
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com>
|
||||||
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
|
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
|
||||||
@ -9,18 +8,15 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
|
|
||||||
class DelugeError(Exception):
|
class DelugeError(Exception):
|
||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
inst = super(DelugeError, cls).__new__(cls, *args, **kwargs)
|
inst = super().__new__(cls, *args, **kwargs)
|
||||||
inst._args = args
|
inst._args = args
|
||||||
inst._kwargs = kwargs
|
inst._kwargs = kwargs
|
||||||
return inst
|
return inst
|
||||||
|
|
||||||
def __init__(self, message=None):
|
def __init__(self, message=None):
|
||||||
super(DelugeError, self).__init__(message)
|
super().__init__(message)
|
||||||
self.message = message
|
self.message = message
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -45,12 +41,12 @@ class InvalidPathError(DelugeError):
|
|||||||
|
|
||||||
class WrappedException(DelugeError):
|
class WrappedException(DelugeError):
|
||||||
def __init__(self, message, exception_type, traceback):
|
def __init__(self, message, exception_type, traceback):
|
||||||
super(WrappedException, self).__init__(message)
|
super().__init__(message)
|
||||||
self.type = exception_type
|
self.type = exception_type
|
||||||
self.traceback = traceback
|
self.traceback = traceback
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '%s\n%s' % (self.message, self.traceback)
|
return f'{self.message}\n{self.traceback}'
|
||||||
|
|
||||||
|
|
||||||
class _ClientSideRecreateError(DelugeError):
|
class _ClientSideRecreateError(DelugeError):
|
||||||
@ -64,7 +60,7 @@ class IncompatibleClient(_ClientSideRecreateError):
|
|||||||
'Your deluge client is not compatible with the daemon. '
|
'Your deluge client is not compatible with the daemon. '
|
||||||
'Please upgrade your client to %(daemon_version)s'
|
'Please upgrade your client to %(daemon_version)s'
|
||||||
) % {'daemon_version': self.daemon_version}
|
) % {'daemon_version': self.daemon_version}
|
||||||
super(IncompatibleClient, self).__init__(message=msg)
|
super().__init__(message=msg)
|
||||||
|
|
||||||
|
|
||||||
class NotAuthorizedError(_ClientSideRecreateError):
|
class NotAuthorizedError(_ClientSideRecreateError):
|
||||||
@ -73,14 +69,14 @@ class NotAuthorizedError(_ClientSideRecreateError):
|
|||||||
'current_level': current_level,
|
'current_level': current_level,
|
||||||
'required_level': required_level,
|
'required_level': required_level,
|
||||||
}
|
}
|
||||||
super(NotAuthorizedError, self).__init__(message=msg)
|
super().__init__(message=msg)
|
||||||
self.current_level = current_level
|
self.current_level = current_level
|
||||||
self.required_level = required_level
|
self.required_level = required_level
|
||||||
|
|
||||||
|
|
||||||
class _UsernameBasedPasstroughError(_ClientSideRecreateError):
|
class _UsernameBasedPasstroughError(_ClientSideRecreateError):
|
||||||
def __init__(self, message, username):
|
def __init__(self, message, username):
|
||||||
super(_UsernameBasedPasstroughError, self).__init__(message)
|
super().__init__(message)
|
||||||
self.username = username
|
self.username = username
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
||||||
#
|
#
|
||||||
@ -14,10 +13,6 @@ This module describes the types of events that can be generated by the daemon
|
|||||||
and subsequently emitted to the clients.
|
and subsequently emitted to the clients.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
known_events = {}
|
known_events = {}
|
||||||
|
|
||||||
|
|
||||||
@ -27,12 +22,12 @@ class DelugeEventMetaClass(type):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(cls, name, bases, dct): # pylint: disable=bad-mcs-method-argument
|
def __init__(cls, name, bases, dct): # pylint: disable=bad-mcs-method-argument
|
||||||
super(DelugeEventMetaClass, cls).__init__(name, bases, dct)
|
super().__init__(name, bases, dct)
|
||||||
if name != 'DelugeEvent':
|
if name != 'DelugeEvent':
|
||||||
known_events[name] = cls
|
known_events[name] = cls
|
||||||
|
|
||||||
|
|
||||||
class DelugeEvent(six.with_metaclass(DelugeEventMetaClass, object)):
|
class DelugeEvent(metaclass=DelugeEventMetaClass):
|
||||||
"""
|
"""
|
||||||
The base class for all events.
|
The base class for all events.
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
||||||
#
|
#
|
||||||
@ -7,8 +6,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import cgi
|
import cgi
|
||||||
import logging
|
import logging
|
||||||
import os.path
|
import os.path
|
||||||
@ -19,7 +16,7 @@ from twisted.internet.defer import Deferred
|
|||||||
from twisted.python.failure import Failure
|
from twisted.python.failure import Failure
|
||||||
from twisted.web import client, http
|
from twisted.web import client, http
|
||||||
from twisted.web._newclient import HTTPClientParser
|
from twisted.web._newclient import HTTPClientParser
|
||||||
from twisted.web.error import PageRedirect
|
from twisted.web.error import Error, PageRedirect
|
||||||
from twisted.web.http_headers import Headers
|
from twisted.web.http_headers import Headers
|
||||||
from twisted.web.iweb import IAgent
|
from twisted.web.iweb import IAgent
|
||||||
from zope.interface import implementer
|
from zope.interface import implementer
|
||||||
@ -40,11 +37,11 @@ class CompressionDecoderProtocol(client._GzipProtocol):
|
|||||||
"""A compression decoder protocol for CompressionDecoder."""
|
"""A compression decoder protocol for CompressionDecoder."""
|
||||||
|
|
||||||
def __init__(self, protocol, response):
|
def __init__(self, protocol, response):
|
||||||
super(CompressionDecoderProtocol, self).__init__(protocol, response)
|
super().__init__(protocol, response)
|
||||||
self._zlibDecompress = zlib.decompressobj(32 + zlib.MAX_WBITS)
|
self._zlibDecompress = zlib.decompressobj(32 + zlib.MAX_WBITS)
|
||||||
|
|
||||||
|
|
||||||
class BodyHandler(HTTPClientParser, object):
|
class BodyHandler(HTTPClientParser):
|
||||||
"""An HTTP parser that saves the response to a file."""
|
"""An HTTP parser that saves the response to a file."""
|
||||||
|
|
||||||
def __init__(self, request, finished, length, agent, encoding=None):
|
def __init__(self, request, finished, length, agent, encoding=None):
|
||||||
@ -56,7 +53,7 @@ class BodyHandler(HTTPClientParser, object):
|
|||||||
length (int): The length of the response.
|
length (int): The length of the response.
|
||||||
agent (t.w.i.IAgent): The agent from which the request was sent.
|
agent (t.w.i.IAgent): The agent from which the request was sent.
|
||||||
"""
|
"""
|
||||||
super(BodyHandler, self).__init__(request, finished)
|
super().__init__(request, finished)
|
||||||
self.agent = agent
|
self.agent = agent
|
||||||
self.finished = finished
|
self.finished = finished
|
||||||
self.total_length = length
|
self.total_length = length
|
||||||
@ -76,12 +73,12 @@ class BodyHandler(HTTPClientParser, object):
|
|||||||
with open(self.agent.filename, 'wb') as _file:
|
with open(self.agent.filename, 'wb') as _file:
|
||||||
_file.write(self.data)
|
_file.write(self.data)
|
||||||
self.finished.callback(self.agent.filename)
|
self.finished.callback(self.agent.filename)
|
||||||
self.state = u'DONE'
|
self.state = 'DONE'
|
||||||
HTTPClientParser.connectionLost(self, reason)
|
HTTPClientParser.connectionLost(self, reason)
|
||||||
|
|
||||||
|
|
||||||
@implementer(IAgent)
|
@implementer(IAgent)
|
||||||
class HTTPDownloaderAgent(object):
|
class HTTPDownloaderAgent:
|
||||||
"""A File Downloader Agent."""
|
"""A File Downloader Agent."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -125,6 +122,9 @@ class HTTPDownloaderAgent(object):
|
|||||||
location = response.headers.getRawHeaders(b'location')[0]
|
location = response.headers.getRawHeaders(b'location')[0]
|
||||||
error = PageRedirect(response.code, location=location)
|
error = PageRedirect(response.code, location=location)
|
||||||
finished.errback(Failure(error))
|
finished.errback(Failure(error))
|
||||||
|
elif response.code >= 400:
|
||||||
|
error = Error(response.code)
|
||||||
|
finished.errback(Failure(error))
|
||||||
else:
|
else:
|
||||||
headers = response.headers
|
headers = response.headers
|
||||||
body_length = int(headers.getRawHeaders(b'content-length', default=[0])[0])
|
body_length = int(headers.getRawHeaders(b'content-length', default=[0])[0])
|
||||||
@ -146,7 +146,7 @@ class HTTPDownloaderAgent(object):
|
|||||||
fileext = os.path.splitext(new_file_name)[1]
|
fileext = os.path.splitext(new_file_name)[1]
|
||||||
while os.path.isfile(new_file_name):
|
while os.path.isfile(new_file_name):
|
||||||
# Increment filename if already exists
|
# Increment filename if already exists
|
||||||
new_file_name = '%s-%s%s' % (fileroot, count, fileext)
|
new_file_name = f'{fileroot}-{count}{fileext}'
|
||||||
count += 1
|
count += 1
|
||||||
|
|
||||||
self.filename = new_file_name
|
self.filename = new_file_name
|
||||||
|
|||||||
@ -1,10 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# This file is public domain.
|
# This file is public domain.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
# Language code for this installation. All choices can be found here:
|
# Language code for this installation. All choices can be found here:
|
||||||
# http://www.i18nguy.com/unicode/language-identifiers.html
|
# http://www.i18nguy.com/unicode/language-identifiers.html
|
||||||
LANGUAGE_CODE = 'en-us'
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2007,2008 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2007,2008 Andrew Resch <andrewresch@gmail.com>
|
||||||
#
|
#
|
||||||
@ -7,8 +6,7 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
import builtins
|
||||||
|
|
||||||
import ctypes
|
import ctypes
|
||||||
import gettext
|
import gettext
|
||||||
import locale
|
import locale
|
||||||
@ -17,8 +15,6 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
from glob import glob
|
from glob import glob
|
||||||
|
|
||||||
from six.moves import builtins
|
|
||||||
|
|
||||||
import deluge.common
|
import deluge.common
|
||||||
|
|
||||||
from .languages import LANGUAGES
|
from .languages import LANGUAGES
|
||||||
@ -80,7 +76,7 @@ def set_language(lang):
|
|||||||
translation = gettext.translation(
|
translation = gettext.translation(
|
||||||
'deluge', localedir=get_translations_path(), languages=[lang]
|
'deluge', localedir=get_translations_path(), languages=[lang]
|
||||||
)
|
)
|
||||||
except IOError:
|
except OSError:
|
||||||
log.warning('Unable to find translation (.mo) to set language: %s', lang)
|
log.warning('Unable to find translation (.mo) to set language: %s', lang)
|
||||||
else:
|
else:
|
||||||
translation.install()
|
translation.install()
|
||||||
@ -113,19 +109,17 @@ def setup_translation():
|
|||||||
gettext.bindtextdomain(I18N_DOMAIN, translations_path)
|
gettext.bindtextdomain(I18N_DOMAIN, translations_path)
|
||||||
gettext.textdomain(I18N_DOMAIN)
|
gettext.textdomain(I18N_DOMAIN)
|
||||||
|
|
||||||
# Workaround for Python 2 unicode gettext (keyword removed in Py3).
|
gettext.install(I18N_DOMAIN, translations_path, names=['ngettext'])
|
||||||
kwargs = {} if not deluge.common.PY2 else {'unicode': True}
|
|
||||||
|
|
||||||
gettext.install(I18N_DOMAIN, translations_path, names=['ngettext'], **kwargs)
|
|
||||||
builtins.__dict__['_n'] = builtins.__dict__['ngettext']
|
builtins.__dict__['_n'] = builtins.__dict__['ngettext']
|
||||||
|
|
||||||
def load_libintl(libintls):
|
def load_libintl(libintls):
|
||||||
errors = []
|
errors = []
|
||||||
|
libintl = None
|
||||||
for library in libintls:
|
for library in libintls:
|
||||||
try:
|
try:
|
||||||
libintl = ctypes.cdll.LoadLibrary(library)
|
libintl = ctypes.cdll.LoadLibrary(library)
|
||||||
except OSError as ex:
|
except OSError as ex:
|
||||||
errors.append(ex)
|
errors.append(str(ex))
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
|
||||||
# Copyright (C) 2010 Pedro Algarvio <pedro@algarvio.me>
|
# Copyright (C) 2010 Pedro Algarvio <pedro@algarvio.me>
|
||||||
@ -9,8 +8,6 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
"""Logging functions"""
|
"""Logging functions"""
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
import logging.handlers
|
import logging.handlers
|
||||||
@ -39,7 +36,7 @@ MAX_LOGGER_NAME_LENGTH = 10
|
|||||||
|
|
||||||
class Logging(LoggingLoggerClass):
|
class Logging(LoggingLoggerClass):
|
||||||
def __init__(self, logger_name):
|
def __init__(self, logger_name):
|
||||||
super(Logging, self).__init__(logger_name)
|
super().__init__(logger_name)
|
||||||
|
|
||||||
# This makes module name padding increase to the biggest module name
|
# This makes module name padding increase to the biggest module name
|
||||||
# so that logs keep readability.
|
# so that logs keep readability.
|
||||||
@ -54,39 +51,31 @@ class Logging(LoggingLoggerClass):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def garbage(self, msg, *args, **kwargs):
|
def garbage(self, msg, *args, **kwargs):
|
||||||
yield LoggingLoggerClass.log(self, 1, msg, *args, **kwargs)
|
LoggingLoggerClass.log(self, 1, msg, *args, **kwargs)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def trace(self, msg, *args, **kwargs):
|
def trace(self, msg, *args, **kwargs):
|
||||||
yield LoggingLoggerClass.log(self, 5, msg, *args, **kwargs)
|
LoggingLoggerClass.log(self, 5, msg, *args, **kwargs)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def debug(self, msg, *args, **kwargs):
|
def debug(self, msg, *args, **kwargs):
|
||||||
yield LoggingLoggerClass.debug(self, msg, *args, **kwargs)
|
LoggingLoggerClass.debug(self, msg, *args, **kwargs)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def info(self, msg, *args, **kwargs):
|
def info(self, msg, *args, **kwargs):
|
||||||
yield LoggingLoggerClass.info(self, msg, *args, **kwargs)
|
LoggingLoggerClass.info(self, msg, *args, **kwargs)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def warning(self, msg, *args, **kwargs):
|
def warning(self, msg, *args, **kwargs):
|
||||||
yield LoggingLoggerClass.warning(self, msg, *args, **kwargs)
|
LoggingLoggerClass.warning(self, msg, *args, **kwargs)
|
||||||
|
|
||||||
warn = warning
|
warn = warning
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def error(self, msg, *args, **kwargs):
|
def error(self, msg, *args, **kwargs):
|
||||||
yield LoggingLoggerClass.error(self, msg, *args, **kwargs)
|
LoggingLoggerClass.error(self, msg, *args, **kwargs)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def critical(self, msg, *args, **kwargs):
|
def critical(self, msg, *args, **kwargs):
|
||||||
yield LoggingLoggerClass.critical(self, msg, *args, **kwargs)
|
LoggingLoggerClass.critical(self, msg, *args, **kwargs)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def exception(self, msg, *args, **kwargs):
|
def exception(self, msg, *args, **kwargs):
|
||||||
yield LoggingLoggerClass.exception(self, msg, *args, **kwargs)
|
LoggingLoggerClass.exception(self, msg, *args, **kwargs)
|
||||||
|
|
||||||
def findCaller(self, *args, **kwargs): # NOQA: N802
|
def findCaller(self, *args, **kwargs): # NOQA: N802
|
||||||
f = logging.currentframe().f_back
|
f = logging.currentframe().f_back
|
||||||
@ -102,10 +91,7 @@ class Logging(LoggingLoggerClass):
|
|||||||
continue
|
continue
|
||||||
rv = (co.co_filename, f.f_lineno, co.co_name, None)
|
rv = (co.co_filename, f.f_lineno, co.co_name, None)
|
||||||
break
|
break
|
||||||
if common.PY2:
|
return rv
|
||||||
return rv[:-1]
|
|
||||||
else:
|
|
||||||
return rv
|
|
||||||
|
|
||||||
|
|
||||||
levels = {
|
levels = {
|
||||||
@ -161,7 +147,12 @@ def setup_logger(
|
|||||||
handler_cls = getattr(
|
handler_cls = getattr(
|
||||||
logging.handlers, 'WatchedFileHandler', logging.FileHandler
|
logging.handlers, 'WatchedFileHandler', logging.FileHandler
|
||||||
)
|
)
|
||||||
handler = handler_cls(filename, mode=filemode, encoding='utf-8')
|
try:
|
||||||
|
handler = handler_cls(filename, mode=filemode, encoding='utf-8')
|
||||||
|
except FileNotFoundError:
|
||||||
|
handler = logging.StreamHandler(stream=output_stream)
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.error(f'Unable to write to log file `{filename}`')
|
||||||
else:
|
else:
|
||||||
handler = logging.StreamHandler(stream=output_stream)
|
handler = logging.StreamHandler(stream=output_stream)
|
||||||
|
|
||||||
@ -243,7 +234,7 @@ def tweak_logging_levels():
|
|||||||
log.warning(
|
log.warning(
|
||||||
'logging.conf found! tweaking logging levels from %s', logging_config_file
|
'logging.conf found! tweaking logging levels from %s', logging_config_file
|
||||||
)
|
)
|
||||||
with open(logging_config_file, 'r') as _file:
|
with open(logging_config_file) as _file:
|
||||||
for line in _file:
|
for line in _file:
|
||||||
if line.strip().startswith('#'):
|
if line.strip().startswith('#'):
|
||||||
continue
|
continue
|
||||||
@ -314,7 +305,7 @@ Triggering code:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class _BackwardsCompatibleLOG(object):
|
class _BackwardsCompatibleLOG:
|
||||||
def __getattribute__(self, name):
|
def __getattribute__(self, name):
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
||||||
#
|
#
|
||||||
@ -7,8 +6,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import division, unicode_literals
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from hashlib import sha1 as sha
|
from hashlib import sha1 as sha
|
||||||
|
|
||||||
@ -32,7 +29,7 @@ class InvalidPieceSize(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class TorrentMetadata(object):
|
class TorrentMetadata:
|
||||||
"""This class is used to create .torrent files.
|
"""This class is used to create .torrent files.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Original file from BitTorrent-5.3-GPL.tar.gz
|
# Original file from BitTorrent-5.3-GPL.tar.gz
|
||||||
# Copyright (C) Bram Cohen
|
# Copyright (C) Bram Cohen
|
||||||
@ -11,8 +10,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import division, unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os.path
|
import os.path
|
||||||
import time
|
import time
|
||||||
@ -44,7 +41,7 @@ def dummy(*v):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class RemoteFileProgress(object):
|
class RemoteFileProgress:
|
||||||
def __init__(self, session_id):
|
def __init__(self, session_id):
|
||||||
self.session_id = session_id
|
self.session_id = session_id
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2013 Bro <bro.development@gmail.com>
|
# Copyright (C) 2013 Bro <bro.development@gmail.com>
|
||||||
#
|
#
|
||||||
@ -8,12 +7,8 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from deluge.common import PY2
|
|
||||||
|
|
||||||
|
|
||||||
def is_hidden(filepath):
|
def is_hidden(filepath):
|
||||||
def has_hidden_attribute(filepath):
|
def has_hidden_attribute(filepath):
|
||||||
@ -45,7 +40,7 @@ def get_completion_paths(args):
|
|||||||
:param args: options
|
:param args: options
|
||||||
:type args: dict
|
:type args: dict
|
||||||
:returns: the args argument containing the available completions for the completion_text
|
:returns: the args argument containing the available completions for the completion_text
|
||||||
:rtype: list
|
:rtype: dict
|
||||||
|
|
||||||
"""
|
"""
|
||||||
args['paths'] = []
|
args['paths'] = []
|
||||||
@ -54,10 +49,7 @@ def get_completion_paths(args):
|
|||||||
|
|
||||||
def get_subdirs(dirname):
|
def get_subdirs(dirname):
|
||||||
try:
|
try:
|
||||||
if PY2:
|
return next(os.walk(dirname))[1]
|
||||||
return os.walk(dirname).__next__[1]
|
|
||||||
else:
|
|
||||||
return next(os.walk(dirname))[1]
|
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
# Invalid dirname
|
# Invalid dirname
|
||||||
return []
|
return []
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
|
||||||
#
|
#
|
||||||
@ -9,8 +8,7 @@
|
|||||||
|
|
||||||
|
|
||||||
"""PluginManagerBase"""
|
"""PluginManagerBase"""
|
||||||
from __future__ import unicode_literals
|
import email
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os.path
|
import os.path
|
||||||
|
|
||||||
@ -37,7 +35,7 @@ METADATA_KEYS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
DEPRECATION_WARNING = """
|
DEPRECATION_WARNING = """
|
||||||
The plugin %s is not using the "deluge.plugins" namespace.
|
The plugin %s is not using the "deluge_" namespace.
|
||||||
In order to avoid package name clashes between regular python packages and
|
In order to avoid package name clashes between regular python packages and
|
||||||
deluge plugins, the way deluge plugins should be created has changed.
|
deluge plugins, the way deluge plugins should be created has changed.
|
||||||
If you're seeing this message and you're not the developer of the plugin which
|
If you're seeing this message and you're not the developer of the plugin which
|
||||||
@ -47,7 +45,7 @@ git repository to have an idea of what needs to be changed.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class PluginManagerBase(object):
|
class PluginManagerBase:
|
||||||
"""PluginManagerBase is a base class for PluginManagers to inherit"""
|
"""PluginManagerBase is a base class for PluginManagers to inherit"""
|
||||||
|
|
||||||
def __init__(self, config_file, entry_name):
|
def __init__(self, config_file, entry_name):
|
||||||
@ -164,7 +162,7 @@ class PluginManagerBase(object):
|
|||||||
log.exception(ex)
|
log.exception(ex)
|
||||||
return_d = defer.fail(False)
|
return_d = defer.fail(False)
|
||||||
|
|
||||||
if not instance.__module__.startswith('deluge.plugins.'):
|
if not instance.__module__.startswith('deluge_'):
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
warnings.warn_explicit(
|
warnings.warn_explicit(
|
||||||
@ -257,28 +255,25 @@ class PluginManagerBase(object):
|
|||||||
|
|
||||||
def get_plugin_info(self, name):
|
def get_plugin_info(self, name):
|
||||||
"""Returns a dictionary of plugin info from the metadata"""
|
"""Returns a dictionary of plugin info from the metadata"""
|
||||||
info = {}.fromkeys(METADATA_KEYS)
|
|
||||||
last_header = ''
|
|
||||||
cont_lines = []
|
|
||||||
# Missing plugin info
|
|
||||||
if not self.pkg_env[name]:
|
if not self.pkg_env[name]:
|
||||||
log.warning('Failed to retrieve info for plugin: %s', name)
|
log.warning('Failed to retrieve info for plugin: %s', name)
|
||||||
for k in info:
|
info = {}.fromkeys(METADATA_KEYS, '')
|
||||||
info[k] = 'not available'
|
info['Name'] = info['Version'] = 'not available'
|
||||||
return info
|
return info
|
||||||
for line in self.pkg_env[name][0].get_metadata('PKG-INFO').splitlines():
|
|
||||||
if not line:
|
pkg_info = self.pkg_env[name][0].get_metadata('PKG-INFO')
|
||||||
continue
|
return self.parse_pkg_info(pkg_info)
|
||||||
if line[0] in ' \t' and (
|
|
||||||
len(line.split(':', 1)) == 1 or line.split(':', 1)[0] not in info
|
@staticmethod
|
||||||
):
|
def parse_pkg_info(pkg_info):
|
||||||
# This is a continuation
|
metadata_msg = email.message_from_string(pkg_info)
|
||||||
cont_lines.append(line.strip())
|
metadata_ver = metadata_msg.get('Metadata-Version')
|
||||||
else:
|
|
||||||
if cont_lines:
|
info = {key: metadata_msg.get(key, '') for key in METADATA_KEYS}
|
||||||
info[last_header] = '\n'.join(cont_lines).strip()
|
|
||||||
cont_lines = []
|
# Optional Description field in body (Metadata spec >=2.1)
|
||||||
if line.split(':', 1)[0] in info:
|
if not info['Description'] and metadata_ver.startswith('2'):
|
||||||
last_header = line.split(':', 1)[0]
|
info['Description'] = metadata_msg.get_payload().strip()
|
||||||
info[last_header] = line.split(':', 1)[1].strip()
|
|
||||||
return info
|
return info
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
|
# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
|
||||||
#
|
#
|
||||||
@ -12,8 +11,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from deluge.plugins.init import PluginInitBase
|
from deluge.plugins.init import PluginInitBase
|
||||||
|
|
||||||
|
|
||||||
@ -22,7 +19,7 @@ class CorePlugin(PluginInitBase):
|
|||||||
from .core import Core as _pluginCls
|
from .core import Core as _pluginCls
|
||||||
|
|
||||||
self._plugin_cls = _pluginCls
|
self._plugin_cls = _pluginCls
|
||||||
super(CorePlugin, self).__init__(plugin_name)
|
super().__init__(plugin_name)
|
||||||
|
|
||||||
|
|
||||||
class Gtk3UIPlugin(PluginInitBase):
|
class Gtk3UIPlugin(PluginInitBase):
|
||||||
@ -30,7 +27,7 @@ class Gtk3UIPlugin(PluginInitBase):
|
|||||||
from .gtkui import GtkUI as _pluginCls
|
from .gtkui import GtkUI as _pluginCls
|
||||||
|
|
||||||
self._plugin_cls = _pluginCls
|
self._plugin_cls = _pluginCls
|
||||||
super(Gtk3UIPlugin, self).__init__(plugin_name)
|
super().__init__(plugin_name)
|
||||||
|
|
||||||
|
|
||||||
class WebUIPlugin(PluginInitBase):
|
class WebUIPlugin(PluginInitBase):
|
||||||
@ -38,4 +35,4 @@ class WebUIPlugin(PluginInitBase):
|
|||||||
from .webui import WebUI as _pluginCls
|
from .webui import WebUI as _pluginCls
|
||||||
|
|
||||||
self._plugin_cls = _pluginCls
|
self._plugin_cls = _pluginCls
|
||||||
super(WebUIPlugin, self).__init__(plugin_name)
|
super().__init__(plugin_name)
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Basic plugin template created by:
|
# Basic plugin template created by:
|
||||||
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
||||||
@ -12,8 +11,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import os.path
|
import os.path
|
||||||
|
|
||||||
from pkg_resources import resource_filename
|
from pkg_resources import resource_filename
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
|
# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
|
||||||
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
|
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
|
||||||
@ -13,8 +12,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
@ -30,7 +27,7 @@ import deluge.configmanager
|
|||||||
from deluge._libtorrent import lt
|
from deluge._libtorrent import lt
|
||||||
from deluge.common import AUTH_LEVEL_ADMIN, is_magnet
|
from deluge.common import AUTH_LEVEL_ADMIN, is_magnet
|
||||||
from deluge.core.rpcserver import export
|
from deluge.core.rpcserver import export
|
||||||
from deluge.error import AddTorrentError
|
from deluge.error import AddTorrentError, InvalidTorrentError
|
||||||
from deluge.event import DelugeEvent
|
from deluge.event import DelugeEvent
|
||||||
from deluge.plugins.pluginbase import CorePluginBase
|
from deluge.plugins.pluginbase import CorePluginBase
|
||||||
|
|
||||||
@ -152,7 +149,7 @@ class Core(CorePluginBase):
|
|||||||
try:
|
try:
|
||||||
with open(filename, file_mode) as _file:
|
with open(filename, file_mode) as _file:
|
||||||
filedump = _file.read()
|
filedump = _file.read()
|
||||||
except IOError as ex:
|
except OSError as ex:
|
||||||
log.warning('Unable to open %s: %s', filename, ex)
|
log.warning('Unable to open %s: %s', filename, ex)
|
||||||
raise ex
|
raise ex
|
||||||
|
|
||||||
@ -161,7 +158,10 @@ class Core(CorePluginBase):
|
|||||||
|
|
||||||
# Get the info to see if any exceptions are raised
|
# Get the info to see if any exceptions are raised
|
||||||
if not magnet:
|
if not magnet:
|
||||||
lt.torrent_info(lt.bdecode(filedump))
|
decoded_torrent = lt.bdecode(filedump)
|
||||||
|
if decoded_torrent is None:
|
||||||
|
raise InvalidTorrentError('Torrent file failed decoding.')
|
||||||
|
lt.torrent_info(decoded_torrent)
|
||||||
|
|
||||||
return filedump
|
return filedump
|
||||||
|
|
||||||
@ -169,9 +169,9 @@ class Core(CorePluginBase):
|
|||||||
log.debug('Attempting to open %s for splitting magnets.', filename)
|
log.debug('Attempting to open %s for splitting magnets.', filename)
|
||||||
magnets = []
|
magnets = []
|
||||||
try:
|
try:
|
||||||
with open(filename, 'r') as _file:
|
with open(filename) as _file:
|
||||||
magnets = list(filter(len, _file.read().splitlines()))
|
magnets = list(filter(len, _file.read().splitlines()))
|
||||||
except IOError as ex:
|
except OSError as ex:
|
||||||
log.warning('Unable to open %s: %s', filename, ex)
|
log.warning('Unable to open %s: %s', filename, ex)
|
||||||
|
|
||||||
if len(magnets) < 2:
|
if len(magnets) < 2:
|
||||||
@ -196,7 +196,7 @@ class Core(CorePluginBase):
|
|||||||
try:
|
try:
|
||||||
with open(mname, 'w') as _mfile:
|
with open(mname, 'w') as _mfile:
|
||||||
_mfile.write(magnet)
|
_mfile.write(magnet)
|
||||||
except IOError as ex:
|
except OSError as ex:
|
||||||
log.warning('Unable to open %s: %s', mname, ex)
|
log.warning('Unable to open %s: %s', mname, ex)
|
||||||
return magnets
|
return magnets
|
||||||
|
|
||||||
@ -271,7 +271,7 @@ class Core(CorePluginBase):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
filedump = self.load_torrent(filepath, magnet)
|
filedump = self.load_torrent(filepath, magnet)
|
||||||
except (IOError, EOFError) as ex:
|
except (OSError, EOFError, InvalidTorrentError) as ex:
|
||||||
# If torrent is invalid, keep track of it so can try again on the next pass.
|
# If torrent is invalid, keep track of it so can try again on the next pass.
|
||||||
# This catches torrent files that may not be fully saved to disk at load time.
|
# This catches torrent files that may not be fully saved to disk at load time.
|
||||||
log.debug('Torrent is invalid: %s', ex)
|
log.debug('Torrent is invalid: %s', ex)
|
||||||
|
|||||||
@ -42,22 +42,21 @@ Deluge.ux.preferences.AutoAddPage = Ext.extend(Ext.Panel, {
|
|||||||
dataIndex: 'enabled',
|
dataIndex: 'enabled',
|
||||||
tpl: new Ext.XTemplate('{enabled:this.getCheckbox}', {
|
tpl: new Ext.XTemplate('{enabled:this.getCheckbox}', {
|
||||||
getCheckbox: function (checked, selected) {
|
getCheckbox: function (checked, selected) {
|
||||||
Deluge.ux.AutoAdd.onClickFunctions[
|
Deluge.ux.AutoAdd.onClickFunctions[selected.id] =
|
||||||
selected.id
|
function () {
|
||||||
] = function () {
|
if (selected.enabled) {
|
||||||
if (selected.enabled) {
|
deluge.client.autoadd.disable_watchdir(
|
||||||
deluge.client.autoadd.disable_watchdir(
|
selected.id
|
||||||
selected.id
|
);
|
||||||
);
|
checked = false;
|
||||||
checked = false;
|
} else {
|
||||||
} else {
|
deluge.client.autoadd.enable_watchdir(
|
||||||
deluge.client.autoadd.enable_watchdir(
|
selected.id
|
||||||
selected.id
|
);
|
||||||
);
|
checked = true;
|
||||||
checked = true;
|
}
|
||||||
}
|
autoAdd.updateWatchDirs();
|
||||||
autoAdd.updateWatchDirs();
|
};
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
'<input id="enabled-' +
|
'<input id="enabled-' +
|
||||||
selected.id +
|
selected.id +
|
||||||
|
|||||||
@ -90,9 +90,8 @@ Deluge.ux.AutoAdd.AutoAddWindowBase = Ext.extend(Ext.Window, {
|
|||||||
|
|
||||||
options['enabled'] = Ext.getCmp('enabled').getValue();
|
options['enabled'] = Ext.getCmp('enabled').getValue();
|
||||||
options['path'] = Ext.getCmp('path').getValue();
|
options['path'] = Ext.getCmp('path').getValue();
|
||||||
options['download_location'] = Ext.getCmp(
|
options['download_location'] =
|
||||||
'download_location'
|
Ext.getCmp('download_location').getValue();
|
||||||
).getValue();
|
|
||||||
options['move_completed_path'] = Ext.getCmp(
|
options['move_completed_path'] = Ext.getCmp(
|
||||||
'move_completed_path'
|
'move_completed_path'
|
||||||
).getValue();
|
).getValue();
|
||||||
|
|||||||
@ -150,8 +150,6 @@
|
|||||||
<property name="tooltip_text" translatable="yes">If a .torrent file is added to this directory,
|
<property name="tooltip_text" translatable="yes">If a .torrent file is added to this directory,
|
||||||
it will be added to the session.</property>
|
it will be added to the session.</property>
|
||||||
<property name="invisible_char">●</property>
|
<property name="invisible_char">●</property>
|
||||||
<property name="primary_icon_activatable">False</property>
|
|
||||||
<property name="secondary_icon_activatable">False</property>
|
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="expand">True</property>
|
<property name="expand">True</property>
|
||||||
@ -284,8 +282,6 @@ and it will remain in the same directory.</property>
|
|||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="invisible_char">•</property>
|
<property name="invisible_char">•</property>
|
||||||
<property name="text" translatable="yes">.added</property>
|
<property name="text" translatable="yes">.added</property>
|
||||||
<property name="primary_icon_activatable">False</property>
|
|
||||||
<property name="secondary_icon_activatable">False</property>
|
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="expand">True</property>
|
<property name="expand">True</property>
|
||||||
@ -329,8 +325,6 @@ and deleted from the watch folder.</property>
|
|||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="invisible_char">•</property>
|
<property name="invisible_char">•</property>
|
||||||
<property name="primary_icon_activatable">False</property>
|
|
||||||
<property name="secondary_icon_activatable">False</property>
|
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="expand">True</property>
|
<property name="expand">True</property>
|
||||||
@ -445,8 +439,6 @@ also delete the .torrent file used to add it.</property>
|
|||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="invisible_char">●</property>
|
<property name="invisible_char">●</property>
|
||||||
<property name="primary_icon_activatable">False</property>
|
|
||||||
<property name="secondary_icon_activatable">False</property>
|
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="expand">True</property>
|
<property name="expand">True</property>
|
||||||
@ -534,8 +526,6 @@ also delete the .torrent file used to add it.</property>
|
|||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="invisible_char">●</property>
|
<property name="invisible_char">●</property>
|
||||||
<property name="primary_icon_activatable">False</property>
|
|
||||||
<property name="secondary_icon_activatable">False</property>
|
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="expand">True</property>
|
<property name="expand">True</property>
|
||||||
@ -799,8 +789,6 @@ also delete the .torrent file used to add it.</property>
|
|||||||
<object class="GtkSpinButton" id="max_download_speed">
|
<object class="GtkSpinButton" id="max_download_speed">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="primary_icon_activatable">False</property>
|
|
||||||
<property name="secondary_icon_activatable">False</property>
|
|
||||||
<property name="adjustment">adjustment1</property>
|
<property name="adjustment">adjustment1</property>
|
||||||
<property name="climb_rate">1</property>
|
<property name="climb_rate">1</property>
|
||||||
<property name="digits">1</property>
|
<property name="digits">1</property>
|
||||||
@ -815,8 +803,6 @@ also delete the .torrent file used to add it.</property>
|
|||||||
<object class="GtkSpinButton" id="max_upload_speed">
|
<object class="GtkSpinButton" id="max_upload_speed">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="primary_icon_activatable">False</property>
|
|
||||||
<property name="secondary_icon_activatable">False</property>
|
|
||||||
<property name="adjustment">adjustment2</property>
|
<property name="adjustment">adjustment2</property>
|
||||||
<property name="climb_rate">1</property>
|
<property name="climb_rate">1</property>
|
||||||
<property name="digits">1</property>
|
<property name="digits">1</property>
|
||||||
@ -833,8 +819,6 @@ also delete the .torrent file used to add it.</property>
|
|||||||
<object class="GtkSpinButton" id="max_connections">
|
<object class="GtkSpinButton" id="max_connections">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="primary_icon_activatable">False</property>
|
|
||||||
<property name="secondary_icon_activatable">False</property>
|
|
||||||
<property name="adjustment">adjustment3</property>
|
<property name="adjustment">adjustment3</property>
|
||||||
<property name="climb_rate">1</property>
|
<property name="climb_rate">1</property>
|
||||||
</object>
|
</object>
|
||||||
@ -850,8 +834,6 @@ also delete the .torrent file used to add it.</property>
|
|||||||
<object class="GtkSpinButton" id="max_upload_slots">
|
<object class="GtkSpinButton" id="max_upload_slots">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="primary_icon_activatable">False</property>
|
|
||||||
<property name="secondary_icon_activatable">False</property>
|
|
||||||
<property name="adjustment">adjustment4</property>
|
<property name="adjustment">adjustment4</property>
|
||||||
<property name="climb_rate">1</property>
|
<property name="climb_rate">1</property>
|
||||||
</object>
|
</object>
|
||||||
@ -1063,8 +1045,6 @@ also delete the .torrent file used to add it.</property>
|
|||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="invisible_char">●</property>
|
<property name="invisible_char">●</property>
|
||||||
<property name="primary_icon_activatable">False</property>
|
|
||||||
<property name="secondary_icon_activatable">False</property>
|
|
||||||
<property name="adjustment">adjustment5</property>
|
<property name="adjustment">adjustment5</property>
|
||||||
<property name="climb_rate">1</property>
|
<property name="climb_rate">1</property>
|
||||||
<property name="digits">1</property>
|
<property name="digits">1</property>
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
|
# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
|
||||||
#
|
#
|
||||||
@ -12,14 +11,12 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import gi # isort:skip (Required before Gtk import).
|
import gi # isort:skip (Required before Gtk import).
|
||||||
|
|
||||||
gi.require_version('Gtk', '3.0') # NOQA: E402
|
gi.require_version('Gtk', '3.0')
|
||||||
|
|
||||||
# isort:imports-thirdparty
|
# isort:imports-thirdparty
|
||||||
from gi.repository import Gtk
|
from gi.repository import Gtk
|
||||||
@ -41,7 +38,7 @@ class IncompatibleOption(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class OptionsDialog(object):
|
class OptionsDialog:
|
||||||
spin_ids = ['max_download_speed', 'max_upload_speed', 'stop_ratio']
|
spin_ids = ['max_download_speed', 'max_upload_speed', 'stop_ratio']
|
||||||
spin_int_ids = ['max_upload_slots', 'max_connections']
|
spin_int_ids = ['max_upload_slots', 'max_connections']
|
||||||
chk_ids = [
|
chk_ids = [
|
||||||
@ -327,7 +324,7 @@ class OptionsDialog(object):
|
|||||||
dialogs.ErrorDialog(_('Incompatible Option'), str(ex), self.dialog).run()
|
dialogs.ErrorDialog(_('Incompatible Option'), str(ex), self.dialog).run()
|
||||||
|
|
||||||
def on_error_show(self, result):
|
def on_error_show(self, result):
|
||||||
d = dialogs.ErrorDialog(_('Error'), result.value.exception_msg, self.dialog)
|
d = dialogs.ErrorDialog(_('Error'), result.value.message, self.dialog)
|
||||||
result.cleanFailure()
|
result.cleanFailure()
|
||||||
d.run()
|
d.run()
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
|
# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
|
||||||
#
|
#
|
||||||
@ -12,8 +11,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from deluge.plugins.pluginbase import WebPluginBase
|
from deluge.plugins.pluginbase import WebPluginBase
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
|
# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
|
||||||
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
|
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
|
||||||
#
|
#
|
||||||
@ -7,8 +6,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from deluge.plugins.init import PluginInitBase
|
from deluge.plugins.init import PluginInitBase
|
||||||
|
|
||||||
|
|
||||||
@ -17,7 +14,7 @@ class CorePlugin(PluginInitBase):
|
|||||||
from .core import Core as _pluginCls
|
from .core import Core as _pluginCls
|
||||||
|
|
||||||
self._plugin_cls = _pluginCls
|
self._plugin_cls = _pluginCls
|
||||||
super(CorePlugin, self).__init__(plugin_name)
|
super().__init__(plugin_name)
|
||||||
|
|
||||||
|
|
||||||
class GtkUIPlugin(PluginInitBase):
|
class GtkUIPlugin(PluginInitBase):
|
||||||
@ -25,7 +22,7 @@ class GtkUIPlugin(PluginInitBase):
|
|||||||
from .gtkui import GtkUI as _pluginCls
|
from .gtkui import GtkUI as _pluginCls
|
||||||
|
|
||||||
self._plugin_cls = _pluginCls
|
self._plugin_cls = _pluginCls
|
||||||
super(GtkUIPlugin, self).__init__(plugin_name)
|
super().__init__(plugin_name)
|
||||||
|
|
||||||
|
|
||||||
class WebUIPlugin(PluginInitBase):
|
class WebUIPlugin(PluginInitBase):
|
||||||
@ -33,4 +30,4 @@ class WebUIPlugin(PluginInitBase):
|
|||||||
from .webui import WebUI as _pluginCls
|
from .webui import WebUI as _pluginCls
|
||||||
|
|
||||||
self._plugin_cls = _pluginCls
|
self._plugin_cls = _pluginCls
|
||||||
super(WebUIPlugin, self).__init__(plugin_name)
|
super().__init__(plugin_name)
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Basic plugin template created by:
|
# Basic plugin template created by:
|
||||||
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
||||||
@ -12,13 +11,10 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import os.path
|
import os.path
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from sys import exc_info
|
from sys import exc_info
|
||||||
|
|
||||||
import six
|
|
||||||
from pkg_resources import resource_filename
|
from pkg_resources import resource_filename
|
||||||
|
|
||||||
|
|
||||||
@ -47,7 +43,7 @@ def raises_errors_as(error):
|
|||||||
return func(self, *args, **kwargs)
|
return func(self, *args, **kwargs)
|
||||||
except Exception:
|
except Exception:
|
||||||
(value, tb) = exc_info()[1:]
|
(value, tb) = exc_info()[1:]
|
||||||
six.reraise(error, value, tb)
|
raise error(value).with_traceback(tb) from None
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
@ -74,7 +70,7 @@ class BadIP(Exception):
|
|||||||
_message = None
|
_message = None
|
||||||
|
|
||||||
def __init__(self, message):
|
def __init__(self, message):
|
||||||
super(BadIP, self).__init__(message)
|
super().__init__(message)
|
||||||
|
|
||||||
def __set_message(self, message):
|
def __set_message(self, message):
|
||||||
self._message = message
|
self._message = message
|
||||||
@ -86,7 +82,7 @@ class BadIP(Exception):
|
|||||||
del __get_message, __set_message
|
del __get_message, __set_message
|
||||||
|
|
||||||
|
|
||||||
class IP(object):
|
class IP:
|
||||||
__slots__ = ('q1', 'q2', 'q3', 'q4', '_long')
|
__slots__ = ('q1', 'q2', 'q3', 'q4', '_long')
|
||||||
|
|
||||||
def __init__(self, q1, q2, q3, q4):
|
def __init__(self, q1, q2, q3, q4):
|
||||||
@ -109,7 +105,7 @@ class IP(object):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def parse(cls, ip):
|
def parse(cls, ip):
|
||||||
try:
|
try:
|
||||||
q1, q2, q3, q4 = [int(q) for q in ip.split('.')]
|
q1, q2, q3, q4 = (int(q) for q in ip.split('.'))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise BadIP(_('The IP address "%s" is badly formed' % ip))
|
raise BadIP(_('The IP address "%s" is badly formed' % ip))
|
||||||
if q1 < 0 or q2 < 0 or q3 < 0 or q4 < 0:
|
if q1 < 0 or q2 < 0 or q3 < 0 or q4 < 0:
|
||||||
@ -169,7 +165,7 @@ class IP(object):
|
|||||||
return self.long == other.long
|
return self.long == other.long
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<%s long=%s address="%s">' % (
|
return '<{} long={} address="{}">'.format(
|
||||||
self.__class__.__name__,
|
self.__class__.__name__,
|
||||||
self.long,
|
self.long,
|
||||||
self.address,
|
self.address,
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com>
|
||||||
# Copyright (C) 2009-2010 John Garland <johnnybg+deluge@gmail.com>
|
# Copyright (C) 2009-2010 John Garland <johnnybg+deluge@gmail.com>
|
||||||
@ -8,14 +7,13 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import division, unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from email.utils import formatdate
|
from email.utils import formatdate
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
from twisted.internet import defer, threads
|
from twisted.internet import defer, threads
|
||||||
from twisted.internet.task import LoopingCall
|
from twisted.internet.task import LoopingCall
|
||||||
@ -32,12 +30,6 @@ from .common import IP, BadIP
|
|||||||
from .detect import UnknownFormatError, create_reader, detect_compression, detect_format
|
from .detect import UnknownFormatError, create_reader, detect_compression, detect_format
|
||||||
from .readers import ReaderParseError
|
from .readers import ReaderParseError
|
||||||
|
|
||||||
try:
|
|
||||||
from urllib.parse import urljoin
|
|
||||||
except ImportError:
|
|
||||||
# PY2 fallback
|
|
||||||
from urlparse import urljoin # pylint: disable=ungrouped-imports
|
|
||||||
|
|
||||||
# TODO: check return values for deferred callbacks
|
# TODO: check return values for deferred callbacks
|
||||||
# TODO: review class attributes for redundancy
|
# TODO: review class attributes for redundancy
|
||||||
|
|
||||||
|
|||||||
@ -55,7 +55,7 @@ Deluge.ux.preferences.BlocklistPage = Ext.extend(Ext.Panel, {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.checkListDays = this.SettingsFset.add({
|
this.checkListDays = this.SettingsFset.add({
|
||||||
fieldLabel: _('Check for new list every:'),
|
fieldLabel: _('Check for new list every (days):'),
|
||||||
labelSeparator: '',
|
labelSeparator: '',
|
||||||
name: 'check_list_days',
|
name: 'check_list_days',
|
||||||
value: 4,
|
value: 4,
|
||||||
|
|||||||
@ -53,8 +53,6 @@
|
|||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="invisible_char">●</property>
|
<property name="invisible_char">●</property>
|
||||||
<property name="primary_icon_activatable">False</property>
|
|
||||||
<property name="secondary_icon_activatable">False</property>
|
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="expand">True</property>
|
<property name="expand">True</property>
|
||||||
@ -124,8 +122,6 @@
|
|||||||
<object class="GtkSpinButton" id="spin_check_days">
|
<object class="GtkSpinButton" id="spin_check_days">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="primary_icon_activatable">False</property>
|
|
||||||
<property name="secondary_icon_activatable">False</property>
|
|
||||||
<property name="adjustment">adjustment1</property>
|
<property name="adjustment">adjustment1</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
@ -139,7 +135,7 @@
|
|||||||
<object class="GtkLabel" id="label4">
|
<object class="GtkLabel" id="label4">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="label" translatable="yes">Check for new list every:</property>
|
<property name="label" translatable="yes">Check for new list every (days):</property>
|
||||||
<property name="xalign">0</property>
|
<property name="xalign">0</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2009-2010 John Garland <johnnybg+deluge@gmail.com>
|
# Copyright (C) 2009-2010 John Garland <johnnybg+deluge@gmail.com>
|
||||||
#
|
#
|
||||||
@ -8,8 +7,6 @@
|
|||||||
#
|
#
|
||||||
# pylint: disable=redefined-builtin
|
# pylint: disable=redefined-builtin
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import bz2
|
import bz2
|
||||||
import gzip
|
import gzip
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2009-2010 John Garland <johnnybg+deluge@gmail.com>
|
# Copyright (C) 2009-2010 John Garland <johnnybg+deluge@gmail.com>
|
||||||
#
|
#
|
||||||
@ -7,8 +6,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from .decompressers import BZipped2, GZipped, Zipped
|
from .decompressers import BZipped2, GZipped, Zipped
|
||||||
from .readers import EmuleReader, PeerGuardianReader, SafePeerReader
|
from .readers import EmuleReader, PeerGuardianReader, SafePeerReader
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com>
|
||||||
#
|
#
|
||||||
@ -7,14 +6,12 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import gi # isort:skip (Required before Gtk import).
|
import gi # isort:skip (Required before Gtk import).
|
||||||
|
|
||||||
gi.require_version('Gtk', '3.0') # NOQA: E402
|
gi.require_version('Gtk', '3.0')
|
||||||
|
|
||||||
# isort:imports-thirdparty
|
# isort:imports-thirdparty
|
||||||
from gi.repository import Gtk
|
from gi.repository import Gtk
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2007 Steve 'Tarka' Smith (tarka@internode.on.net)
|
# Copyright (C) 2007 Steve 'Tarka' Smith (tarka@internode.on.net)
|
||||||
#
|
#
|
||||||
@ -7,8 +6,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import gzip
|
import gzip
|
||||||
import logging
|
import logging
|
||||||
import socket
|
import socket
|
||||||
@ -23,14 +20,14 @@ class PGException(Exception):
|
|||||||
|
|
||||||
# Incrementally reads PeerGuardian blocklists v1 and v2.
|
# Incrementally reads PeerGuardian blocklists v1 and v2.
|
||||||
# See http://wiki.phoenixlabs.org/wiki/P2B_Format
|
# See http://wiki.phoenixlabs.org/wiki/P2B_Format
|
||||||
class PGReader(object):
|
class PGReader:
|
||||||
def __init__(self, filename):
|
def __init__(self, filename):
|
||||||
log.debug('PGReader loading: %s', filename)
|
log.debug('PGReader loading: %s', filename)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with gzip.open(filename, 'rb') as _file:
|
with gzip.open(filename, 'rb') as _file:
|
||||||
self.fd = _file
|
self.fd = _file
|
||||||
except IOError:
|
except OSError:
|
||||||
log.debug('Blocklist: PGReader: Incorrect file type or list is corrupt')
|
log.debug('Blocklist: PGReader: Incorrect file type or list is corrupt')
|
||||||
|
|
||||||
# 4 bytes, should be 0xffffffff
|
# 4 bytes, should be 0xffffffff
|
||||||
@ -65,8 +62,5 @@ class PGReader(object):
|
|||||||
|
|
||||||
return (start, end)
|
return (start, end)
|
||||||
|
|
||||||
# Python 2 compatibility
|
|
||||||
next = __next__
|
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
self.fd.close()
|
self.fd.close()
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2009-2010 John Garland <johnnybg+deluge@gmail.com>
|
# Copyright (C) 2009-2010 John Garland <johnnybg+deluge@gmail.com>
|
||||||
#
|
#
|
||||||
@ -7,8 +6,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@ -23,7 +20,7 @@ class ReaderParseError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class BaseReader(object):
|
class BaseReader:
|
||||||
"""Base reader for blocklist files"""
|
"""Base reader for blocklist files"""
|
||||||
|
|
||||||
def __init__(self, _file):
|
def __init__(self, _file):
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
||||||
#
|
#
|
||||||
@ -7,8 +6,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from deluge.plugins.pluginbase import WebPluginBase
|
from deluge.plugins.pluginbase import WebPluginBase
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
||||||
#
|
#
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
|
# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
|
||||||
#
|
#
|
||||||
@ -7,8 +6,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from deluge.plugins.init import PluginInitBase
|
from deluge.plugins.init import PluginInitBase
|
||||||
|
|
||||||
|
|
||||||
@ -17,7 +14,7 @@ class CorePlugin(PluginInitBase):
|
|||||||
from .core import Core as _pluginCls
|
from .core import Core as _pluginCls
|
||||||
|
|
||||||
self._plugin_cls = _pluginCls
|
self._plugin_cls = _pluginCls
|
||||||
super(CorePlugin, self).__init__(plugin_name)
|
super().__init__(plugin_name)
|
||||||
|
|
||||||
|
|
||||||
class GtkUIPlugin(PluginInitBase):
|
class GtkUIPlugin(PluginInitBase):
|
||||||
@ -25,7 +22,7 @@ class GtkUIPlugin(PluginInitBase):
|
|||||||
from .gtkui import GtkUI as _pluginCls
|
from .gtkui import GtkUI as _pluginCls
|
||||||
|
|
||||||
self._plugin_cls = _pluginCls
|
self._plugin_cls = _pluginCls
|
||||||
super(GtkUIPlugin, self).__init__(plugin_name)
|
super().__init__(plugin_name)
|
||||||
|
|
||||||
|
|
||||||
class WebUIPlugin(PluginInitBase):
|
class WebUIPlugin(PluginInitBase):
|
||||||
@ -33,4 +30,4 @@ class WebUIPlugin(PluginInitBase):
|
|||||||
from .webui import WebUI as _pluginCls
|
from .webui import WebUI as _pluginCls
|
||||||
|
|
||||||
self._plugin_cls = _pluginCls
|
self._plugin_cls = _pluginCls
|
||||||
super(WebUIPlugin, self).__init__(plugin_name)
|
super().__init__(plugin_name)
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Basic plugin template created by:
|
# Basic plugin template created by:
|
||||||
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
||||||
@ -12,8 +11,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import os.path
|
import os.path
|
||||||
|
|
||||||
from pkg_resources import resource_filename
|
from pkg_resources import resource_filename
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
||||||
#
|
#
|
||||||
@ -7,8 +6,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|||||||
@ -71,8 +71,6 @@
|
|||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="can_default">True</property>
|
<property name="can_default">True</property>
|
||||||
<property name="has_default">True</property>
|
<property name="has_default">True</property>
|
||||||
<property name="primary_icon_activatable">False</property>
|
|
||||||
<property name="secondary_icon_activatable">False</property>
|
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="left_attach">1</property>
|
<property name="left_attach">1</property>
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
|
# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
|
||||||
#
|
#
|
||||||
@ -7,13 +6,11 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import gi # isort:skip (Required before Gtk import).
|
import gi # isort:skip (Required before Gtk import).
|
||||||
|
|
||||||
gi.require_version('Gtk', '3.0') # NOQA: E402
|
gi.require_version('Gtk', '3.0')
|
||||||
|
|
||||||
# isort:imports-thirdparty
|
# isort:imports-thirdparty
|
||||||
from gi.repository import Gtk
|
from gi.repository import Gtk
|
||||||
@ -41,7 +38,7 @@ EVENT_MAP = {
|
|||||||
EVENTS = ['complete', 'added', 'removed']
|
EVENTS = ['complete', 'added', 'removed']
|
||||||
|
|
||||||
|
|
||||||
class ExecutePreferences(object):
|
class ExecutePreferences:
|
||||||
def __init__(self, plugin):
|
def __init__(self, plugin):
|
||||||
self.plugin = plugin
|
self.plugin = plugin
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
|
# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
|
||||||
#
|
#
|
||||||
@ -7,8 +6,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from deluge.plugins.pluginbase import WebPluginBase
|
from deluge.plugins.pluginbase import WebPluginBase
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
|
# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
|
||||||
#
|
#
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
||||||
#
|
#
|
||||||
@ -11,8 +10,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from deluge.plugins.init import PluginInitBase
|
from deluge.plugins.init import PluginInitBase
|
||||||
|
|
||||||
|
|
||||||
@ -21,7 +18,7 @@ class CorePlugin(PluginInitBase):
|
|||||||
from .core import Core as _pluginCls
|
from .core import Core as _pluginCls
|
||||||
|
|
||||||
self._plugin_cls = _pluginCls
|
self._plugin_cls = _pluginCls
|
||||||
super(CorePlugin, self).__init__(plugin_name)
|
super().__init__(plugin_name)
|
||||||
|
|
||||||
|
|
||||||
class GtkUIPlugin(PluginInitBase):
|
class GtkUIPlugin(PluginInitBase):
|
||||||
@ -29,7 +26,7 @@ class GtkUIPlugin(PluginInitBase):
|
|||||||
from .gtkui import GtkUI as _pluginCls
|
from .gtkui import GtkUI as _pluginCls
|
||||||
|
|
||||||
self._plugin_cls = _pluginCls
|
self._plugin_cls = _pluginCls
|
||||||
super(GtkUIPlugin, self).__init__(plugin_name)
|
super().__init__(plugin_name)
|
||||||
|
|
||||||
|
|
||||||
class WebUIPlugin(PluginInitBase):
|
class WebUIPlugin(PluginInitBase):
|
||||||
@ -37,4 +34,4 @@ class WebUIPlugin(PluginInitBase):
|
|||||||
from .webui import WebUI as _pluginCls
|
from .webui import WebUI as _pluginCls
|
||||||
|
|
||||||
self._plugin_cls = _pluginCls
|
self._plugin_cls = _pluginCls
|
||||||
super(WebUIPlugin, self).__init__(plugin_name)
|
super().__init__(plugin_name)
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Basic plugin template created by:
|
# Basic plugin template created by:
|
||||||
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
||||||
@ -12,8 +11,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import os.path
|
import os.path
|
||||||
|
|
||||||
from pkg_resources import resource_filename
|
from pkg_resources import resource_filename
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
||||||
#
|
#
|
||||||
@ -11,8 +10,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import errno
|
import errno
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@ -37,14 +34,11 @@ if windows_check():
|
|||||||
'C:\\Program Files (x86)\\7-Zip\\7z.exe',
|
'C:\\Program Files (x86)\\7-Zip\\7z.exe',
|
||||||
]
|
]
|
||||||
|
|
||||||
try:
|
import winreg
|
||||||
import winreg
|
|
||||||
except ImportError:
|
|
||||||
import _winreg as winreg # For Python 2.
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
hkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, 'Software\\7-Zip')
|
hkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, 'Software\\7-Zip')
|
||||||
except WindowsError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
win_7z_path = os.path.join(winreg.QueryValueEx(hkey, 'Path')[0], '7z.exe')
|
win_7z_path = os.path.join(winreg.QueryValueEx(hkey, 'Path')[0], '7z.exe')
|
||||||
|
|||||||
@ -62,8 +62,6 @@
|
|||||||
<child>
|
<child>
|
||||||
<object class="GtkEntry" id="entry_path">
|
<object class="GtkEntry" id="entry_path">
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="primary_icon_activatable">False</property>
|
|
||||||
<property name="secondary_icon_activatable">False</property>
|
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="expand">True</property>
|
<property name="expand">True</property>
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
||||||
#
|
#
|
||||||
@ -11,13 +10,11 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import gi # isort:skip (Required before Gtk import).
|
import gi # isort:skip (Required before Gtk import).
|
||||||
|
|
||||||
gi.require_version('Gtk', '3.0') # NOQA: E402
|
gi.require_version('Gtk', '3.0')
|
||||||
|
|
||||||
# isort:imports-thirdparty
|
# isort:imports-thirdparty
|
||||||
from gi.repository import Gtk
|
from gi.repository import Gtk
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
||||||
#
|
#
|
||||||
@ -11,8 +10,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from deluge.plugins.pluginbase import WebPluginBase
|
from deluge.plugins.pluginbase import WebPluginBase
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
||||||
#
|
#
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
||||||
#
|
#
|
||||||
@ -11,8 +10,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from deluge.plugins.init import PluginInitBase
|
from deluge.plugins.init import PluginInitBase
|
||||||
|
|
||||||
|
|
||||||
@ -21,7 +18,7 @@ class CorePlugin(PluginInitBase):
|
|||||||
from .core import Core as _pluginCls
|
from .core import Core as _pluginCls
|
||||||
|
|
||||||
self._plugin_cls = _pluginCls
|
self._plugin_cls = _pluginCls
|
||||||
super(CorePlugin, self).__init__(plugin_name)
|
super().__init__(plugin_name)
|
||||||
|
|
||||||
|
|
||||||
class GtkUIPlugin(PluginInitBase):
|
class GtkUIPlugin(PluginInitBase):
|
||||||
@ -29,7 +26,7 @@ class GtkUIPlugin(PluginInitBase):
|
|||||||
from .gtkui import GtkUI as _pluginCls
|
from .gtkui import GtkUI as _pluginCls
|
||||||
|
|
||||||
self._plugin_cls = _pluginCls
|
self._plugin_cls = _pluginCls
|
||||||
super(GtkUIPlugin, self).__init__(plugin_name)
|
super().__init__(plugin_name)
|
||||||
|
|
||||||
|
|
||||||
class WebUIPlugin(PluginInitBase):
|
class WebUIPlugin(PluginInitBase):
|
||||||
@ -37,4 +34,4 @@ class WebUIPlugin(PluginInitBase):
|
|||||||
from .webui import WebUI as _pluginCls
|
from .webui import WebUI as _pluginCls
|
||||||
|
|
||||||
self._plugin_cls = _pluginCls
|
self._plugin_cls = _pluginCls
|
||||||
super(WebUIPlugin, self).__init__(plugin_name)
|
super().__init__(plugin_name)
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Basic plugin template created by:
|
# Basic plugin template created by:
|
||||||
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
||||||
@ -12,8 +11,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import os.path
|
import os.path
|
||||||
|
|
||||||
from pkg_resources import resource_filename
|
from pkg_resources import resource_filename
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
||||||
#
|
#
|
||||||
@ -15,8 +14,6 @@
|
|||||||
torrent-label core plugin.
|
torrent-label core plugin.
|
||||||
adds a status field for tracker.
|
adds a status field for tracker.
|
||||||
"""
|
"""
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
|||||||
@ -148,8 +148,7 @@ Deluge.ux.LabelOptionsWindow = Ext.extend(Ext.Window, {
|
|||||||
xtype: 'fieldset',
|
xtype: 'fieldset',
|
||||||
border: false,
|
border: false,
|
||||||
labelWidth: 1,
|
labelWidth: 1,
|
||||||
style:
|
style: 'margin-bottom: 0px; padding-bottom: 0px;',
|
||||||
'margin-bottom: 0px; padding-bottom: 0px;',
|
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
xtype: 'checkbox',
|
xtype: 'checkbox',
|
||||||
@ -218,8 +217,7 @@ Deluge.ux.LabelOptionsWindow = Ext.extend(Ext.Window, {
|
|||||||
xtype: 'fieldset',
|
xtype: 'fieldset',
|
||||||
border: false,
|
border: false,
|
||||||
labelWidth: 1,
|
labelWidth: 1,
|
||||||
style:
|
style: 'margin-bottom: 0px; padding-bottom: 0px;',
|
||||||
'margin-bottom: 0px; padding-bottom: 0px;',
|
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
xtype: 'checkbox',
|
xtype: 'checkbox',
|
||||||
@ -260,8 +258,7 @@ Deluge.ux.LabelOptionsWindow = Ext.extend(Ext.Window, {
|
|||||||
width: 60,
|
width: 60,
|
||||||
decimalPrecision: 2,
|
decimalPrecision: 2,
|
||||||
incrementValue: 0.1,
|
incrementValue: 0.1,
|
||||||
style:
|
style: 'position: relative; left: 100px',
|
||||||
'position: relative; left: 100px',
|
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -285,8 +282,7 @@ Deluge.ux.LabelOptionsWindow = Ext.extend(Ext.Window, {
|
|||||||
xtype: 'fieldset',
|
xtype: 'fieldset',
|
||||||
border: false,
|
border: false,
|
||||||
labelWidth: 1,
|
labelWidth: 1,
|
||||||
style:
|
style: 'margin-bottom: 0px; padding-bottom: 0px;',
|
||||||
'margin-bottom: 0px; padding-bottom: 0px;',
|
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
xtype: 'checkbox',
|
xtype: 'checkbox',
|
||||||
@ -339,8 +335,7 @@ Deluge.ux.LabelOptionsWindow = Ext.extend(Ext.Window, {
|
|||||||
xtype: 'fieldset',
|
xtype: 'fieldset',
|
||||||
border: false,
|
border: false,
|
||||||
labelWidth: 1,
|
labelWidth: 1,
|
||||||
style:
|
style: 'margin-bottom: 0px; padding-bottom: 0px;',
|
||||||
'margin-bottom: 0px; padding-bottom: 0px;',
|
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
xtype: 'checkbox',
|
xtype: 'checkbox',
|
||||||
@ -408,9 +403,8 @@ Deluge.ux.LabelOptionsWindow = Ext.extend(Ext.Window, {
|
|||||||
onOkClick: function () {
|
onOkClick: function () {
|
||||||
var values = this.form.getForm().getFieldValues();
|
var values = this.form.getForm().getFieldValues();
|
||||||
if (values['auto_add_trackers']) {
|
if (values['auto_add_trackers']) {
|
||||||
values['auto_add_trackers'] = values['auto_add_trackers'].split(
|
values['auto_add_trackers'] =
|
||||||
'\n'
|
values['auto_add_trackers'].split('\n');
|
||||||
);
|
|
||||||
}
|
}
|
||||||
deluge.client.label.set_options(this.label, values);
|
deluge.client.label.set_options(this.label, values);
|
||||||
this.hide();
|
this.hide();
|
||||||
|
|||||||
@ -141,8 +141,6 @@
|
|||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="activates_default">True</property>
|
<property name="activates_default">True</property>
|
||||||
<property name="primary_icon_activatable">False</property>
|
|
||||||
<property name="secondary_icon_activatable">False</property>
|
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="expand">True</property>
|
<property name="expand">True</property>
|
||||||
|
|||||||
@ -209,8 +209,6 @@
|
|||||||
<object class="GtkSpinButton" id="max_upload_speed">
|
<object class="GtkSpinButton" id="max_upload_speed">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="primary_icon_activatable">False</property>
|
|
||||||
<property name="secondary_icon_activatable">False</property>
|
|
||||||
<property name="adjustment">adjustment1</property>
|
<property name="adjustment">adjustment1</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
@ -239,8 +237,6 @@
|
|||||||
<object class="GtkSpinButton" id="max_download_speed">
|
<object class="GtkSpinButton" id="max_download_speed">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="primary_icon_activatable">False</property>
|
|
||||||
<property name="secondary_icon_activatable">False</property>
|
|
||||||
<property name="adjustment">adjustment2</property>
|
<property name="adjustment">adjustment2</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
@ -310,8 +306,6 @@
|
|||||||
<object class="GtkSpinButton" id="max_upload_slots">
|
<object class="GtkSpinButton" id="max_upload_slots">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="primary_icon_activatable">False</property>
|
|
||||||
<property name="secondary_icon_activatable">False</property>
|
|
||||||
<property name="adjustment">adjustment3</property>
|
<property name="adjustment">adjustment3</property>
|
||||||
<property name="numeric">True</property>
|
<property name="numeric">True</property>
|
||||||
</object>
|
</object>
|
||||||
@ -342,8 +336,6 @@
|
|||||||
<object class="GtkSpinButton" id="max_connections">
|
<object class="GtkSpinButton" id="max_connections">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="primary_icon_activatable">False</property>
|
|
||||||
<property name="secondary_icon_activatable">False</property>
|
|
||||||
<property name="adjustment">adjustment4</property>
|
<property name="adjustment">adjustment4</property>
|
||||||
<property name="numeric">True</property>
|
<property name="numeric">True</property>
|
||||||
</object>
|
</object>
|
||||||
@ -483,8 +475,6 @@
|
|||||||
<object class="GtkSpinButton" id="stop_ratio">
|
<object class="GtkSpinButton" id="stop_ratio">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="primary_icon_activatable">False</property>
|
|
||||||
<property name="secondary_icon_activatable">False</property>
|
|
||||||
<property name="adjustment">adjustment5</property>
|
<property name="adjustment">adjustment5</property>
|
||||||
<property name="digits">2</property>
|
<property name="digits">2</property>
|
||||||
</object>
|
</object>
|
||||||
@ -599,8 +589,6 @@
|
|||||||
<child>
|
<child>
|
||||||
<object class="GtkEntry" id="move_completed_path_entry">
|
<object class="GtkEntry" id="move_completed_path_entry">
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="primary_icon_activatable">False</property>
|
|
||||||
<property name="secondary_icon_activatable">False</property>
|
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="left_attach">1</property>
|
<property name="left_attach">1</property>
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
||||||
#
|
#
|
||||||
@ -7,8 +6,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from deluge import component # for systray
|
from deluge import component # for systray
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
||||||
#
|
#
|
||||||
@ -7,8 +6,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from gi.repository.Gtk import Builder
|
from gi.repository.Gtk import Builder
|
||||||
@ -20,7 +17,7 @@ from ..common import get_resource
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class LabelConfig(object):
|
class LabelConfig:
|
||||||
"""
|
"""
|
||||||
there used to be some options here...
|
there used to be some options here...
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
||||||
# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
|
||||||
@ -8,13 +7,11 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import gi # isort:skip (Required before Gtk import).
|
import gi # isort:skip (Required before Gtk import).
|
||||||
|
|
||||||
gi.require_version('Gtk', '3.0') # NOQA: E402
|
gi.require_version('Gtk', '3.0')
|
||||||
|
|
||||||
# isort:imports-thirdparty
|
# isort:imports-thirdparty
|
||||||
from gi.repository import Gtk
|
from gi.repository import Gtk
|
||||||
@ -32,7 +29,7 @@ NO_LABEL = 'No Label'
|
|||||||
|
|
||||||
|
|
||||||
# menu
|
# menu
|
||||||
class LabelSidebarMenu(object):
|
class LabelSidebarMenu:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
||||||
self.treeview = component.get('FilterTreeView')
|
self.treeview = component.get('FilterTreeView')
|
||||||
@ -107,7 +104,7 @@ class LabelSidebarMenu(object):
|
|||||||
|
|
||||||
|
|
||||||
# dialogs:
|
# dialogs:
|
||||||
class AddDialog(object):
|
class AddDialog:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -129,7 +126,7 @@ class AddDialog(object):
|
|||||||
self.dialog.destroy()
|
self.dialog.destroy()
|
||||||
|
|
||||||
|
|
||||||
class OptionsDialog(object):
|
class OptionsDialog:
|
||||||
spin_ids = ['max_download_speed', 'max_upload_speed', 'stop_ratio']
|
spin_ids = ['max_download_speed', 'max_upload_speed', 'stop_ratio']
|
||||||
spin_int_ids = ['max_upload_slots', 'max_connections']
|
spin_int_ids = ['max_upload_slots', 'max_connections']
|
||||||
chk_ids = [
|
chk_ids = [
|
||||||
@ -174,7 +171,7 @@ class OptionsDialog(object):
|
|||||||
self.builder.connect_signals(self)
|
self.builder.connect_signals(self)
|
||||||
# Show the label name in the header label
|
# Show the label name in the header label
|
||||||
self.builder.get_object('label_header').set_markup(
|
self.builder.get_object('label_header').set_markup(
|
||||||
'<b>%s:</b> %s' % (_('Label Options'), self.label)
|
'<b>{}:</b> {}'.format(_('Label Options'), self.label)
|
||||||
)
|
)
|
||||||
|
|
||||||
for chk_id, group in self.sensitive_groups:
|
for chk_id, group in self.sensitive_groups:
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
||||||
#
|
#
|
||||||
@ -8,8 +7,6 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from gi.repository.Gtk import Menu, MenuItem
|
from gi.repository.Gtk import Menu, MenuItem
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
@ -10,8 +9,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import print_function, unicode_literals
|
|
||||||
|
|
||||||
from deluge.ui.client import sclient
|
from deluge.ui.client import sclient
|
||||||
|
|
||||||
sclient.set_core_uri()
|
sclient.set_core_uri()
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
||||||
#
|
#
|
||||||
@ -11,8 +10,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from deluge.plugins.pluginbase import WebPluginBase
|
from deluge.plugins.pluginbase import WebPluginBase
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
||||||
#
|
#
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me>
|
# Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me>
|
||||||
#
|
#
|
||||||
@ -12,8 +11,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from deluge.plugins.init import PluginInitBase
|
from deluge.plugins.init import PluginInitBase
|
||||||
|
|
||||||
|
|
||||||
@ -22,7 +19,7 @@ class CorePlugin(PluginInitBase):
|
|||||||
from .core import Core as _pluginCls
|
from .core import Core as _pluginCls
|
||||||
|
|
||||||
self._plugin_cls = _pluginCls
|
self._plugin_cls = _pluginCls
|
||||||
super(CorePlugin, self).__init__(plugin_name)
|
super().__init__(plugin_name)
|
||||||
|
|
||||||
|
|
||||||
class GtkUIPlugin(PluginInitBase):
|
class GtkUIPlugin(PluginInitBase):
|
||||||
@ -30,7 +27,7 @@ class GtkUIPlugin(PluginInitBase):
|
|||||||
from .gtkui import GtkUI as _pluginCls
|
from .gtkui import GtkUI as _pluginCls
|
||||||
|
|
||||||
self._plugin_cls = _pluginCls
|
self._plugin_cls = _pluginCls
|
||||||
super(GtkUIPlugin, self).__init__(plugin_name)
|
super().__init__(plugin_name)
|
||||||
|
|
||||||
|
|
||||||
class WebUIPlugin(PluginInitBase):
|
class WebUIPlugin(PluginInitBase):
|
||||||
@ -38,4 +35,4 @@ class WebUIPlugin(PluginInitBase):
|
|||||||
from .webui import WebUI as _pluginCls
|
from .webui import WebUI as _pluginCls
|
||||||
|
|
||||||
self._plugin_cls = _pluginCls
|
self._plugin_cls = _pluginCls
|
||||||
super(WebUIPlugin, self).__init__(plugin_name)
|
super().__init__(plugin_name)
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me>
|
# Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me>
|
||||||
#
|
#
|
||||||
@ -12,8 +11,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os.path
|
import os.path
|
||||||
|
|
||||||
@ -30,7 +27,7 @@ def get_resource(filename):
|
|||||||
return resource_filename(__package__, os.path.join('data', filename))
|
return resource_filename(__package__, os.path.join('data', filename))
|
||||||
|
|
||||||
|
|
||||||
class CustomNotifications(object):
|
class CustomNotifications:
|
||||||
def __init__(self, plugin_name=None):
|
def __init__(self, plugin_name=None):
|
||||||
self.custom_notifications = {'email': {}, 'popup': {}, 'blink': {}, 'sound': {}}
|
self.custom_notifications = {'email': {}, 'popup': {}, 'blink': {}, 'sound': {}}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me>
|
# Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me>
|
||||||
#
|
#
|
||||||
@ -12,8 +11,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import smtplib
|
import smtplib
|
||||||
from email.utils import formatdate
|
from email.utils import formatdate
|
||||||
@ -119,7 +116,6 @@ Date: %(date)s
|
|||||||
message = '\r\n'.join((headers + message).splitlines())
|
message = '\r\n'.join((headers + message).splitlines())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Python 2.6
|
|
||||||
server = smtplib.SMTP(
|
server = smtplib.SMTP(
|
||||||
self.config['smtp_host'], self.config['smtp_port'], timeout=60
|
self.config['smtp_host'], self.config['smtp_port'], timeout=60
|
||||||
)
|
)
|
||||||
@ -152,7 +148,7 @@ Date: %(date)s
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
server.sendmail(self.config['smtp_from'], to_addrs, message)
|
server.sendmail(self.config['smtp_from'], to_addrs, message.encode())
|
||||||
except smtplib.SMTPException as ex:
|
except smtplib.SMTPException as ex:
|
||||||
err_msg = (
|
err_msg = (
|
||||||
_('There was an error sending the notification email: %s') % ex
|
_('There was an error sending the notification email: %s') % ex
|
||||||
|
|||||||
@ -187,8 +187,6 @@
|
|||||||
<object class="GtkEntry" id="smtp_host">
|
<object class="GtkEntry" id="smtp_host">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="primary_icon_activatable">False</property>
|
|
||||||
<property name="secondary_icon_activatable">False</property>
|
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="left_attach">1</property>
|
<property name="left_attach">1</property>
|
||||||
@ -217,8 +215,6 @@
|
|||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="max_length">5</property>
|
<property name="max_length">5</property>
|
||||||
<property name="width_chars">5</property>
|
<property name="width_chars">5</property>
|
||||||
<property name="primary_icon_activatable">False</property>
|
|
||||||
<property name="secondary_icon_activatable">False</property>
|
|
||||||
<property name="adjustment">adjustment1</property>
|
<property name="adjustment">adjustment1</property>
|
||||||
<property name="climb_rate">1</property>
|
<property name="climb_rate">1</property>
|
||||||
<property name="numeric">True</property>
|
<property name="numeric">True</property>
|
||||||
@ -246,8 +242,6 @@
|
|||||||
<object class="GtkEntry" id="smtp_user">
|
<object class="GtkEntry" id="smtp_user">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="primary_icon_activatable">False</property>
|
|
||||||
<property name="secondary_icon_activatable">False</property>
|
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="left_attach">1</property>
|
<property name="left_attach">1</property>
|
||||||
@ -273,8 +267,6 @@
|
|||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="visibility">False</property>
|
<property name="visibility">False</property>
|
||||||
<property name="primary_icon_activatable">False</property>
|
|
||||||
<property name="secondary_icon_activatable">False</property>
|
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="left_attach">1</property>
|
<property name="left_attach">1</property>
|
||||||
@ -427,8 +419,6 @@
|
|||||||
<object class="GtkEntry" id="smtp_from">
|
<object class="GtkEntry" id="smtp_from">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="primary_icon_activatable">False</property>
|
|
||||||
<property name="secondary_icon_activatable">False</property>
|
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="left_attach">1</property>
|
<property name="left_attach">1</property>
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me>
|
# Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me>
|
||||||
#
|
#
|
||||||
@ -12,8 +11,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from os.path import basename
|
from os.path import basename
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# vim: sw=4 ts=4 fenc=utf-8 et
|
# vim: sw=4 ts=4 fenc=utf-8 et
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Copyright © 2009-2010 UfSoft.org - Pedro Algarvio <pedro@algarvio.me>
|
# Copyright © 2009-2010 UfSoft.org - Pedro Algarvio <pedro@algarvio.me>
|
||||||
@ -6,8 +5,6 @@
|
|||||||
# License: BSD - Please view the LICENSE file for additional information.
|
# License: BSD - Please view the LICENSE file for additional information.
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from twisted.internet import task
|
from twisted.internet import task
|
||||||
@ -70,14 +67,14 @@ class TestEmailNotifications(component.Component):
|
|||||||
|
|
||||||
def custom_email_message_provider(self, *evt_args, **evt_kwargs):
|
def custom_email_message_provider(self, *evt_args, **evt_kwargs):
|
||||||
log.debug('Running custom email message provider: %s %s', evt_args, evt_kwargs)
|
log.debug('Running custom email message provider: %s %s', evt_args, evt_kwargs)
|
||||||
subject = '%s Email Subject: %s' % (self.events[0].__class__.__name__, self.n)
|
subject = f'{self.events[0].__class__.__name__} Email Subject: {self.n}'
|
||||||
message = '%s Email Message: %s' % (self.events[0].__class__.__name__, self.n)
|
message = f'{self.events[0].__class__.__name__} Email Message: {self.n}'
|
||||||
return subject, message
|
return subject, message
|
||||||
|
|
||||||
def custom_popup_message_provider(self, *evt_args, **evt_kwargs):
|
def custom_popup_message_provider(self, *evt_args, **evt_kwargs):
|
||||||
log.debug('Running custom popup message provider: %s %s', evt_args, evt_kwargs)
|
log.debug('Running custom popup message provider: %s %s', evt_args, evt_kwargs)
|
||||||
title = '%s Popup Title: %s' % (self.events[0].__class__.__name__, self.n)
|
title = f'{self.events[0].__class__.__name__} Popup Title: {self.n}'
|
||||||
message = '%s Popup Message: %s' % (self.events[0].__class__.__name__, self.n)
|
message = f'{self.events[0].__class__.__name__} Popup Message: {self.n}'
|
||||||
return title, message
|
return title, message
|
||||||
|
|
||||||
def custom_blink_message_provider(self, *evt_args, **evt_kwargs):
|
def custom_blink_message_provider(self, *evt_args, **evt_kwargs):
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me>
|
# Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me>
|
||||||
#
|
#
|
||||||
@ -12,8 +11,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from deluge.plugins.pluginbase import WebPluginBase
|
from deluge.plugins.pluginbase import WebPluginBase
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me>
|
# Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me>
|
||||||
#
|
#
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
||||||
#
|
#
|
||||||
@ -11,8 +10,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from deluge.plugins.init import PluginInitBase
|
from deluge.plugins.init import PluginInitBase
|
||||||
|
|
||||||
|
|
||||||
@ -21,7 +18,7 @@ class CorePlugin(PluginInitBase):
|
|||||||
from .core import Core as _pluginCls
|
from .core import Core as _pluginCls
|
||||||
|
|
||||||
self._plugin_cls = _pluginCls
|
self._plugin_cls = _pluginCls
|
||||||
super(CorePlugin, self).__init__(plugin_name)
|
super().__init__(plugin_name)
|
||||||
|
|
||||||
|
|
||||||
class GtkUIPlugin(PluginInitBase):
|
class GtkUIPlugin(PluginInitBase):
|
||||||
@ -29,7 +26,7 @@ class GtkUIPlugin(PluginInitBase):
|
|||||||
from .gtkui import GtkUI as _pluginCls
|
from .gtkui import GtkUI as _pluginCls
|
||||||
|
|
||||||
self._plugin_cls = _pluginCls
|
self._plugin_cls = _pluginCls
|
||||||
super(GtkUIPlugin, self).__init__(plugin_name)
|
super().__init__(plugin_name)
|
||||||
|
|
||||||
|
|
||||||
class WebUIPlugin(PluginInitBase):
|
class WebUIPlugin(PluginInitBase):
|
||||||
@ -37,4 +34,4 @@ class WebUIPlugin(PluginInitBase):
|
|||||||
from .webui import WebUI as _pluginCls
|
from .webui import WebUI as _pluginCls
|
||||||
|
|
||||||
self._plugin_cls = _pluginCls
|
self._plugin_cls = _pluginCls
|
||||||
super(WebUIPlugin, self).__init__(plugin_name)
|
super().__init__(plugin_name)
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Basic plugin template created by:
|
# Basic plugin template created by:
|
||||||
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
|
||||||
@ -12,8 +11,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import os.path
|
import os.path
|
||||||
|
|
||||||
from pkg_resources import resource_filename
|
from pkg_resources import resource_filename
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
||||||
#
|
#
|
||||||
@ -11,8 +10,6 @@
|
|||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user