mirror of
https://git.deluge-torrent.org/deluge
synced 2025-04-20 11:35:49 +00:00
Compare commits
3 commits
develop
...
deluge-2.1
Author | SHA1 | Date | |
---|---|---|---|
|
0b5f45b486 | ||
|
05e13a6b20 | ||
|
20efcfd345 |
578 changed files with 335973 additions and 350108 deletions
32
.github/workflows/cd.yml
vendored
32
.github/workflows/cd.yml
vendored
|
@ -19,37 +19,37 @@ on:
|
|||
|
||||
jobs:
|
||||
windows_package:
|
||||
runs-on: windows-2022
|
||||
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.7, 1.2.19]
|
||||
libtorrent: [2.0.6, 1.2.15]
|
||||
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v4
|
||||
- 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@v4
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.inputs.ref }}
|
||||
fetch-depth: 0
|
||||
path: deluge_src
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python}}
|
||||
architecture: ${{ matrix.arch }}
|
||||
cache: pip
|
||||
|
||||
- name: Prepare pip
|
||||
run: python -m pip install wheel setuptools==68.*
|
||||
run: python -m pip install wheel
|
||||
|
||||
- name: Install GTK
|
||||
run: |
|
||||
|
@ -62,14 +62,11 @@ jobs:
|
|||
python -m pip install --no-index --find-links="C:\GTK\release\python" pycairo PyGObject
|
||||
|
||||
- name: Install Python dependencies
|
||||
# Pillow no longer provides 32-bit wheels for Windows
|
||||
# so specify only-binary to install old version.
|
||||
run: >
|
||||
python -m pip install
|
||||
--only-binary=pillow
|
||||
twisted[tls]==22.8.0
|
||||
twisted[tls]==22.4.0
|
||||
libtorrent==${{ matrix.libtorrent }}
|
||||
pyinstaller
|
||||
pyinstaller==4.10
|
||||
pygame
|
||||
-r requirements.txt
|
||||
|
||||
|
@ -84,13 +81,12 @@ jobs:
|
|||
run: |
|
||||
pyinstaller --clean delugewin.spec --distpath freeze
|
||||
|
||||
- name: Verify Deluge exes
|
||||
working-directory: packaging/win/freeze/Deluge/
|
||||
- name: Fix OpenSSL for libtorrent x64
|
||||
if: ${{ matrix.arch == 'x64' }}
|
||||
working-directory: packaging/win/freeze/Deluge
|
||||
run: |
|
||||
deluge-debug.exe -v
|
||||
deluged-debug.exe -v
|
||||
deluge-web-debug.exe -v
|
||||
deluge-console -v
|
||||
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
|
||||
|
@ -98,7 +94,7 @@ jobs:
|
|||
python setup_nsis.py
|
||||
makensis /Darch=${{ matrix.arch }} deluge-win-installer.nsi
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: deluge-py${{ matrix.python }}-lt${{ matrix.libtorrent }}-${{ matrix.arch }}
|
||||
path: packaging/win/*.exe
|
||||
|
|
36
.github/workflows/ci.yml
vendored
36
.github/workflows/ci.yml
vendored
|
@ -6,25 +6,22 @@ on:
|
|||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
core-dump:
|
||||
description: "Set to 1 to enable retrieving core dump from crashes"
|
||||
default: "0"
|
||||
|
||||
jobs:
|
||||
test-linux:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.7", "3.10"]
|
||||
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: "pip"
|
||||
|
@ -36,8 +33,8 @@ jobs:
|
|||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install --upgrade pip wheel setuptools
|
||||
pip install -r requirements-ci.txt
|
||||
pip install --upgrade pip wheel
|
||||
pip install -r requirements.txt -r requirements-tests.txt
|
||||
pip install -e .
|
||||
|
||||
- name: Install security dependencies
|
||||
|
@ -49,21 +46,18 @@ jobs:
|
|||
TESTSSL_VER: 3.0.6
|
||||
TESTSSL_URL: https://codeload.github.com/drwetter/testssl.sh/tar.gz/refs/tags/v
|
||||
|
||||
- name: Setup core dump catch and store
|
||||
if: github.event.inputs.core-dump == '1'
|
||||
- name: Setup core dump directory
|
||||
run: |
|
||||
sudo mkdir /cores/ && sudo chmod 777 /cores/
|
||||
echo "/cores/%E.%p" | sudo tee /proc/sys/kernel/core_pattern
|
||||
ulimit -c unlimited
|
||||
sudo apt install glibc-tools
|
||||
echo "DEBUG_PREFIX=catchsegv python -X dev -m" >> $GITHUB_ENV
|
||||
|
||||
- name: Test with pytest
|
||||
run: |
|
||||
ulimit -c unlimited # Enable core dumps to be captured
|
||||
python -c 'from deluge._libtorrent import lt; print(lt.__version__)';
|
||||
$DEBUG_PREFIX pytest -v -m "not (todo or gtkui)" deluge
|
||||
catchsegv python -X dev -m pytest -v -m "not (todo or gtkui)" deluge
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v2
|
||||
# capture all crashes as build artifacts
|
||||
if: failure()
|
||||
with:
|
||||
|
@ -71,19 +65,19 @@ jobs:
|
|||
path: /cores
|
||||
|
||||
test-windows:
|
||||
runs-on: windows-2022
|
||||
runs-on: windows-2019
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.7", "3.10"]
|
||||
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: "pip"
|
||||
|
@ -91,8 +85,8 @@ jobs:
|
|||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install --upgrade pip wheel setuptools
|
||||
pip install -r requirements-ci.txt
|
||||
pip install --upgrade pip wheel
|
||||
pip install -r requirements.txt -r requirements-tests.txt
|
||||
pip install -e .
|
||||
|
||||
- name: Test with pytest
|
||||
|
|
23
.github/workflows/docs.yml
vendored
23
.github/workflows/docs.yml
vendored
|
@ -15,23 +15,30 @@ jobs:
|
|||
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: "3.10"
|
||||
cache: "pip"
|
||||
cache-dependency-path: "requirements*.txt"
|
||||
python-version: "3.8"
|
||||
- name: Cache pip
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
# This path is specific to Ubuntu
|
||||
path: ~/.cache/pip
|
||||
# Look to see if there is a cache hit for the corresponding requirements file
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('requirements*.txt') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install --upgrade pip wheel
|
||||
pip install tox
|
||||
sudo apt-get install enchant-2
|
||||
sudo apt-get install enchant
|
||||
|
||||
- name: Build docs with tox
|
||||
- name: Test with tox
|
||||
env:
|
||||
TOX_ENV: docs
|
||||
run: |
|
||||
|
|
6
.github/workflows/lint.yml
vendored
6
.github/workflows/lint.yml
vendored
|
@ -11,7 +11,7 @@ jobs:
|
|||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
- name: Run pre-commit linting
|
||||
uses: pre-commit/action@v3.0.1
|
||||
uses: pre-commit/action@v2.0.2
|
||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -12,14 +12,14 @@ __pycache__/
|
|||
*.tar.*
|
||||
.tox/
|
||||
deluge/i18n/*/
|
||||
deluge.pot
|
||||
deluge/ui/web/js/*.js
|
||||
deluge/ui/web/js/extjs/ext-extensions*.js
|
||||
*.desktop
|
||||
*.metainfo.xml
|
||||
*.appdata.xml
|
||||
.build_data*
|
||||
osx/app
|
||||
RELEASE-VERSION
|
||||
.venv*
|
||||
# used by setuptools to cache downloaded eggs
|
||||
/.eggs
|
||||
_pytest_temp/
|
||||
|
|
|
@ -6,25 +6,35 @@ exclude: >
|
|||
deluge/tests/data/.*svg|
|
||||
)$
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.6.4
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.3.0
|
||||
hooks:
|
||||
- id: ruff
|
||||
name: Chk Ruff
|
||||
args: [--fix]
|
||||
- id: ruff-format
|
||||
name: Fmt Ruff
|
||||
- id: black
|
||||
name: Fmt Black
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v2.7.1
|
||||
rev: v2.5.1
|
||||
hooks:
|
||||
- id: prettier
|
||||
name: Fmt Prettier
|
||||
# Workaround to list modified files only.
|
||||
args: [--list-different]
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.10.1
|
||||
hooks:
|
||||
- id: isort
|
||||
name: Fmt isort
|
||||
- repo: https://github.com/pycqa/flake8
|
||||
rev: 4.0.1
|
||||
hooks:
|
||||
- id: flake8
|
||||
name: Chk Flake8
|
||||
additional_dependencies:
|
||||
- pep8-naming==0.12.1
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.1.0
|
||||
hooks:
|
||||
- id: double-quote-string-fixer
|
||||
name: Fix Double-quotes
|
||||
- id: end-of-file-fixer
|
||||
name: Fix End-of-files
|
||||
exclude_types: [javascript, css]
|
||||
|
@ -34,8 +44,8 @@ repos:
|
|||
- id: trailing-whitespace
|
||||
name: Fix Trailing whitespace
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.3.1
|
||||
rev: v2.31.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py37-plus]
|
||||
args: [--py36-plus]
|
||||
stages: [manual]
|
||||
|
|
|
@ -5,14 +5,6 @@
|
|||
# Required
|
||||
version: 2
|
||||
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3.10"
|
||||
jobs:
|
||||
post_checkout:
|
||||
- git fetch --unshallow || true
|
||||
|
||||
# Build documentation in the docs/ directory with Sphinx
|
||||
sphinx:
|
||||
configuration: docs/source/conf.py
|
||||
|
@ -22,8 +14,9 @@ formats: all
|
|||
|
||||
# Optionally set the version of Python and requirements required to build your docs
|
||||
python:
|
||||
version: 3.7
|
||||
install:
|
||||
- requirements: requirements.txt
|
||||
- requirements: docs/requirements.txt
|
||||
- method: pip
|
||||
- method: setuptools
|
||||
path: .
|
||||
|
|
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -1,15 +1,5 @@
|
|||
# Changelog
|
||||
|
||||
## 2.2.x (TBA)
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- Python 3.6 support removed (Python >= 3.7)
|
||||
|
||||
### Web UI
|
||||
|
||||
- Accept network interface name in addition to IP adress in "Incoming Address"
|
||||
|
||||
## 2.1.1 (2022-07-10)
|
||||
|
||||
### Core
|
||||
|
|
|
@ -50,7 +50,7 @@ All modules will require the [common](#common) section dependencies.
|
|||
- [PyGObject]
|
||||
- [Pycairo]
|
||||
- [librsvg] _>= 2_
|
||||
- [ayatanaappindicator3] w/GIR - Optional: Ubuntu system tray icon.
|
||||
- [libappindicator3] w/GIR - Optional: Ubuntu system tray icon.
|
||||
|
||||
### MacOS
|
||||
|
||||
|
@ -95,6 +95,6 @@ All modules will require the [common](#common) section dependencies.
|
|||
[mako]: https://www.makotemplates.org/
|
||||
[pygame]: https://www.pygame.org/
|
||||
[libnotify]: https://developer.gnome.org/libnotify/
|
||||
[ayatanaappindicator3]: https://lazka.github.io/pgi-docs/AyatanaAppIndicator3-0.1/index.html
|
||||
[python-appindicator]: https://packages.ubuntu.com/xenial/python-appindicator
|
||||
[librsvg]: https://wiki.gnome.org/action/show/Projects/LibRsvg
|
||||
[ifaddr]: https://pypi.org/project/ifaddr/
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
from twisted.web.http import Request
|
||||
|
||||
__request__: Request
|
||||
|
||||
def _(string: str) -> str: ...
|
||||
def _n(string: str) -> str: ...
|
|
@ -14,7 +14,6 @@ Example:
|
|||
>>> from deluge._libtorrent import lt
|
||||
|
||||
"""
|
||||
|
||||
from deluge.common import VersionSplit, get_version
|
||||
from deluge.error import LibtorrentImportError
|
||||
|
||||
|
|
|
@ -85,6 +85,7 @@ def bdecode(x):
|
|||
|
||||
|
||||
class Bencached:
|
||||
|
||||
__slots__ = ['bencoded']
|
||||
|
||||
def __init__(self, s):
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
#
|
||||
|
||||
"""Common functions for various parts of Deluge to use."""
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
import functools
|
||||
|
@ -24,21 +23,15 @@ import tarfile
|
|||
import time
|
||||
from contextlib import closing
|
||||
from datetime import datetime
|
||||
from importlib import resources
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from urllib.parse import unquote_plus, urljoin
|
||||
from urllib.request import pathname2url
|
||||
|
||||
import pkg_resources
|
||||
|
||||
from deluge.decorators import deprecated
|
||||
from deluge.error import InvalidPathError
|
||||
|
||||
try:
|
||||
from importlib.metadata import distribution
|
||||
except ImportError:
|
||||
from pkg_resources import get_distribution as distribution
|
||||
|
||||
|
||||
try:
|
||||
import chardet
|
||||
except ImportError:
|
||||
|
@ -97,7 +90,7 @@ def get_version():
|
|||
Returns:
|
||||
str: The version of Deluge.
|
||||
"""
|
||||
return distribution('Deluge').version
|
||||
return pkg_resources.get_distribution('Deluge').version
|
||||
|
||||
|
||||
def get_default_config_dir(filename=None):
|
||||
|
@ -297,22 +290,20 @@ def get_pixmap(fname):
|
|||
return resource_filename('deluge', os.path.join('ui', 'data', 'pixmaps', fname))
|
||||
|
||||
|
||||
def resource_filename(module: str, path: str) -> str:
|
||||
"""Get filesystem path for a non-python resource.
|
||||
def resource_filename(module, path):
|
||||
"""Get filesystem path for a resource.
|
||||
|
||||
Abstracts getting module resource files. Originally created to
|
||||
workaround pkg_resources.resource_filename limitations with
|
||||
multiple Deluge packages installed.
|
||||
This function contains a work-around for pkg_resources.resource_filename
|
||||
not returning the correct path with multiple packages installed.
|
||||
|
||||
So if there's a second deluge package, installed globally and another in
|
||||
develop mode somewhere else, while pkg_resources.get_distribution('Deluge')
|
||||
returns the proper deluge instance, pkg_resources.resource_filename
|
||||
does not, it returns the first found on the python path, which is wrong.
|
||||
"""
|
||||
path = Path(path)
|
||||
|
||||
try:
|
||||
with resources.as_file(resources.files(module) / path) as resource_file:
|
||||
return str(resource_file)
|
||||
except AttributeError:
|
||||
# Python <= 3.8
|
||||
with resources.path(module, path.parts[0]) as resource_file:
|
||||
return str(resource_file.joinpath(*path.parts[1:]))
|
||||
return pkg_resources.get_distribution('Deluge').get_resource_filename(
|
||||
pkg_resources._manager, os.path.join(*(module.split('.') + [path]))
|
||||
)
|
||||
|
||||
|
||||
def open_file(path, timestamp=None):
|
||||
|
@ -424,31 +415,25 @@ def translate_size_units():
|
|||
|
||||
|
||||
def fsize(fsize_b, precision=1, shortform=False):
|
||||
"""Formats the bytes value into a string with KiB, MiB, GiB or TiB units.
|
||||
"""Formats the bytes value into a string with KiB, MiB or GiB units.
|
||||
|
||||
Args:
|
||||
fsize_b (int): The filesize in bytes.
|
||||
precision (int): The output float precision, 1 by default.
|
||||
shortform (bool): The output short|long form, False (long form) by default.
|
||||
precision (int): The filesize float precision.
|
||||
|
||||
Returns:
|
||||
str: A formatted string in KiB, MiB, GiB or TiB units.
|
||||
str: A formatted string in KiB, MiB or GiB units.
|
||||
|
||||
Examples:
|
||||
>>> fsize(112245)
|
||||
'109.6 KiB'
|
||||
>>> fsize(112245, precision=0)
|
||||
'110 KiB'
|
||||
>>> fsize(112245, shortform=True)
|
||||
'109.6 K'
|
||||
|
||||
Note:
|
||||
This function has been refactored for performance with the
|
||||
fsize units being translated outside the function.
|
||||
|
||||
Notice that short forms K|M|G|T are synonymous here with
|
||||
KiB|MiB|GiB|TiB. They are powers of 1024, not 1000.
|
||||
|
||||
"""
|
||||
|
||||
if fsize_b >= 1024**4:
|
||||
|
@ -484,7 +469,7 @@ def fpcnt(dec, precision=2):
|
|||
|
||||
Args:
|
||||
dec (float): The ratio in the range [0.0, 1.0].
|
||||
precision (int): The output float precision, 2 by default.
|
||||
precision (int): The percentage float precision.
|
||||
|
||||
Returns:
|
||||
str: A formatted string representing a percentage.
|
||||
|
@ -508,8 +493,6 @@ def fspeed(bps, precision=1, shortform=False):
|
|||
|
||||
Args:
|
||||
bps (int): The speed in bytes per second.
|
||||
precision (int): The output float precision, 1 by default.
|
||||
shortform (bool): The output short|long form, False (long form) by default.
|
||||
|
||||
Returns:
|
||||
str: A formatted string representing transfer speed.
|
||||
|
@ -518,10 +501,6 @@ def fspeed(bps, precision=1, shortform=False):
|
|||
>>> fspeed(43134)
|
||||
'42.1 KiB/s'
|
||||
|
||||
Note:
|
||||
Notice that short forms K|M|G|T are synonymous here with
|
||||
KiB|MiB|GiB|TiB. They are powers of 1024, not 1000.
|
||||
|
||||
"""
|
||||
|
||||
if bps < 1024**2:
|
||||
|
@ -558,7 +537,7 @@ def fpeer(num_peers, total_peers):
|
|||
total_peers (int): The total number of peers.
|
||||
|
||||
Returns:
|
||||
str: A formatted string 'num_peers (total_peers)' or if total_peers < 0, just 'num_peers'.
|
||||
str: A formatted string 'num_peers (total_peers)' or total_peers < 0, just 'num_peers'.
|
||||
|
||||
Examples:
|
||||
>>> fpeer(10, 20)
|
||||
|
@ -607,16 +586,16 @@ def ftime(secs):
|
|||
time_str = f'{secs // 604800}w {secs // 86400 % 7}d'
|
||||
else:
|
||||
time_str = f'{secs // 31449600}y {secs // 604800 % 52}w'
|
||||
|
||||
return time_str
|
||||
|
||||
|
||||
def fdate(seconds, date_only=False, precision_secs=False):
|
||||
"""Formats a date time string in the locale's date representation based on the system's timezone.
|
||||
"""Formats a date time string in the locale's date representation based on the systems timezone.
|
||||
|
||||
Args:
|
||||
seconds (float): Time in seconds since the Epoch.
|
||||
date_only (bool): Whether to include only the date, False by default.
|
||||
precision_secs (bool): Include seconds in time format, False by default.
|
||||
precision_secs (bool): Include seconds in time format.
|
||||
|
||||
Returns:
|
||||
str: A string in the locale's datetime representation or "" if seconds < 0
|
||||
|
@ -641,14 +620,10 @@ def tokenize(text):
|
|||
Returns:
|
||||
list: A list of strings and/or numbers.
|
||||
|
||||
Note:
|
||||
This function is used to implement robust tokenization of user input
|
||||
It automatically coerces integer and floating point numbers, ignores
|
||||
whitespace and knows how to separate numbers from strings even without
|
||||
whitespace.
|
||||
|
||||
Possible optimization: move the 2 regexes outside of function.
|
||||
|
||||
This function is used to implement robust tokenization of user input
|
||||
It automatically coerces integer and floating point numbers, ignores
|
||||
whitespace and knows how to separate numbers from strings even without
|
||||
whitespace.
|
||||
"""
|
||||
tokenized_input = []
|
||||
for token in re.split(r'(\d+(?:\.\d+)?)', text):
|
||||
|
@ -669,16 +644,12 @@ size_units = [
|
|||
{'prefix': 'GiB', 'divider': 1024**3},
|
||||
{'prefix': 'TiB', 'divider': 1024**4},
|
||||
{'prefix': 'PiB', 'divider': 1024**5},
|
||||
{'prefix': 'k', 'divider': 1000**1},
|
||||
{'prefix': 'm', 'divider': 1000**2},
|
||||
{'prefix': 'g', 'divider': 1000**3},
|
||||
{'prefix': 't', 'divider': 1000**4},
|
||||
{'prefix': 'p', 'divider': 1000**5},
|
||||
{'prefix': 'KB', 'divider': 1000**1},
|
||||
{'prefix': 'MB', 'divider': 1000**2},
|
||||
{'prefix': 'GB', 'divider': 1000**3},
|
||||
{'prefix': 'TB', 'divider': 1000**4},
|
||||
{'prefix': 'PB', 'divider': 1000**5},
|
||||
{'prefix': 'm', 'divider': 1000**2},
|
||||
]
|
||||
|
||||
|
||||
|
@ -721,16 +692,6 @@ def parse_human_size(size):
|
|||
raise InvalidSize(msg % (size, tokens))
|
||||
|
||||
|
||||
def anchorify_urls(text: str) -> str:
|
||||
"""
|
||||
Wrap all occurrences of text URLs with HTML
|
||||
"""
|
||||
url_pattern = r'((htt)|(ft)|(ud))ps?://\S+'
|
||||
html_href_pattern = r'<a href="\g<0>">\g<0></a>'
|
||||
|
||||
return re.sub(url_pattern, html_href_pattern, text)
|
||||
|
||||
|
||||
def is_url(url):
|
||||
"""
|
||||
A simple test to check if the URL is valid
|
||||
|
@ -872,7 +833,7 @@ def create_magnet_uri(infohash, name=None, trackers=None):
|
|||
Args:
|
||||
infohash (str): The info-hash of the torrent.
|
||||
name (str, optional): The name of the torrent.
|
||||
trackers (list or dict, optional): A list of trackers or a dict or some {tracker: tier} pairs.
|
||||
trackers (list or dict, optional): A list of trackers or dict or {tracker: tier} pairs.
|
||||
|
||||
Returns:
|
||||
str: A magnet URI string.
|
||||
|
@ -914,7 +875,7 @@ def get_path_size(path):
|
|||
return os.path.getsize(path)
|
||||
|
||||
dir_size = 0
|
||||
for p, dummy_dirs, files in os.walk(path):
|
||||
for (p, dummy_dirs, files) in os.walk(path):
|
||||
for _file in files:
|
||||
filename = os.path.join(p, _file)
|
||||
dir_size += os.path.getsize(filename)
|
||||
|
|
|
@ -59,16 +59,11 @@ class Component:
|
|||
Deluge core.
|
||||
|
||||
**update()** - This method is called every 1 second by default while the
|
||||
Component is in a *Started* state. The interval can be
|
||||
Componented is in a *Started* state. The interval can be
|
||||
specified during instantiation. The update() timer can be
|
||||
paused by instructing the :class:`ComponentRegistry` to pause
|
||||
this Component.
|
||||
|
||||
**pause()** - This method is called when the component is being paused.
|
||||
|
||||
**resume()** - This method is called when the component resumes from a Paused
|
||||
state.
|
||||
|
||||
**shutdown()** - This method is called when the client is exiting. If the
|
||||
Component is in a "Started" state when this is called, a
|
||||
call to stop() will be issued prior to shutdown().
|
||||
|
@ -85,10 +80,10 @@ class Component:
|
|||
|
||||
**Stopped** - The Component has either been stopped or has yet to be started.
|
||||
|
||||
**Stopping** - The Component has had its stop method called, but it hasn't
|
||||
**Stopping** - The Component has had it's stop method called, but it hasn't
|
||||
fully stopped yet.
|
||||
|
||||
**Paused** - The Component has had its update timer stopped, but will
|
||||
**Paused** - The Component has had it's update timer stopped, but will
|
||||
still be considered in a Started state.
|
||||
|
||||
"""
|
||||
|
@ -116,8 +111,9 @@ class Component:
|
|||
_ComponentRegistry.deregister(self)
|
||||
|
||||
def _component_start_timer(self):
|
||||
self._component_timer = LoopingCall(self.update)
|
||||
self._component_timer.start(self._component_interval)
|
||||
if hasattr(self, 'update'):
|
||||
self._component_timer = LoopingCall(self.update)
|
||||
self._component_timer.start(self._component_interval)
|
||||
|
||||
def _component_start(self):
|
||||
def on_start(result):
|
||||
|
@ -133,10 +129,13 @@ class Component:
|
|||
return fail(result)
|
||||
|
||||
if self._component_state == 'Stopped':
|
||||
self._component_state = 'Starting'
|
||||
d = deferLater(reactor, 0, self.start)
|
||||
d.addCallbacks(on_start, on_start_fail)
|
||||
self._component_starting_deferred = d
|
||||
if hasattr(self, 'start'):
|
||||
self._component_state = 'Starting'
|
||||
d = deferLater(reactor, 0, self.start)
|
||||
d.addCallbacks(on_start, on_start_fail)
|
||||
self._component_starting_deferred = d
|
||||
else:
|
||||
d = maybeDeferred(on_start, None)
|
||||
elif self._component_state == 'Starting':
|
||||
return self._component_starting_deferred
|
||||
elif self._component_state == 'Started':
|
||||
|
@ -166,11 +165,14 @@ class Component:
|
|||
return result
|
||||
|
||||
if self._component_state != 'Stopped' and self._component_state != 'Stopping':
|
||||
self._component_state = 'Stopping'
|
||||
d = maybeDeferred(self.stop)
|
||||
d.addCallback(on_stop)
|
||||
d.addErrback(on_stop_fail)
|
||||
self._component_stopping_deferred = d
|
||||
if hasattr(self, 'stop'):
|
||||
self._component_state = 'Stopping'
|
||||
d = maybeDeferred(self.stop)
|
||||
d.addCallback(on_stop)
|
||||
d.addErrback(on_stop_fail)
|
||||
self._component_stopping_deferred = d
|
||||
else:
|
||||
d = maybeDeferred(on_stop, None)
|
||||
|
||||
if self._component_state == 'Stopping':
|
||||
return self._component_stopping_deferred
|
||||
|
@ -180,12 +182,13 @@ class Component:
|
|||
def _component_pause(self):
|
||||
def on_pause(result):
|
||||
self._component_state = 'Paused'
|
||||
if self._component_timer and self._component_timer.running:
|
||||
self._component_timer.stop()
|
||||
|
||||
if self._component_state == 'Started':
|
||||
d = maybeDeferred(self.pause)
|
||||
d.addCallback(on_pause)
|
||||
if self._component_timer and self._component_timer.running:
|
||||
d = maybeDeferred(self._component_timer.stop)
|
||||
d.addCallback(on_pause)
|
||||
else:
|
||||
d = succeed(None)
|
||||
elif self._component_state == 'Paused':
|
||||
d = succeed(None)
|
||||
else:
|
||||
|
@ -202,10 +205,9 @@ class Component:
|
|||
def _component_resume(self):
|
||||
def on_resume(result):
|
||||
self._component_state = 'Started'
|
||||
self._component_start_timer()
|
||||
|
||||
if self._component_state == 'Paused':
|
||||
d = maybeDeferred(self.resume)
|
||||
d = maybeDeferred(self._component_start_timer)
|
||||
d.addCallback(on_resume)
|
||||
else:
|
||||
d = fail(
|
||||
|
@ -220,7 +222,9 @@ class Component:
|
|||
|
||||
def _component_shutdown(self):
|
||||
def on_stop(result):
|
||||
return maybeDeferred(self.shutdown)
|
||||
if hasattr(self, 'shutdown'):
|
||||
return maybeDeferred(self.shutdown)
|
||||
return succeed(None)
|
||||
|
||||
d = self._component_stop()
|
||||
d.addCallback(on_stop)
|
||||
|
@ -241,12 +245,6 @@ class Component:
|
|||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def pause(self):
|
||||
pass
|
||||
|
||||
def resume(self):
|
||||
pass
|
||||
|
||||
|
||||
class ComponentRegistry:
|
||||
"""The ComponentRegistry holds a list of currently registered :class:`Component` objects.
|
||||
|
|
|
@ -38,7 +38,6 @@ this can only be done for the 'config file version' and not for the 'format'
|
|||
version as this will be done internally.
|
||||
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
# the additional special exception to link portions of this program with the OpenSSL library.
|
||||
# See LICENSE for more details.
|
||||
#
|
||||
import asyncio
|
||||
|
||||
import tempfile
|
||||
import warnings
|
||||
from unittest.mock import Mock, patch
|
||||
|
@ -12,7 +12,7 @@ import pytest
|
|||
import pytest_twisted
|
||||
from twisted.internet import reactor
|
||||
from twisted.internet.defer import Deferred, maybeDeferred
|
||||
from twisted.internet.error import CannotListenError, ProcessTerminated
|
||||
from twisted.internet.error import CannotListenError
|
||||
from twisted.python.failure import Failure
|
||||
|
||||
import deluge.component as _component
|
||||
|
@ -42,18 +42,15 @@ def mock_callback():
|
|||
The returned Mock instance will have a `deferred` attribute which will complete when the callback has been called.
|
||||
"""
|
||||
|
||||
def reset(timeout=0.5, *args, **kwargs):
|
||||
def reset():
|
||||
if mock.called:
|
||||
original_reset_mock(*args, **kwargs)
|
||||
if mock.deferred:
|
||||
mock.deferred.cancel()
|
||||
deferred = Deferred(canceller=lambda x: deferred.callback(None))
|
||||
deferred.addTimeout(timeout, reactor)
|
||||
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()
|
||||
mock.__qualname__ = 'mock'
|
||||
original_reset_mock = mock.reset_mock
|
||||
mock.reset_mock = reset
|
||||
mock.reset_mock()
|
||||
|
@ -62,9 +59,8 @@ def mock_callback():
|
|||
|
||||
@pytest.fixture
|
||||
def config_dir(tmp_path):
|
||||
config_dir = tmp_path / 'config'
|
||||
deluge.configmanager.set_config_dir(config_dir)
|
||||
yield config_dir
|
||||
deluge.configmanager.set_config_dir(tmp_path)
|
||||
yield tmp_path
|
||||
|
||||
|
||||
@pytest_twisted.async_yield_fixture()
|
||||
|
@ -88,10 +84,9 @@ async def client(request, config_dir, monkeypatch, listen_port):
|
|||
|
||||
|
||||
@pytest_twisted.async_yield_fixture
|
||||
async def daemon(request, config_dir, tmp_path):
|
||||
async def daemon(request, config_dir):
|
||||
listen_port = DEFAULT_LISTEN_PORT
|
||||
logfile = tmp_path / 'daemon.log'
|
||||
|
||||
logfile = f'daemon_{request.node.name}.log'
|
||||
if hasattr(request.cls, 'daemon_custom_script'):
|
||||
custom_script = request.cls.daemon_custom_script
|
||||
else:
|
||||
|
@ -121,10 +116,7 @@ async def daemon(request, config_dir, tmp_path):
|
|||
raise exception_error
|
||||
daemon.listen_port = listen_port
|
||||
yield daemon
|
||||
try:
|
||||
await daemon.kill()
|
||||
except ProcessTerminated:
|
||||
pass
|
||||
await daemon.kill()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
|
@ -145,7 +137,7 @@ def common_fixture(config_dir, request, monkeypatch, listen_port):
|
|||
|
||||
|
||||
@pytest_twisted.async_yield_fixture(scope='function')
|
||||
async def component():
|
||||
async def component(request):
|
||||
"""Verify component registry is clean, and clean up after test."""
|
||||
if len(_component._ComponentRegistry.components) != 0:
|
||||
warnings.warn(
|
||||
|
@ -198,18 +190,3 @@ def mock_mkstemp(tmp_path):
|
|||
tmp_file = tempfile.mkstemp(dir=tmp_path)
|
||||
with patch('tempfile.mkstemp', return_value=tmp_file):
|
||||
yield tmp_file
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(session, config, items) -> None:
|
||||
"""
|
||||
Automatically runs async tests with pytest_twisted.ensureDeferred
|
||||
"""
|
||||
function_items = (item for item in items if isinstance(item, pytest.Function))
|
||||
for function_item in function_items:
|
||||
function = function_item.obj
|
||||
if hasattr(function, '__func__'):
|
||||
# methods need to be unwrapped.
|
||||
function = function.__func__
|
||||
if asyncio.iscoroutinefunction(function):
|
||||
# This is how pytest_twisted marks ensureDeferred tests
|
||||
setattr(function, '_pytest_twisted_mark', 'async_test')
|
||||
|
|
|
@ -14,16 +14,10 @@ This should typically only be used by the Core. Plugins should utilize the
|
|||
`:mod:EventManager` for similar functionality.
|
||||
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from functools import partial
|
||||
from typing import Any, Callable
|
||||
from types import SimpleNamespace
|
||||
|
||||
from twisted.internet import reactor, task, threads
|
||||
from twisted.internet import reactor
|
||||
|
||||
import deluge.component as component
|
||||
from deluge._libtorrent import lt
|
||||
|
@ -37,7 +31,7 @@ class AlertManager(component.Component):
|
|||
|
||||
def __init__(self):
|
||||
log.debug('AlertManager init...')
|
||||
component.Component.__init__(self, 'AlertManager')
|
||||
component.Component.__init__(self, 'AlertManager', interval=0.3)
|
||||
self.session = component.get('Core').session
|
||||
|
||||
# Increase the alert queue size so that alerts don't get lost.
|
||||
|
@ -58,88 +52,48 @@ class AlertManager(component.Component):
|
|||
self.session.apply_settings({'alert_mask': alert_mask})
|
||||
|
||||
# handlers is a dictionary of lists {"alert_type": [handler1,h2,..]}
|
||||
self.handlers = defaultdict(list)
|
||||
self.handlers_timeout_secs = 2
|
||||
self.handlers = {}
|
||||
self.delayed_calls = []
|
||||
self._event = threading.Event()
|
||||
|
||||
def update(self):
|
||||
pass
|
||||
|
||||
def start(self):
|
||||
thread = threading.Thread(
|
||||
target=self.wait_for_alert_in_thread, name='alert-poller', daemon=True
|
||||
)
|
||||
thread.start()
|
||||
self._event.set()
|
||||
|
||||
def stop(self):
|
||||
self.cancel_delayed_calls()
|
||||
|
||||
def pause(self):
|
||||
self._event.clear()
|
||||
|
||||
def resume(self):
|
||||
self._event.set()
|
||||
|
||||
def wait_for_alert_in_thread(self):
|
||||
while self._component_state not in ('Stopping', 'Stopped'):
|
||||
if self.check_delayed_calls():
|
||||
time.sleep(0.05)
|
||||
continue
|
||||
|
||||
if self.session.wait_for_alert(1000) is None:
|
||||
continue
|
||||
if self._event.wait():
|
||||
threads.blockingCallFromThread(reactor, self.maybe_handle_alerts)
|
||||
|
||||
def on_delayed_call_timeout(self, result, timeout, **kwargs):
|
||||
log.warning('Alert handler was timed-out before being called %s', kwargs)
|
||||
|
||||
def cancel_delayed_calls(self):
|
||||
"""Cancel all delayed handlers."""
|
||||
for delayed_call in self.delayed_calls:
|
||||
delayed_call.cancel()
|
||||
self.delayed_calls = []
|
||||
|
||||
def check_delayed_calls(self) -> bool:
|
||||
"""Returns True if any handler calls are delayed."""
|
||||
self.delayed_calls = [dc for dc in self.delayed_calls if not dc.called]
|
||||
return len(self.delayed_calls) > 0
|
||||
|
||||
def maybe_handle_alerts(self) -> None:
|
||||
if self._component_state != 'Started':
|
||||
return
|
||||
|
||||
self.delayed_calls = [dc for dc in self.delayed_calls if dc.active()]
|
||||
self.handle_alerts()
|
||||
|
||||
def register_handler(self, alert_type: str, handler: Callable[[Any], None]) -> None:
|
||||
def stop(self):
|
||||
for delayed_call in self.delayed_calls:
|
||||
if delayed_call.active():
|
||||
delayed_call.cancel()
|
||||
self.delayed_calls = []
|
||||
|
||||
def register_handler(self, alert_type, handler):
|
||||
"""
|
||||
Registers a function that will be called when 'alert_type' is pop'd
|
||||
in handle_alerts. The handler function should look like: handler(alert)
|
||||
Where 'alert' is the actual alert object from libtorrent.
|
||||
|
||||
Args:
|
||||
alert_type: String representation of the libtorrent alert name.
|
||||
Can be supplied with or without `_alert` suffix.
|
||||
handler: Callback function when the alert is raised.
|
||||
:param alert_type: str, this is string representation of the alert name
|
||||
:param handler: func(alert), the function to be called when the alert is raised
|
||||
"""
|
||||
if alert_type and alert_type.endswith('_alert'):
|
||||
alert_type = alert_type[: -len('_alert')]
|
||||
if alert_type not in self.handlers:
|
||||
# There is no entry for this alert type yet, so lets make it with an
|
||||
# empty list.
|
||||
self.handlers[alert_type] = []
|
||||
|
||||
# Append the handler to the list in the handlers dictionary
|
||||
self.handlers[alert_type].append(handler)
|
||||
log.debug('Registered handler for alert %s', alert_type)
|
||||
|
||||
def deregister_handler(self, handler: Callable[[Any], None]):
|
||||
def deregister_handler(self, handler):
|
||||
"""
|
||||
De-registers the `handler` function from all alert types.
|
||||
De-registers the `:param:handler` function from all alert types.
|
||||
|
||||
Args:
|
||||
handler: The handler function to deregister.
|
||||
:param handler: func, the handler function to deregister
|
||||
"""
|
||||
for alert_type_handlers in self.handlers.values():
|
||||
with contextlib.suppress(ValueError):
|
||||
alert_type_handlers.remove(handler)
|
||||
# Iterate through all handlers and remove 'handler' where found
|
||||
for (dummy_key, value) in self.handlers.items():
|
||||
if handler in value:
|
||||
# Handler is in this alert type list
|
||||
value.remove(handler)
|
||||
|
||||
def handle_alerts(self):
|
||||
"""
|
||||
|
@ -158,32 +112,26 @@ class AlertManager(component.Component):
|
|||
num_alerts,
|
||||
)
|
||||
|
||||
# Loop through all alerts in the queue
|
||||
for alert in alerts:
|
||||
alert_type = alert.what()
|
||||
|
||||
alert_type = type(alert).__name__
|
||||
# Display the alert message
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
log.debug('%s: %s', alert_type, decode_bytes(alert.message()))
|
||||
|
||||
if alert_type not in self.handlers:
|
||||
continue
|
||||
|
||||
# Call any handlers for this alert type
|
||||
for handler in self.handlers[alert_type]:
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
log.debug('Handling alert: %s', alert_type)
|
||||
d = task.deferLater(reactor, 0, handler, alert)
|
||||
on_handler_timeout = partial(
|
||||
self.on_delayed_call_timeout,
|
||||
handler=handler.__qualname__,
|
||||
alert_type=alert_type,
|
||||
)
|
||||
d.addTimeout(
|
||||
self.handlers_timeout_secs,
|
||||
reactor,
|
||||
onTimeoutCancel=on_handler_timeout,
|
||||
)
|
||||
self.delayed_calls.append(d)
|
||||
if alert_type in self.handlers:
|
||||
for handler in self.handlers[alert_type]:
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
log.debug('Handling alert: %s', alert_type)
|
||||
# Copy alert attributes
|
||||
alert_copy = SimpleNamespace(
|
||||
**{
|
||||
attr: getattr(alert, attr)
|
||||
for attr in dir(alert)
|
||||
if not attr.startswith('__')
|
||||
}
|
||||
)
|
||||
self.delayed_calls.append(reactor.callLater(0, handler, alert_copy))
|
||||
|
||||
def set_alert_queue_size(self, queue_size):
|
||||
"""Sets the maximum size of the libtorrent alert queue"""
|
||||
|
|
|
@ -12,16 +12,17 @@ import logging
|
|||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import threading
|
||||
from base64 import b64decode, b64encode
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
from urllib.request import URLError, urlopen
|
||||
|
||||
from twisted.internet import defer, reactor, task, threads
|
||||
from twisted.internet import defer, reactor, task
|
||||
from twisted.web.client import Agent, readBody
|
||||
|
||||
import deluge.common
|
||||
import deluge.component as component
|
||||
from deluge import metafile, path_chooser_common
|
||||
from deluge import path_chooser_common
|
||||
from deluge._libtorrent import LT_VERSION, lt
|
||||
from deluge.configmanager import ConfigManager, get_config_dir
|
||||
from deluge.core.alertmanager import AlertManager
|
||||
|
@ -198,7 +199,7 @@ class Core(component.Component):
|
|||
self.session_status_timer_interval = 0.5
|
||||
self.session_status_timer = task.LoopingCall(self.session.post_session_stats)
|
||||
self.alertmanager.register_handler(
|
||||
'session_stats', self._on_alert_session_stats
|
||||
'session_stats_alert', self._on_alert_session_stats
|
||||
)
|
||||
self.session_rates_timer_interval = 2
|
||||
self.session_rates_timer = task.LoopingCall(self._update_session_rates)
|
||||
|
@ -991,33 +992,31 @@ class Core(component.Component):
|
|||
path,
|
||||
tracker,
|
||||
piece_length,
|
||||
comment=None,
|
||||
target=None,
|
||||
webseeds=None,
|
||||
private=False,
|
||||
created_by=None,
|
||||
trackers=None,
|
||||
add_to_session=False,
|
||||
torrent_format=metafile.TorrentFormat.V1,
|
||||
comment,
|
||||
target,
|
||||
webseeds,
|
||||
private,
|
||||
created_by,
|
||||
trackers,
|
||||
add_to_session,
|
||||
):
|
||||
if isinstance(torrent_format, str):
|
||||
torrent_format = metafile.TorrentFormat(torrent_format)
|
||||
|
||||
log.debug('creating torrent..')
|
||||
return threads.deferToThread(
|
||||
self._create_torrent_thread,
|
||||
path,
|
||||
tracker,
|
||||
piece_length,
|
||||
comment=comment,
|
||||
target=target,
|
||||
webseeds=webseeds,
|
||||
private=private,
|
||||
created_by=created_by,
|
||||
trackers=trackers,
|
||||
add_to_session=add_to_session,
|
||||
torrent_format=torrent_format,
|
||||
)
|
||||
threading.Thread(
|
||||
target=self._create_torrent_thread,
|
||||
args=(
|
||||
path,
|
||||
tracker,
|
||||
piece_length,
|
||||
comment,
|
||||
target,
|
||||
webseeds,
|
||||
private,
|
||||
created_by,
|
||||
trackers,
|
||||
add_to_session,
|
||||
),
|
||||
).start()
|
||||
|
||||
def _create_torrent_thread(
|
||||
self,
|
||||
|
@ -1031,41 +1030,27 @@ class Core(component.Component):
|
|||
created_by,
|
||||
trackers,
|
||||
add_to_session,
|
||||
torrent_format,
|
||||
):
|
||||
from deluge import metafile
|
||||
|
||||
filecontent = metafile.make_meta_file_content(
|
||||
metafile.make_meta_file(
|
||||
path,
|
||||
tracker,
|
||||
piece_length,
|
||||
comment=comment,
|
||||
target=target,
|
||||
webseeds=webseeds,
|
||||
private=private,
|
||||
created_by=created_by,
|
||||
trackers=trackers,
|
||||
torrent_format=torrent_format,
|
||||
)
|
||||
|
||||
write_file = False
|
||||
if target or not add_to_session:
|
||||
write_file = True
|
||||
|
||||
if not target:
|
||||
target = metafile.default_meta_file_path(path)
|
||||
filename = os.path.split(target)[-1]
|
||||
|
||||
if write_file:
|
||||
with open(target, 'wb') as _file:
|
||||
_file.write(filecontent)
|
||||
|
||||
filedump = b64encode(filecontent)
|
||||
log.debug('torrent created!')
|
||||
if add_to_session:
|
||||
options = {}
|
||||
options['download_location'] = os.path.split(path)[0]
|
||||
self.add_torrent_file(filename, filedump, options)
|
||||
return filename, filedump
|
||||
with open(target, 'rb') as _file:
|
||||
filedump = b64encode(_file.read())
|
||||
self.add_torrent_file(os.path.split(target)[1], filedump, options)
|
||||
|
||||
@export
|
||||
def upload_plugin(self, filename: str, filedump: Union[str, bytes]) -> None:
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
#
|
||||
|
||||
"""The Deluge daemon"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
|
||||
|
||||
"""PluginManager for Core"""
|
||||
|
||||
import logging
|
||||
|
||||
from twisted.internet import defer
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
#
|
||||
|
||||
"""RPCServer Module"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
@ -47,11 +46,13 @@ TCallable = TypeVar('TCallable', bound=Callable)
|
|||
|
||||
|
||||
@overload
|
||||
def export(func: TCallable) -> TCallable: ...
|
||||
def export(func: TCallable) -> TCallable:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def export(auth_level: int) -> Callable[[TCallable], TCallable]: ...
|
||||
def export(auth_level: int) -> Callable[[TCallable], TCallable]:
|
||||
...
|
||||
|
||||
|
||||
def export(auth_level=AUTH_LEVEL_DEFAULT):
|
||||
|
@ -273,9 +274,9 @@ class DelugeRPCProtocol(DelugeTransferProtocol):
|
|||
raise IncompatibleClient(deluge.common.get_version())
|
||||
ret = component.get('AuthManager').authorize(*args, **kwargs)
|
||||
if ret:
|
||||
self.factory.authorized_sessions[self.transport.sessionno] = (
|
||||
self.AuthLevel(ret, args[0])
|
||||
)
|
||||
self.factory.authorized_sessions[
|
||||
self.transport.sessionno
|
||||
] = self.AuthLevel(ret, args[0])
|
||||
self.factory.session_protocols[self.transport.sessionno] = self
|
||||
except Exception as ex:
|
||||
send_error()
|
||||
|
@ -544,8 +545,8 @@ class RPCServer(component.Component):
|
|||
:type event: :class:`deluge.event.DelugeEvent`
|
||||
"""
|
||||
log.debug('intevents: %s', self.factory.interested_events)
|
||||
# Use copy of `interested_events` since it can mutate while iterating.
|
||||
for session_id, interest in self.factory.interested_events.copy().items():
|
||||
# Find sessions interested in this event
|
||||
for session_id, interest in self.factory.interested_events.items():
|
||||
if event.name in interest:
|
||||
log.debug('Emit Event: %s %s', event.name, event.args)
|
||||
# This session is interested so send a RPC_EVENT
|
||||
|
|
|
@ -1138,8 +1138,9 @@ class Torrent:
|
|||
'download_location': lambda: self.options['download_location'],
|
||||
'seeds_peers_ratio': lambda: -1.0
|
||||
if self.status.num_incomplete == 0
|
||||
# Use -1.0 to signify infinity
|
||||
else (self.status.num_complete / self.status.num_incomplete),
|
||||
else ( # Use -1.0 to signify infinity
|
||||
self.status.num_complete / self.status.num_incomplete
|
||||
),
|
||||
'seed_rank': lambda: self.status.seed_rank,
|
||||
'state': lambda: self.state,
|
||||
'stop_at_ratio': lambda: self.options['stop_at_ratio'],
|
||||
|
@ -1543,18 +1544,20 @@ class Torrent:
|
|||
self.status.pieces, self.handle.piece_availability()
|
||||
):
|
||||
if piece:
|
||||
# Completed.
|
||||
pieces.append(3)
|
||||
pieces.append(3) # Completed.
|
||||
elif avail_piece:
|
||||
# Available, just not downloaded nor being downloaded.
|
||||
pieces.append(1)
|
||||
pieces.append(
|
||||
1
|
||||
) # Available, just not downloaded nor being downloaded.
|
||||
else:
|
||||
# Missing, no known peer with piece, or not asked for yet.
|
||||
pieces.append(0)
|
||||
pieces.append(
|
||||
0
|
||||
) # Missing, no known peer with piece, or not asked for yet.
|
||||
|
||||
for peer_info in self.handle.get_peer_info():
|
||||
if peer_info.downloading_piece_index >= 0:
|
||||
# Being downloaded from peer.
|
||||
pieces[peer_info.downloading_piece_index] = 2
|
||||
pieces[
|
||||
peer_info.downloading_piece_index
|
||||
] = 2 # Being downloaded from peer.
|
||||
|
||||
return pieces
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
#
|
||||
|
||||
"""TorrentManager handles Torrent objects"""
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import operator
|
||||
|
@ -51,10 +50,10 @@ from deluge.event import (
|
|||
log = logging.getLogger(__name__)
|
||||
|
||||
LT_DEFAULT_ADD_TORRENT_FLAGS = (
|
||||
lt.torrent_flags.paused
|
||||
| lt.torrent_flags.auto_managed
|
||||
| lt.torrent_flags.update_subscribe
|
||||
| lt.torrent_flags.apply_ip_filter
|
||||
lt.add_torrent_params_flags_t.flag_paused
|
||||
| lt.add_torrent_params_flags_t.flag_auto_managed
|
||||
| lt.add_torrent_params_flags_t.flag_update_subscribe
|
||||
| lt.add_torrent_params_flags_t.flag_apply_ip_filter
|
||||
)
|
||||
|
||||
|
||||
|
@ -203,32 +202,34 @@ class TorrentManager(component.Component):
|
|||
|
||||
# Register alert functions
|
||||
alert_handles = [
|
||||
'external_ip',
|
||||
'performance',
|
||||
'add_torrent',
|
||||
'metadata_received',
|
||||
'torrent_finished',
|
||||
'torrent_paused',
|
||||
'torrent_checked',
|
||||
'torrent_resumed',
|
||||
'tracker_reply',
|
||||
'tracker_announce',
|
||||
'tracker_warning',
|
||||
'tracker_error',
|
||||
'file_renamed',
|
||||
'file_error',
|
||||
'file_completed',
|
||||
'storage_moved',
|
||||
'storage_moved_failed',
|
||||
'state_update',
|
||||
'state_changed',
|
||||
'save_resume_data',
|
||||
'save_resume_data_failed',
|
||||
'fastresume_rejected',
|
||||
'external_ip_alert',
|
||||
'performance_alert',
|
||||
'add_torrent_alert',
|
||||
'metadata_received_alert',
|
||||
'torrent_finished_alert',
|
||||
'torrent_paused_alert',
|
||||
'torrent_checked_alert',
|
||||
'torrent_resumed_alert',
|
||||
'tracker_reply_alert',
|
||||
'tracker_announce_alert',
|
||||
'tracker_warning_alert',
|
||||
'tracker_error_alert',
|
||||
'file_renamed_alert',
|
||||
'file_error_alert',
|
||||
'file_completed_alert',
|
||||
'storage_moved_alert',
|
||||
'storage_moved_failed_alert',
|
||||
'state_update_alert',
|
||||
'state_changed_alert',
|
||||
'save_resume_data_alert',
|
||||
'save_resume_data_failed_alert',
|
||||
'fastresume_rejected_alert',
|
||||
]
|
||||
|
||||
for alert_handle in alert_handles:
|
||||
on_alert_func = getattr(self, ''.join(['on_alert_', alert_handle]))
|
||||
on_alert_func = getattr(
|
||||
self, ''.join(['on_alert_', alert_handle.replace('_alert', '')])
|
||||
)
|
||||
self.alerts.register_handler(alert_handle, on_alert_func)
|
||||
|
||||
# Define timers
|
||||
|
@ -291,8 +292,8 @@ class TorrentManager(component.Component):
|
|||
if torrent.options['remove_at_ratio']:
|
||||
self.remove(torrent_id)
|
||||
break
|
||||
|
||||
torrent.pause()
|
||||
if not torrent.status.paused:
|
||||
torrent.pause()
|
||||
|
||||
def __getitem__(self, torrent_id):
|
||||
"""Return the Torrent with torrent_id.
|
||||
|
@ -368,11 +369,11 @@ class TorrentManager(component.Component):
|
|||
add_torrent_params.flags = (
|
||||
(
|
||||
LT_DEFAULT_ADD_TORRENT_FLAGS
|
||||
| lt.torrent_flags.duplicate_is_error
|
||||
| lt.torrent_flags.upload_mode
|
||||
| lt.add_torrent_params_flags_t.flag_duplicate_is_error
|
||||
| lt.add_torrent_params_flags_t.flag_upload_mode
|
||||
)
|
||||
^ lt.torrent_flags.auto_managed
|
||||
^ lt.torrent_flags.paused
|
||||
^ lt.add_torrent_params_flags_t.flag_auto_managed
|
||||
^ lt.add_torrent_params_flags_t.flag_paused
|
||||
)
|
||||
|
||||
torrent_handle = self.session.add_torrent(add_torrent_params)
|
||||
|
@ -480,12 +481,16 @@ class TorrentManager(component.Component):
|
|||
|
||||
# Set flags: enable duplicate_is_error & override_resume_data, disable auto_managed.
|
||||
add_torrent_params['flags'] = (
|
||||
LT_DEFAULT_ADD_TORRENT_FLAGS | lt.torrent_flags.duplicate_is_error
|
||||
) ^ lt.torrent_flags.auto_managed
|
||||
LT_DEFAULT_ADD_TORRENT_FLAGS
|
||||
| lt.add_torrent_params_flags_t.flag_duplicate_is_error
|
||||
| lt.add_torrent_params_flags_t.flag_override_resume_data
|
||||
) ^ lt.add_torrent_params_flags_t.flag_auto_managed
|
||||
if options['seed_mode']:
|
||||
add_torrent_params['flags'] |= lt.torrent_flags.seed_mode
|
||||
add_torrent_params['flags'] |= lt.add_torrent_params_flags_t.flag_seed_mode
|
||||
if options['super_seeding']:
|
||||
add_torrent_params['flags'] |= lt.torrent_flags.super_seeding
|
||||
add_torrent_params[
|
||||
'flags'
|
||||
] |= lt.add_torrent_params_flags_t.flag_super_seeding
|
||||
|
||||
return torrent_id, add_torrent_params
|
||||
|
||||
|
|
|
@ -166,8 +166,7 @@ def deprecated(func):
|
|||
|
||||
class CoroutineDeferred(defer.Deferred):
|
||||
"""Wraps a coroutine in a Deferred.
|
||||
It will dynamically pass through the underlying coroutine without wrapping where apporpriate.
|
||||
"""
|
||||
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
|
||||
|
@ -196,33 +195,17 @@ class CoroutineDeferred(defer.Deferred):
|
|||
d = defer.ensureDeferred(self.coro)
|
||||
d.chainDeferred(self)
|
||||
|
||||
def _callback_activate(self):
|
||||
"""Verify awaited status before calling activate."""
|
||||
def addCallbacks(self, *args, **kwargs): # noqa: N802
|
||||
assert not self.awaited, 'Cannot add callbacks to an already awaited coroutine.'
|
||||
self.activate()
|
||||
|
||||
def addCallback(self, *args, **kwargs): # noqa: N802
|
||||
self._callback_activate()
|
||||
return super().addCallback(*args, **kwargs)
|
||||
|
||||
def addCallbacks(self, *args, **kwargs): # noqa: N802
|
||||
self._callback_activate()
|
||||
return super().addCallbacks(*args, **kwargs)
|
||||
|
||||
def addErrback(self, *args, **kwargs): # noqa: N802
|
||||
self._callback_activate()
|
||||
return super().addErrback(*args, **kwargs)
|
||||
|
||||
def addBoth(self, *args, **kwargs): # noqa: N802
|
||||
self._callback_activate()
|
||||
return super().addBoth(*args, **kwargs)
|
||||
|
||||
|
||||
_RetT = TypeVar('_RetT')
|
||||
|
||||
|
||||
def maybe_coroutine(
|
||||
f: Callable[..., Coroutine[Any, Any, _RetT]],
|
||||
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."""
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@ This module describes the types of events that can be generated by the daemon
|
|||
and subsequently emitted to the clients.
|
||||
|
||||
"""
|
||||
|
||||
known_events = {}
|
||||
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
# See LICENSE for more details.
|
||||
#
|
||||
|
||||
import email.message
|
||||
import cgi
|
||||
import logging
|
||||
import os.path
|
||||
import zlib
|
||||
|
@ -133,10 +133,9 @@ class HTTPDownloaderAgent:
|
|||
content_disp = headers.getRawHeaders(b'content-disposition')[0].decode(
|
||||
'utf-8'
|
||||
)
|
||||
message = email.message.EmailMessage()
|
||||
message['content-disposition'] = content_disp
|
||||
new_file_name = message.get_filename()
|
||||
if new_file_name:
|
||||
content_disp_params = cgi.parse_header(content_disp)[1]
|
||||
if 'filename' in content_disp_params:
|
||||
new_file_name = content_disp_params['filename']
|
||||
new_file_name = sanitise_filename(new_file_name)
|
||||
new_file_name = os.path.join(
|
||||
os.path.split(self.filename)[0], new_file_name
|
||||
|
@ -153,10 +152,7 @@ class HTTPDownloaderAgent:
|
|||
self.filename = new_file_name
|
||||
|
||||
cont_type_header = headers.getRawHeaders(b'content-type')[0].decode()
|
||||
message = email.message.EmailMessage()
|
||||
message['content-type'] = cont_type_header
|
||||
cont_type = message.get_content_type()
|
||||
params = message['content-type'].params
|
||||
cont_type, params = cgi.parse_header(cont_type_header)
|
||||
# Only re-ecode text content types.
|
||||
encoding = None
|
||||
if cont_type.startswith('text/'):
|
||||
|
|
8418
deluge/i18n/af.po
8418
deluge/i18n/af.po
File diff suppressed because it is too large
Load diff
8449
deluge/i18n/ar.po
8449
deluge/i18n/ar.po
File diff suppressed because it is too large
Load diff
8407
deluge/i18n/ast.po
8407
deluge/i18n/ast.po
File diff suppressed because it is too large
Load diff
9782
deluge/i18n/be.po
9782
deluge/i18n/be.po
File diff suppressed because it is too large
Load diff
8448
deluge/i18n/bg.po
8448
deluge/i18n/bg.po
File diff suppressed because it is too large
Load diff
8264
deluge/i18n/bn.po
8264
deluge/i18n/bn.po
File diff suppressed because it is too large
Load diff
8290
deluge/i18n/bs.po
8290
deluge/i18n/bs.po
File diff suppressed because it is too large
Load diff
8651
deluge/i18n/ca.po
8651
deluge/i18n/ca.po
File diff suppressed because it is too large
Load diff
8480
deluge/i18n/cs.po
8480
deluge/i18n/cs.po
File diff suppressed because it is too large
Load diff
8262
deluge/i18n/cy.po
8262
deluge/i18n/cy.po
File diff suppressed because it is too large
Load diff
8537
deluge/i18n/da.po
8537
deluge/i18n/da.po
File diff suppressed because it is too large
Load diff
9348
deluge/i18n/de.po
9348
deluge/i18n/de.po
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
8498
deluge/i18n/el.po
8498
deluge/i18n/el.po
File diff suppressed because it is too large
Load diff
8476
deluge/i18n/en_AU.po
8476
deluge/i18n/en_AU.po
File diff suppressed because it is too large
Load diff
8476
deluge/i18n/en_CA.po
8476
deluge/i18n/en_CA.po
File diff suppressed because it is too large
Load diff
9921
deluge/i18n/en_GB.po
9921
deluge/i18n/en_GB.po
File diff suppressed because it is too large
Load diff
8280
deluge/i18n/eo.po
8280
deluge/i18n/eo.po
File diff suppressed because it is too large
Load diff
8512
deluge/i18n/es.po
8512
deluge/i18n/es.po
File diff suppressed because it is too large
Load diff
8447
deluge/i18n/et.po
8447
deluge/i18n/et.po
File diff suppressed because it is too large
Load diff
8474
deluge/i18n/eu.po
8474
deluge/i18n/eu.po
File diff suppressed because it is too large
Load diff
8381
deluge/i18n/fa.po
8381
deluge/i18n/fa.po
File diff suppressed because it is too large
Load diff
8628
deluge/i18n/fi.po
8628
deluge/i18n/fi.po
File diff suppressed because it is too large
Load diff
8305
deluge/i18n/fo.po
8305
deluge/i18n/fo.po
File diff suppressed because it is too large
Load diff
8564
deluge/i18n/fr.po
8564
deluge/i18n/fr.po
File diff suppressed because it is too large
Load diff
8336
deluge/i18n/fy.po
8336
deluge/i18n/fy.po
File diff suppressed because it is too large
Load diff
8252
deluge/i18n/ga.po
8252
deluge/i18n/ga.po
File diff suppressed because it is too large
Load diff
8464
deluge/i18n/gl.po
8464
deluge/i18n/gl.po
File diff suppressed because it is too large
Load diff
8440
deluge/i18n/he.po
8440
deluge/i18n/he.po
File diff suppressed because it is too large
Load diff
8447
deluge/i18n/hi.po
8447
deluge/i18n/hi.po
File diff suppressed because it is too large
Load diff
8510
deluge/i18n/hr.po
8510
deluge/i18n/hr.po
File diff suppressed because it is too large
Load diff
8450
deluge/i18n/hu.po
8450
deluge/i18n/hu.po
File diff suppressed because it is too large
Load diff
8321
deluge/i18n/id.po
8321
deluge/i18n/id.po
File diff suppressed because it is too large
Load diff
8405
deluge/i18n/is.po
8405
deluge/i18n/is.po
File diff suppressed because it is too large
Load diff
8480
deluge/i18n/it.po
8480
deluge/i18n/it.po
File diff suppressed because it is too large
Load diff
8252
deluge/i18n/iu.po
8252
deluge/i18n/iu.po
File diff suppressed because it is too large
Load diff
8452
deluge/i18n/ja.po
8452
deluge/i18n/ja.po
File diff suppressed because it is too large
Load diff
8462
deluge/i18n/ka.po
8462
deluge/i18n/ka.po
File diff suppressed because it is too large
Load diff
8468
deluge/i18n/kk.po
8468
deluge/i18n/kk.po
File diff suppressed because it is too large
Load diff
8268
deluge/i18n/km.po
8268
deluge/i18n/km.po
File diff suppressed because it is too large
Load diff
8295
deluge/i18n/kn.po
8295
deluge/i18n/kn.po
File diff suppressed because it is too large
Load diff
8448
deluge/i18n/ko.po
8448
deluge/i18n/ko.po
File diff suppressed because it is too large
Load diff
8252
deluge/i18n/ku.po
8252
deluge/i18n/ku.po
File diff suppressed because it is too large
Load diff
8252
deluge/i18n/ky.po
8252
deluge/i18n/ky.po
File diff suppressed because it is too large
Load diff
8252
deluge/i18n/la.po
8252
deluge/i18n/la.po
File diff suppressed because it is too large
Load diff
8272
deluge/i18n/lb.po
8272
deluge/i18n/lb.po
File diff suppressed because it is too large
Load diff
8488
deluge/i18n/lt.po
8488
deluge/i18n/lt.po
File diff suppressed because it is too large
Load diff
8496
deluge/i18n/lv.po
8496
deluge/i18n/lv.po
File diff suppressed because it is too large
Load diff
8458
deluge/i18n/mk.po
8458
deluge/i18n/mk.po
File diff suppressed because it is too large
Load diff
8252
deluge/i18n/ml.po
8252
deluge/i18n/ml.po
File diff suppressed because it is too large
Load diff
6214
deluge/i18n/mo.po
6214
deluge/i18n/mo.po
File diff suppressed because it is too large
Load diff
8478
deluge/i18n/ms.po
8478
deluge/i18n/ms.po
File diff suppressed because it is too large
Load diff
8268
deluge/i18n/nap.po
8268
deluge/i18n/nap.po
File diff suppressed because it is too large
Load diff
8447
deluge/i18n/nb.po
8447
deluge/i18n/nb.po
File diff suppressed because it is too large
Load diff
7022
deluge/i18n/nds.po
7022
deluge/i18n/nds.po
File diff suppressed because it is too large
Load diff
8480
deluge/i18n/nl.po
8480
deluge/i18n/nl.po
File diff suppressed because it is too large
Load diff
8422
deluge/i18n/nn.po
8422
deluge/i18n/nn.po
File diff suppressed because it is too large
Load diff
8431
deluge/i18n/oc.po
8431
deluge/i18n/oc.po
File diff suppressed because it is too large
Load diff
8587
deluge/i18n/pl.po
8587
deluge/i18n/pl.po
File diff suppressed because it is too large
Load diff
8252
deluge/i18n/pms.po
8252
deluge/i18n/pms.po
File diff suppressed because it is too large
Load diff
8476
deluge/i18n/pt.po
8476
deluge/i18n/pt.po
File diff suppressed because it is too large
Load diff
8482
deluge/i18n/pt_BR.po
8482
deluge/i18n/pt_BR.po
File diff suppressed because it is too large
Load diff
8476
deluge/i18n/ro.po
8476
deluge/i18n/ro.po
File diff suppressed because it is too large
Load diff
10059
deluge/i18n/ru.po
10059
deluge/i18n/ru.po
File diff suppressed because it is too large
Load diff
8460
deluge/i18n/si.po
8460
deluge/i18n/si.po
File diff suppressed because it is too large
Load diff
8475
deluge/i18n/sk.po
8475
deluge/i18n/sk.po
File diff suppressed because it is too large
Load diff
8474
deluge/i18n/sl.po
8474
deluge/i18n/sl.po
File diff suppressed because it is too large
Load diff
8484
deluge/i18n/sr.po
8484
deluge/i18n/sr.po
File diff suppressed because it is too large
Load diff
8610
deluge/i18n/sv.po
8610
deluge/i18n/sv.po
File diff suppressed because it is too large
Load diff
8252
deluge/i18n/ta.po
8252
deluge/i18n/ta.po
File diff suppressed because it is too large
Load diff
8252
deluge/i18n/te.po
8252
deluge/i18n/te.po
File diff suppressed because it is too large
Load diff
8252
deluge/i18n/th.po
8252
deluge/i18n/th.po
File diff suppressed because it is too large
Load diff
8252
deluge/i18n/tl.po
8252
deluge/i18n/tl.po
File diff suppressed because it is too large
Load diff
8252
deluge/i18n/tlh.po
8252
deluge/i18n/tlh.po
File diff suppressed because it is too large
Load diff
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue