mirror of
https://git.deluge-torrent.org/deluge
synced 2025-04-19 19:14:55 +00:00
Compare commits
138 commits
deluge-2.1
...
develop
Author | SHA1 | Date | |
---|---|---|---|
|
0b5addf58e | ||
|
98d01fbe35 | ||
|
ee33c0c5bb | ||
|
0e197ee07e | ||
|
e83f6b84fb | ||
|
0878616b2e | ||
|
7c5b7b44a3 | ||
|
7071da85c3 | ||
|
cb182daaaf | ||
|
8df36c454b | ||
|
40d4f7efef | ||
|
d064ad06c5 | ||
|
0d72195281 | ||
|
2247668571 | ||
|
e7d08d7645 | ||
|
90c5e75373 | ||
|
c88f750108 | ||
|
491458c4ad | ||
|
5d96cfc72f | ||
|
3bceb4bfc1 | ||
|
9d802b2a91 | ||
|
8867da94f8 | ||
|
e1fa8d18ec | ||
|
d5af32802f | ||
|
d1d72b1be8 | ||
|
776efe4faa | ||
|
f101f0afdd | ||
|
d98d15422a | ||
|
d9e3facbe8 | ||
|
6ba23a8013 | ||
|
af70ff1fdc | ||
|
18fa028d2d | ||
|
322faa7a54 | ||
|
785ad00d2b | ||
|
1e5f248fb8 | ||
|
80985c02da | ||
|
7660e2e5ca | ||
|
7f3f7f69ee | ||
|
5dd7aa5321 | ||
|
ee97864086 | ||
|
848d668af9 | ||
|
d9ef65d745 | ||
|
7f70d6c6ff | ||
|
b7450b5082 | ||
|
7046824115 | ||
|
fa8d19335e | ||
|
0c1a02dcb5 | ||
|
810751d72a | ||
|
7199805c89 | ||
|
29cf72577f | ||
|
42accef295 | ||
|
54d6f50231 | ||
|
b5f8c5af2d | ||
|
c7dc60571e | ||
|
1989d0de73 | ||
|
1751d62df9 | ||
|
4088e13905 | ||
|
b63699c6de | ||
|
8dba0efa85 | ||
|
b2005ecd78 | ||
|
39b99182ba | ||
|
66eaea0059 | ||
|
5aa4d07816 | ||
|
f3d7b1ffe8 | ||
|
d8f9fe4acf | ||
|
f43b605b80 | ||
|
1dbb18b80a | ||
|
21470799d0 | ||
|
e24081a17e | ||
|
6c9b058d81 | ||
|
18dca70084 | ||
|
ed1366d5ce | ||
|
7082d9cec4 | ||
|
015b0660be | ||
|
a459e78268 | ||
|
8001110625 | ||
|
d8b586e6ba | ||
|
905a7dc3bc | ||
|
89b79e4b7f | ||
|
e70e43e631 | ||
|
b24a5d2465 | ||
|
701f68d70b | ||
|
de570ae536 | ||
|
40a66278a3 | ||
|
366cded7be | ||
|
dbedf7f639 | ||
|
81116a63ca | ||
|
a83ac65ab6 | ||
|
d2a56ce15e | ||
|
71b634e968 | ||
|
39bd97f03e | ||
|
196086c1fb | ||
|
527cfa586c | ||
|
25a2b113e2 | ||
|
c38b4c72d0 | ||
|
0745c0eff8 | ||
|
e90f6c7eef | ||
|
7b1a0ef89c | ||
|
75b27485e1 | ||
|
a64cdfaf78 | ||
|
4b6ac1f4c4 | ||
|
683a4f906e | ||
|
e70a983a55 | ||
|
9ce8afe507 | ||
|
f67fb4d520 | ||
|
d00068423f | ||
|
7336877928 | ||
|
543fce4f29 | ||
|
38feea0fa4 | ||
|
7af584d649 | ||
|
1ba7beb7bc | ||
|
f4f4accd34 | ||
|
ae22a52f2f | ||
|
22f74b60ce | ||
|
253eb2240b | ||
|
6c924e6128 | ||
|
930cf87103 | ||
|
45c9f3b90a | ||
|
13f81efe98 | ||
|
98c5830013 | ||
|
8332d1aa39 | ||
|
6f7445be18 | ||
|
fb30478123 | ||
|
5d7b416373 | ||
|
4de754328f | ||
|
c4b9cc7292 | ||
|
fa750c9fd0 | ||
|
2a945de069 | ||
|
d0acd3e06e | ||
|
3565a9a817 | ||
|
b3d1fd79a8 | ||
|
b64084d248 | ||
|
e120536d87 | ||
|
f52cf760e4 | ||
|
94d790c159 | ||
|
f78506161d | ||
|
592b05cd87 | ||
|
6c8f9ce756 |
578 changed files with 351478 additions and 337322 deletions
32
.github/workflows/cd.yml
vendored
32
.github/workflows/cd.yml
vendored
|
@ -19,37 +19,37 @@ on:
|
|||
|
||||
jobs:
|
||||
windows_package:
|
||||
runs-on: windows-2019
|
||||
runs-on: windows-2022
|
||||
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]
|
||||
libtorrent: [2.0.7, 1.2.19]
|
||||
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Checkout Deluge source to subdir to enable packaging any tag/commit
|
||||
- name: Checkout Deluge source
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.ref }}
|
||||
fetch-depth: 0
|
||||
path: deluge_src
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python}}
|
||||
architecture: ${{ matrix.arch }}
|
||||
cache: pip
|
||||
|
||||
- name: Prepare pip
|
||||
run: python -m pip install wheel
|
||||
run: python -m pip install wheel setuptools==68.*
|
||||
|
||||
- name: Install GTK
|
||||
run: |
|
||||
|
@ -62,11 +62,14 @@ 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
|
||||
twisted[tls]==22.4.0
|
||||
--only-binary=pillow
|
||||
twisted[tls]==22.8.0
|
||||
libtorrent==${{ matrix.libtorrent }}
|
||||
pyinstaller==4.10
|
||||
pyinstaller
|
||||
pygame
|
||||
-r requirements.txt
|
||||
|
||||
|
@ -81,12 +84,13 @@ jobs:
|
|||
run: |
|
||||
pyinstaller --clean delugewin.spec --distpath freeze
|
||||
|
||||
- name: Fix OpenSSL for libtorrent x64
|
||||
if: ${{ matrix.arch == 'x64' }}
|
||||
working-directory: packaging/win/freeze/Deluge
|
||||
- name: Verify Deluge exes
|
||||
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
|
||||
deluge-debug.exe -v
|
||||
deluged-debug.exe -v
|
||||
deluge-web-debug.exe -v
|
||||
deluge-console -v
|
||||
|
||||
- name: Make Deluge Installer
|
||||
working-directory: ./packaging/win
|
||||
|
@ -94,7 +98,7 @@ jobs:
|
|||
python setup_nsis.py
|
||||
makensis /Darch=${{ matrix.arch }} deluge-win-installer.nsi
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
- uses: actions/upload-artifact@v4
|
||||
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,22 +6,25 @@ 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-20.04
|
||||
runs-on: ubuntu-22.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@v2
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: "pip"
|
||||
|
@ -33,8 +36,8 @@ jobs:
|
|||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install --upgrade pip wheel
|
||||
pip install -r requirements.txt -r requirements-tests.txt
|
||||
pip install --upgrade pip wheel setuptools
|
||||
pip install -r requirements-ci.txt
|
||||
pip install -e .
|
||||
|
||||
- name: Install security dependencies
|
||||
|
@ -46,18 +49,21 @@ jobs:
|
|||
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 catch and store
|
||||
if: github.event.inputs.core-dump == '1'
|
||||
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__)';
|
||||
catchsegv python -X dev -m pytest -v -m "not (todo or gtkui)" deluge
|
||||
$DEBUG_PREFIX pytest -v -m "not (todo or gtkui)" deluge
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
- uses: actions/upload-artifact@v4
|
||||
# capture all crashes as build artifacts
|
||||
if: failure()
|
||||
with:
|
||||
|
@ -65,19 +71,19 @@ jobs:
|
|||
path: /cores
|
||||
|
||||
test-windows:
|
||||
runs-on: windows-latest
|
||||
runs-on: windows-2022
|
||||
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@v2
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: "pip"
|
||||
|
@ -85,8 +91,8 @@ jobs:
|
|||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install --upgrade pip wheel
|
||||
pip install -r requirements.txt -r requirements-tests.txt
|
||||
pip install --upgrade pip wheel setuptools
|
||||
pip install -r requirements-ci.txt
|
||||
pip install -e .
|
||||
|
||||
- name: Test with pytest
|
||||
|
|
23
.github/workflows/docs.yml
vendored
23
.github/workflows/docs.yml
vendored
|
@ -15,30 +15,23 @@ jobs:
|
|||
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-python@v2
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
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 }}-
|
||||
python-version: "3.10"
|
||||
cache: "pip"
|
||||
cache-dependency-path: "requirements*.txt"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install --upgrade pip wheel
|
||||
pip install tox
|
||||
sudo apt-get install enchant
|
||||
sudo apt-get install enchant-2
|
||||
|
||||
- name: Test with tox
|
||||
- name: Build docs 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@v2
|
||||
- uses: actions/setup-python@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
- name: Run pre-commit linting
|
||||
uses: pre-commit/action@v2.0.2
|
||||
uses: pre-commit/action@v3.0.1
|
||||
|
|
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
|
||||
*.appdata.xml
|
||||
*.metainfo.xml
|
||||
.build_data*
|
||||
osx/app
|
||||
RELEASE-VERSION
|
||||
.venv*
|
||||
# used by setuptools to cache downloaded eggs
|
||||
/.eggs
|
||||
_pytest_temp/
|
||||
|
|
|
@ -6,35 +6,25 @@ exclude: >
|
|||
deluge/tests/data/.*svg|
|
||||
)$
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.3.0
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.6.4
|
||||
hooks:
|
||||
- id: black
|
||||
name: Fmt Black
|
||||
- id: ruff
|
||||
name: Chk Ruff
|
||||
args: [--fix]
|
||||
- id: ruff-format
|
||||
name: Fmt Ruff
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v2.5.1
|
||||
rev: v2.7.1
|
||||
hooks:
|
||||
- id: prettier
|
||||
name: Fmt Prettier
|
||||
# Workaround to list modified files only.
|
||||
args: [--list-different]
|
||||
- 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
|
||||
rev: v4.4.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]
|
||||
|
@ -44,8 +34,8 @@ repos:
|
|||
- id: trailing-whitespace
|
||||
name: Fix Trailing whitespace
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.31.0
|
||||
rev: v3.3.1
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py36-plus]
|
||||
args: [--py37-plus]
|
||||
stages: [manual]
|
||||
|
|
|
@ -5,6 +5,14 @@
|
|||
# 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
|
||||
|
@ -14,9 +22,8 @@ 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: setuptools
|
||||
- method: pip
|
||||
path: .
|
||||
|
|
26
CHANGELOG.md
26
CHANGELOG.md
|
@ -1,11 +1,31 @@
|
|||
# Changelog
|
||||
|
||||
## 2.1.0 (WIP)
|
||||
## 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
|
||||
|
||||
- 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
|
||||
|
||||
- Drop Python 2 support for Python 3 only.
|
||||
- Set libtorrent minimum required version to 1.2.
|
||||
- Add support for SVG tracker icons.
|
||||
- Fix tracker icon error handling.
|
||||
- Fix cleaning-up tracker icon temp files.
|
||||
|
|
|
@ -50,7 +50,7 @@ All modules will require the [common](#common) section dependencies.
|
|||
- [PyGObject]
|
||||
- [Pycairo]
|
||||
- [librsvg] _>= 2_
|
||||
- [libappindicator3] w/GIR - Optional: Ubuntu system tray icon.
|
||||
- [ayatanaappindicator3] 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/
|
||||
[python-appindicator]: https://packages.ubuntu.com/xenial/python-appindicator
|
||||
[ayatanaappindicator3]: https://lazka.github.io/pgi-docs/AyatanaAppIndicator3-0.1/index.html
|
||||
[librsvg]: https://wiki.gnome.org/action/show/Projects/LibRsvg
|
||||
[ifaddr]: https://pypi.org/project/ifaddr/
|
||||
|
|
6
__builtins__.pyi
Normal file
6
__builtins__.pyi
Normal file
|
@ -0,0 +1,6 @@
|
|||
from twisted.web.http import Request
|
||||
|
||||
__request__: Request
|
||||
|
||||
def _(string: str) -> str: ...
|
||||
def _n(string: str) -> str: ...
|
|
@ -14,6 +14,7 @@ Example:
|
|||
>>> from deluge._libtorrent import lt
|
||||
|
||||
"""
|
||||
|
||||
from deluge.common import VersionSplit, get_version
|
||||
from deluge.error import LibtorrentImportError
|
||||
|
||||
|
|
|
@ -85,7 +85,6 @@ def bdecode(x):
|
|||
|
||||
|
||||
class Bencached:
|
||||
|
||||
__slots__ = ['bencoded']
|
||||
|
||||
def __init__(self, s):
|
||||
|
|
117
deluge/common.py
117
deluge/common.py
|
@ -7,6 +7,7 @@
|
|||
#
|
||||
|
||||
"""Common functions for various parts of Deluge to use."""
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
import functools
|
||||
|
@ -23,15 +24,21 @@ 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:
|
||||
|
@ -90,7 +97,7 @@ def get_version():
|
|||
Returns:
|
||||
str: The version of Deluge.
|
||||
"""
|
||||
return pkg_resources.get_distribution('Deluge').version
|
||||
return distribution('Deluge').version
|
||||
|
||||
|
||||
def get_default_config_dir(filename=None):
|
||||
|
@ -290,20 +297,22 @@ def get_pixmap(fname):
|
|||
return resource_filename('deluge', os.path.join('ui', 'data', 'pixmaps', fname))
|
||||
|
||||
|
||||
def resource_filename(module, path):
|
||||
"""Get filesystem path for a resource.
|
||||
def resource_filename(module: str, path: str) -> str:
|
||||
"""Get filesystem path for a non-python resource.
|
||||
|
||||
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.
|
||||
Abstracts getting module resource files. Originally created to
|
||||
workaround pkg_resources.resource_filename limitations with
|
||||
multiple Deluge packages installed.
|
||||
"""
|
||||
return pkg_resources.get_distribution('Deluge').get_resource_filename(
|
||||
pkg_resources._manager, os.path.join(*(module.split('.') + [path]))
|
||||
)
|
||||
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:]))
|
||||
|
||||
|
||||
def open_file(path, timestamp=None):
|
||||
|
@ -415,25 +424,31 @@ def translate_size_units():
|
|||
|
||||
|
||||
def fsize(fsize_b, precision=1, shortform=False):
|
||||
"""Formats the bytes value into a string with KiB, MiB or GiB units.
|
||||
"""Formats the bytes value into a string with KiB, MiB, GiB or TiB units.
|
||||
|
||||
Args:
|
||||
fsize_b (int): The filesize in bytes.
|
||||
precision (int): The filesize float precision.
|
||||
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 in KiB, MiB or GiB units.
|
||||
str: A formatted string in KiB, MiB, GiB or TiB 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:
|
||||
|
@ -469,7 +484,7 @@ def fpcnt(dec, precision=2):
|
|||
|
||||
Args:
|
||||
dec (float): The ratio in the range [0.0, 1.0].
|
||||
precision (int): The percentage float precision.
|
||||
precision (int): The output float precision, 2 by default.
|
||||
|
||||
Returns:
|
||||
str: A formatted string representing a percentage.
|
||||
|
@ -493,6 +508,8 @@ 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.
|
||||
|
@ -501,6 +518,10 @@ 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:
|
||||
|
@ -537,7 +558,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 total_peers < 0, just 'num_peers'.
|
||||
str: A formatted string 'num_peers (total_peers)' or if total_peers < 0, just 'num_peers'.
|
||||
|
||||
Examples:
|
||||
>>> fpeer(10, 20)
|
||||
|
@ -586,16 +607,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 systems timezone.
|
||||
"""Formats a date time string in the locale's date representation based on the system's timezone.
|
||||
|
||||
Args:
|
||||
seconds (float): Time in seconds since the Epoch.
|
||||
precision_secs (bool): Include seconds in time format.
|
||||
date_only (bool): Whether to include only the date, False by default.
|
||||
precision_secs (bool): Include seconds in time format, False by default.
|
||||
|
||||
Returns:
|
||||
str: A string in the locale's datetime representation or "" if seconds < 0
|
||||
|
@ -620,10 +641,14 @@ def tokenize(text):
|
|||
Returns:
|
||||
list: A list of strings and/or numbers.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
"""
|
||||
tokenized_input = []
|
||||
for token in re.split(r'(\d+(?:\.\d+)?)', text):
|
||||
|
@ -644,12 +669,16 @@ 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},
|
||||
]
|
||||
|
||||
|
||||
|
@ -692,6 +721,16 @@ 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
|
||||
|
@ -734,6 +773,8 @@ MAGNET_SCHEME = 'magnet:?'
|
|||
XT_BTIH_PARAM = 'xt=urn:btih:'
|
||||
DN_PARAM = 'dn='
|
||||
TR_PARAM = 'tr='
|
||||
TR_TIER_PARAM = 'tr.'
|
||||
TR_TIER_REGEX = re.compile(r'^tr.(\d+)=(\S+)')
|
||||
|
||||
|
||||
def is_magnet(uri):
|
||||
|
@ -776,8 +817,6 @@ def get_magnet_info(uri):
|
|||
|
||||
"""
|
||||
|
||||
tr0_param = 'tr.'
|
||||
tr0_param_regex = re.compile(r'^tr.(\d+)=(\S+)')
|
||||
if not uri.startswith(MAGNET_SCHEME):
|
||||
return {}
|
||||
|
||||
|
@ -805,12 +844,14 @@ def get_magnet_info(uri):
|
|||
tracker = unquote_plus(param[len(TR_PARAM) :])
|
||||
trackers[tracker] = tier
|
||||
tier += 1
|
||||
elif param.startswith(tr0_param):
|
||||
try:
|
||||
tier, tracker = re.match(tr0_param_regex, param).groups()
|
||||
trackers[tracker] = tier
|
||||
except AttributeError:
|
||||
pass
|
||||
elif param.startswith(TR_TIER_PARAM):
|
||||
tracker_match = re.match(TR_TIER_REGEX, param)
|
||||
if not tracker_match:
|
||||
continue
|
||||
|
||||
tier, tracker = tracker_match.groups()
|
||||
tracker = unquote_plus(tracker)
|
||||
trackers[tracker] = int(tier)
|
||||
|
||||
if info_hash:
|
||||
if not name:
|
||||
|
@ -831,7 +872,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 dict or {tracker: tier} pairs.
|
||||
trackers (list or dict, optional): A list of trackers or a dict or some {tracker: tier} pairs.
|
||||
|
||||
Returns:
|
||||
str: A magnet URI string.
|
||||
|
@ -873,7 +914,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,11 +59,16 @@ class Component:
|
|||
Deluge core.
|
||||
|
||||
**update()** - This method is called every 1 second by default while the
|
||||
Componented is in a *Started* state. The interval can be
|
||||
Component 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().
|
||||
|
@ -80,10 +85,10 @@ class Component:
|
|||
|
||||
**Stopped** - The Component has either been stopped or has yet to be started.
|
||||
|
||||
**Stopping** - The Component has had it's stop method called, but it hasn't
|
||||
**Stopping** - The Component has had its stop method called, but it hasn't
|
||||
fully stopped yet.
|
||||
|
||||
**Paused** - The Component has had it's update timer stopped, but will
|
||||
**Paused** - The Component has had its update timer stopped, but will
|
||||
still be considered in a Started state.
|
||||
|
||||
"""
|
||||
|
@ -111,9 +116,8 @@ class Component:
|
|||
_ComponentRegistry.deregister(self)
|
||||
|
||||
def _component_start_timer(self):
|
||||
if hasattr(self, 'update'):
|
||||
self._component_timer = LoopingCall(self.update)
|
||||
self._component_timer.start(self._component_interval)
|
||||
self._component_timer = LoopingCall(self.update)
|
||||
self._component_timer.start(self._component_interval)
|
||||
|
||||
def _component_start(self):
|
||||
def on_start(result):
|
||||
|
@ -129,13 +133,10 @@ class Component:
|
|||
return fail(result)
|
||||
|
||||
if self._component_state == 'Stopped':
|
||||
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)
|
||||
self._component_state = 'Starting'
|
||||
d = deferLater(reactor, 0, self.start)
|
||||
d.addCallbacks(on_start, on_start_fail)
|
||||
self._component_starting_deferred = d
|
||||
elif self._component_state == 'Starting':
|
||||
return self._component_starting_deferred
|
||||
elif self._component_state == 'Started':
|
||||
|
@ -165,14 +166,11 @@ class Component:
|
|||
return result
|
||||
|
||||
if self._component_state != 'Stopped' and self._component_state != 'Stopping':
|
||||
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)
|
||||
self._component_state = 'Stopping'
|
||||
d = maybeDeferred(self.stop)
|
||||
d.addCallback(on_stop)
|
||||
d.addErrback(on_stop_fail)
|
||||
self._component_stopping_deferred = d
|
||||
|
||||
if self._component_state == 'Stopping':
|
||||
return self._component_stopping_deferred
|
||||
|
@ -182,13 +180,12 @@ 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':
|
||||
if self._component_timer and self._component_timer.running:
|
||||
d = maybeDeferred(self._component_timer.stop)
|
||||
d.addCallback(on_pause)
|
||||
else:
|
||||
d = succeed(None)
|
||||
d = maybeDeferred(self.pause)
|
||||
d.addCallback(on_pause)
|
||||
elif self._component_state == 'Paused':
|
||||
d = succeed(None)
|
||||
else:
|
||||
|
@ -205,9 +202,10 @@ 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._component_start_timer)
|
||||
d = maybeDeferred(self.resume)
|
||||
d.addCallback(on_resume)
|
||||
else:
|
||||
d = fail(
|
||||
|
@ -222,9 +220,7 @@ class Component:
|
|||
|
||||
def _component_shutdown(self):
|
||||
def on_stop(result):
|
||||
if hasattr(self, 'shutdown'):
|
||||
return maybeDeferred(self.shutdown)
|
||||
return succeed(None)
|
||||
return maybeDeferred(self.shutdown)
|
||||
|
||||
d = self._component_stop()
|
||||
d.addCallback(on_stop)
|
||||
|
@ -245,6 +241,12 @@ 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,6 +38,7 @@ 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
|
||||
from twisted.internet.error import CannotListenError, ProcessTerminated
|
||||
from twisted.python.failure import Failure
|
||||
|
||||
import deluge.component as _component
|
||||
|
@ -42,15 +42,18 @@ def mock_callback():
|
|||
The returned Mock instance will have a `deferred` attribute which will complete when the callback has been called.
|
||||
"""
|
||||
|
||||
def reset():
|
||||
def reset(timeout=0.5, *args, **kwargs):
|
||||
if mock.called:
|
||||
original_reset_mock()
|
||||
deferred = Deferred()
|
||||
deferred.addTimeout(0.5, reactor)
|
||||
original_reset_mock(*args, **kwargs)
|
||||
if mock.deferred:
|
||||
mock.deferred.cancel()
|
||||
deferred = Deferred(canceller=lambda x: deferred.callback(None))
|
||||
deferred.addTimeout(timeout, 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()
|
||||
|
@ -59,8 +62,9 @@ def mock_callback():
|
|||
|
||||
@pytest.fixture
|
||||
def config_dir(tmp_path):
|
||||
deluge.configmanager.set_config_dir(tmp_path)
|
||||
yield tmp_path
|
||||
config_dir = tmp_path / 'config'
|
||||
deluge.configmanager.set_config_dir(config_dir)
|
||||
yield config_dir
|
||||
|
||||
|
||||
@pytest_twisted.async_yield_fixture()
|
||||
|
@ -84,9 +88,10 @@ async def client(request, config_dir, monkeypatch, listen_port):
|
|||
|
||||
|
||||
@pytest_twisted.async_yield_fixture
|
||||
async def daemon(request, config_dir):
|
||||
async def daemon(request, config_dir, tmp_path):
|
||||
listen_port = DEFAULT_LISTEN_PORT
|
||||
logfile = f'daemon_{request.node.name}.log'
|
||||
logfile = tmp_path / 'daemon.log'
|
||||
|
||||
if hasattr(request.cls, 'daemon_custom_script'):
|
||||
custom_script = request.cls.daemon_custom_script
|
||||
else:
|
||||
|
@ -116,7 +121,10 @@ async def daemon(request, config_dir):
|
|||
raise exception_error
|
||||
daemon.listen_port = listen_port
|
||||
yield daemon
|
||||
await daemon.kill()
|
||||
try:
|
||||
await daemon.kill()
|
||||
except ProcessTerminated:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
|
@ -137,7 +145,7 @@ def common_fixture(config_dir, request, monkeypatch, listen_port):
|
|||
|
||||
|
||||
@pytest_twisted.async_yield_fixture(scope='function')
|
||||
async def component(request):
|
||||
async def component():
|
||||
"""Verify component registry is clean, and clean up after test."""
|
||||
if len(_component._ComponentRegistry.components) != 0:
|
||||
warnings.warn(
|
||||
|
@ -190,3 +198,18 @@ 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,10 +14,16 @@ This should typically only be used by the Core. Plugins should utilize the
|
|||
`:mod:EventManager` for similar functionality.
|
||||
|
||||
"""
|
||||
import logging
|
||||
from types import SimpleNamespace
|
||||
|
||||
from twisted.internet import reactor
|
||||
import contextlib
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from functools import partial
|
||||
from typing import Any, Callable
|
||||
|
||||
from twisted.internet import reactor, task, threads
|
||||
|
||||
import deluge.component as component
|
||||
from deluge._libtorrent import lt
|
||||
|
@ -31,7 +37,7 @@ class AlertManager(component.Component):
|
|||
|
||||
def __init__(self):
|
||||
log.debug('AlertManager init...')
|
||||
component.Component.__init__(self, 'AlertManager', interval=0.3)
|
||||
component.Component.__init__(self, 'AlertManager')
|
||||
self.session = component.get('Core').session
|
||||
|
||||
# Increase the alert queue size so that alerts don't get lost.
|
||||
|
@ -52,48 +58,88 @@ class AlertManager(component.Component):
|
|||
self.session.apply_settings({'alert_mask': alert_mask})
|
||||
|
||||
# handlers is a dictionary of lists {"alert_type": [handler1,h2,..]}
|
||||
self.handlers = {}
|
||||
self.handlers = defaultdict(list)
|
||||
self.handlers_timeout_secs = 2
|
||||
self.delayed_calls = []
|
||||
self._event = threading.Event()
|
||||
|
||||
def update(self):
|
||||
self.delayed_calls = [dc for dc in self.delayed_calls if dc.active()]
|
||||
self.handle_alerts()
|
||||
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:
|
||||
if delayed_call.active():
|
||||
delayed_call.cancel()
|
||||
delayed_call.cancel()
|
||||
self.delayed_calls = []
|
||||
|
||||
def register_handler(self, alert_type, handler):
|
||||
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.handle_alerts()
|
||||
|
||||
def register_handler(self, alert_type: str, handler: Callable[[Any], None]) -> None:
|
||||
"""
|
||||
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.
|
||||
|
||||
: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
|
||||
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.
|
||||
"""
|
||||
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] = []
|
||||
if alert_type and alert_type.endswith('_alert'):
|
||||
alert_type = alert_type[: -len('_alert')]
|
||||
|
||||
# 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):
|
||||
def deregister_handler(self, handler: Callable[[Any], None]):
|
||||
"""
|
||||
De-registers the `:param:handler` function from all alert types.
|
||||
De-registers the `handler` function from all alert types.
|
||||
|
||||
:param handler: func, the handler function to deregister
|
||||
Args:
|
||||
handler: The handler function to deregister.
|
||||
"""
|
||||
# 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)
|
||||
for alert_type_handlers in self.handlers.values():
|
||||
with contextlib.suppress(ValueError):
|
||||
alert_type_handlers.remove(handler)
|
||||
|
||||
def handle_alerts(self):
|
||||
"""
|
||||
|
@ -112,26 +158,32 @@ class AlertManager(component.Component):
|
|||
num_alerts,
|
||||
)
|
||||
|
||||
# Loop through all alerts in the queue
|
||||
for alert in alerts:
|
||||
alert_type = type(alert).__name__
|
||||
alert_type = alert.what()
|
||||
|
||||
# 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
|
||||
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))
|
||||
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)
|
||||
|
||||
def set_alert_queue_size(self, queue_size):
|
||||
"""Sets the maximum size of the libtorrent alert queue"""
|
||||
|
|
|
@ -12,17 +12,16 @@ 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
|
||||
from twisted.internet import defer, reactor, task, threads
|
||||
from twisted.web.client import Agent, readBody
|
||||
|
||||
import deluge.common
|
||||
import deluge.component as component
|
||||
from deluge import path_chooser_common
|
||||
from deluge import metafile, path_chooser_common
|
||||
from deluge._libtorrent import LT_VERSION, lt
|
||||
from deluge.configmanager import ConfigManager, get_config_dir
|
||||
from deluge.core.alertmanager import AlertManager
|
||||
|
@ -199,7 +198,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_alert', self._on_alert_session_stats
|
||||
'session_stats', self._on_alert_session_stats
|
||||
)
|
||||
self.session_rates_timer_interval = 2
|
||||
self.session_rates_timer = task.LoopingCall(self._update_session_rates)
|
||||
|
@ -992,31 +991,33 @@ class Core(component.Component):
|
|||
path,
|
||||
tracker,
|
||||
piece_length,
|
||||
comment,
|
||||
target,
|
||||
webseeds,
|
||||
private,
|
||||
created_by,
|
||||
trackers,
|
||||
add_to_session,
|
||||
comment=None,
|
||||
target=None,
|
||||
webseeds=None,
|
||||
private=False,
|
||||
created_by=None,
|
||||
trackers=None,
|
||||
add_to_session=False,
|
||||
torrent_format=metafile.TorrentFormat.V1,
|
||||
):
|
||||
if isinstance(torrent_format, str):
|
||||
torrent_format = metafile.TorrentFormat(torrent_format)
|
||||
|
||||
log.debug('creating torrent..')
|
||||
threading.Thread(
|
||||
target=self._create_torrent_thread,
|
||||
args=(
|
||||
path,
|
||||
tracker,
|
||||
piece_length,
|
||||
comment,
|
||||
target,
|
||||
webseeds,
|
||||
private,
|
||||
created_by,
|
||||
trackers,
|
||||
add_to_session,
|
||||
),
|
||||
).start()
|
||||
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,
|
||||
)
|
||||
|
||||
def _create_torrent_thread(
|
||||
self,
|
||||
|
@ -1030,27 +1031,41 @@ class Core(component.Component):
|
|||
created_by,
|
||||
trackers,
|
||||
add_to_session,
|
||||
torrent_format,
|
||||
):
|
||||
from deluge import metafile
|
||||
|
||||
metafile.make_meta_file(
|
||||
filecontent = metafile.make_meta_file_content(
|
||||
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]
|
||||
with open(target, 'rb') as _file:
|
||||
filedump = b64encode(_file.read())
|
||||
self.add_torrent_file(os.path.split(target)[1], filedump, options)
|
||||
self.add_torrent_file(filename, filedump, options)
|
||||
return filename, filedump
|
||||
|
||||
@export
|
||||
def upload_plugin(self, filename: str, filedump: Union[str, bytes]) -> None:
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
#
|
||||
|
||||
"""The Deluge daemon"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
|
||||
"""PluginManager for Core"""
|
||||
|
||||
import logging
|
||||
|
||||
from twisted.internet import defer
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
#
|
||||
|
||||
"""RPCServer Module"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
@ -46,13 +47,11 @@ 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):
|
||||
|
@ -274,9 +273,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()
|
||||
|
@ -545,8 +544,8 @@ class RPCServer(component.Component):
|
|||
:type event: :class:`deluge.event.DelugeEvent`
|
||||
"""
|
||||
log.debug('intevents: %s', self.factory.interested_events)
|
||||
# Find sessions interested in this event
|
||||
for session_id, interest in self.factory.interested_events.items():
|
||||
# Use copy of `interested_events` since it can mutate while iterating.
|
||||
for session_id, interest in self.factory.interested_events.copy().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,9 +1138,8 @@ class Torrent:
|
|||
'download_location': lambda: self.options['download_location'],
|
||||
'seeds_peers_ratio': lambda: -1.0
|
||||
if self.status.num_incomplete == 0
|
||||
else ( # Use -1.0 to signify infinity
|
||||
self.status.num_complete / self.status.num_incomplete
|
||||
),
|
||||
# Use -1.0 to signify infinity
|
||||
else (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'],
|
||||
|
@ -1544,20 +1543,18 @@ class Torrent:
|
|||
self.status.pieces, self.handle.piece_availability()
|
||||
):
|
||||
if piece:
|
||||
pieces.append(3) # Completed.
|
||||
# Completed.
|
||||
pieces.append(3)
|
||||
elif avail_piece:
|
||||
pieces.append(
|
||||
1
|
||||
) # Available, just not downloaded nor being downloaded.
|
||||
# Available, just not downloaded nor being downloaded.
|
||||
pieces.append(1)
|
||||
else:
|
||||
pieces.append(
|
||||
0
|
||||
) # Missing, no known peer with piece, or not asked for yet.
|
||||
# Missing, no known peer with piece, or not asked for yet.
|
||||
pieces.append(0)
|
||||
|
||||
for peer_info in self.handle.get_peer_info():
|
||||
if peer_info.downloading_piece_index >= 0:
|
||||
pieces[
|
||||
peer_info.downloading_piece_index
|
||||
] = 2 # Being downloaded from peer.
|
||||
# Being downloaded from peer.
|
||||
pieces[peer_info.downloading_piece_index] = 2
|
||||
|
||||
return pieces
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
#
|
||||
|
||||
"""TorrentManager handles Torrent objects"""
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import operator
|
||||
|
@ -50,10 +51,10 @@ from deluge.event import (
|
|||
log = logging.getLogger(__name__)
|
||||
|
||||
LT_DEFAULT_ADD_TORRENT_FLAGS = (
|
||||
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
|
||||
lt.torrent_flags.paused
|
||||
| lt.torrent_flags.auto_managed
|
||||
| lt.torrent_flags.update_subscribe
|
||||
| lt.torrent_flags.apply_ip_filter
|
||||
)
|
||||
|
||||
|
||||
|
@ -202,34 +203,32 @@ class TorrentManager(component.Component):
|
|||
|
||||
# Register alert functions
|
||||
alert_handles = [
|
||||
'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',
|
||||
'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',
|
||||
]
|
||||
|
||||
for alert_handle in alert_handles:
|
||||
on_alert_func = getattr(
|
||||
self, ''.join(['on_alert_', alert_handle.replace('_alert', '')])
|
||||
)
|
||||
on_alert_func = getattr(self, ''.join(['on_alert_', alert_handle]))
|
||||
self.alerts.register_handler(alert_handle, on_alert_func)
|
||||
|
||||
# Define timers
|
||||
|
@ -292,8 +291,8 @@ class TorrentManager(component.Component):
|
|||
if torrent.options['remove_at_ratio']:
|
||||
self.remove(torrent_id)
|
||||
break
|
||||
if not torrent.status.paused:
|
||||
torrent.pause()
|
||||
|
||||
torrent.pause()
|
||||
|
||||
def __getitem__(self, torrent_id):
|
||||
"""Return the Torrent with torrent_id.
|
||||
|
@ -369,11 +368,11 @@ class TorrentManager(component.Component):
|
|||
add_torrent_params.flags = (
|
||||
(
|
||||
LT_DEFAULT_ADD_TORRENT_FLAGS
|
||||
| lt.add_torrent_params_flags_t.flag_duplicate_is_error
|
||||
| lt.add_torrent_params_flags_t.flag_upload_mode
|
||||
| lt.torrent_flags.duplicate_is_error
|
||||
| lt.torrent_flags.upload_mode
|
||||
)
|
||||
^ lt.add_torrent_params_flags_t.flag_auto_managed
|
||||
^ lt.add_torrent_params_flags_t.flag_paused
|
||||
^ lt.torrent_flags.auto_managed
|
||||
^ lt.torrent_flags.paused
|
||||
)
|
||||
|
||||
torrent_handle = self.session.add_torrent(add_torrent_params)
|
||||
|
@ -436,8 +435,8 @@ class TorrentManager(component.Component):
|
|||
magnet_info = get_magnet_info(magnet)
|
||||
if magnet_info:
|
||||
add_torrent_params['name'] = magnet_info['name']
|
||||
add_torrent_params['trackers'] = list(magnet_info['trackers'])
|
||||
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))
|
||||
else:
|
||||
raise AddTorrentError(
|
||||
|
@ -481,16 +480,12 @@ 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.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
|
||||
LT_DEFAULT_ADD_TORRENT_FLAGS | lt.torrent_flags.duplicate_is_error
|
||||
) ^ lt.torrent_flags.auto_managed
|
||||
if options['seed_mode']:
|
||||
add_torrent_params['flags'] |= lt.add_torrent_params_flags_t.flag_seed_mode
|
||||
add_torrent_params['flags'] |= lt.torrent_flags.seed_mode
|
||||
if options['super_seeding']:
|
||||
add_torrent_params[
|
||||
'flags'
|
||||
] |= lt.add_torrent_params_flags_t.flag_super_seeding
|
||||
add_torrent_params['flags'] |= lt.torrent_flags.super_seeding
|
||||
|
||||
return torrent_id, add_torrent_params
|
||||
|
||||
|
|
|
@ -166,7 +166,8 @@ 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
|
||||
|
@ -195,17 +196,33 @@ class CoroutineDeferred(defer.Deferred):
|
|||
d = defer.ensureDeferred(self.coro)
|
||||
d.chainDeferred(self)
|
||||
|
||||
def addCallbacks(self, *args, **kwargs): # noqa: N802
|
||||
def _callback_activate(self):
|
||||
"""Verify awaited status before calling activate."""
|
||||
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,6 +13,7 @@ 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 cgi
|
||||
import email.message
|
||||
import logging
|
||||
import os.path
|
||||
import zlib
|
||||
|
@ -133,9 +133,10 @@ class HTTPDownloaderAgent:
|
|||
content_disp = headers.getRawHeaders(b'content-disposition')[0].decode(
|
||||
'utf-8'
|
||||
)
|
||||
content_disp_params = cgi.parse_header(content_disp)[1]
|
||||
if 'filename' in content_disp_params:
|
||||
new_file_name = content_disp_params['filename']
|
||||
message = email.message.EmailMessage()
|
||||
message['content-disposition'] = content_disp
|
||||
new_file_name = message.get_filename()
|
||||
if new_file_name:
|
||||
new_file_name = sanitise_filename(new_file_name)
|
||||
new_file_name = os.path.join(
|
||||
os.path.split(self.filename)[0], new_file_name
|
||||
|
@ -152,7 +153,10 @@ class HTTPDownloaderAgent:
|
|||
self.filename = new_file_name
|
||||
|
||||
cont_type_header = headers.getRawHeaders(b'content-type')[0].decode()
|
||||
cont_type, params = cgi.parse_header(cont_type_header)
|
||||
message = email.message.EmailMessage()
|
||||
message['content-type'] = cont_type_header
|
||||
cont_type = message.get_content_type()
|
||||
params = message['content-type'].params
|
||||
# Only re-ecode text content types.
|
||||
encoding = None
|
||||
if cont_type.startswith('text/'):
|
||||
|
|
8454
deluge/i18n/af.po
8454
deluge/i18n/af.po
File diff suppressed because it is too large
Load diff
8487
deluge/i18n/ar.po
8487
deluge/i18n/ar.po
File diff suppressed because it is too large
Load diff
8445
deluge/i18n/ast.po
8445
deluge/i18n/ast.po
File diff suppressed because it is too large
Load diff
9698
deluge/i18n/be.po
9698
deluge/i18n/be.po
File diff suppressed because it is too large
Load diff
8488
deluge/i18n/bg.po
8488
deluge/i18n/bg.po
File diff suppressed because it is too large
Load diff
8312
deluge/i18n/bn.po
8312
deluge/i18n/bn.po
File diff suppressed because it is too large
Load diff
8338
deluge/i18n/bs.po
8338
deluge/i18n/bs.po
File diff suppressed because it is too large
Load diff
8681
deluge/i18n/ca.po
8681
deluge/i18n/ca.po
File diff suppressed because it is too large
Load diff
8514
deluge/i18n/cs.po
8514
deluge/i18n/cs.po
File diff suppressed because it is too large
Load diff
8304
deluge/i18n/cy.po
8304
deluge/i18n/cy.po
File diff suppressed because it is too large
Load diff
8567
deluge/i18n/da.po
8567
deluge/i18n/da.po
File diff suppressed because it is too large
Load diff
9346
deluge/i18n/de.po
9346
deluge/i18n/de.po
File diff suppressed because it is too large
Load diff
6212
deluge/i18n/deluge.pot
Normal file
6212
deluge/i18n/deluge.pot
Normal file
File diff suppressed because it is too large
Load diff
8528
deluge/i18n/el.po
8528
deluge/i18n/el.po
File diff suppressed because it is too large
Load diff
8510
deluge/i18n/en_AU.po
8510
deluge/i18n/en_AU.po
File diff suppressed because it is too large
Load diff
8510
deluge/i18n/en_CA.po
8510
deluge/i18n/en_CA.po
File diff suppressed because it is too large
Load diff
9989
deluge/i18n/en_GB.po
9989
deluge/i18n/en_GB.po
File diff suppressed because it is too large
Load diff
8328
deluge/i18n/eo.po
8328
deluge/i18n/eo.po
File diff suppressed because it is too large
Load diff
8518
deluge/i18n/es.po
8518
deluge/i18n/es.po
File diff suppressed because it is too large
Load diff
8483
deluge/i18n/et.po
8483
deluge/i18n/et.po
File diff suppressed because it is too large
Load diff
8508
deluge/i18n/eu.po
8508
deluge/i18n/eu.po
File diff suppressed because it is too large
Load diff
8411
deluge/i18n/fa.po
8411
deluge/i18n/fa.po
File diff suppressed because it is too large
Load diff
8620
deluge/i18n/fi.po
8620
deluge/i18n/fi.po
File diff suppressed because it is too large
Load diff
8353
deluge/i18n/fo.po
8353
deluge/i18n/fo.po
File diff suppressed because it is too large
Load diff
8524
deluge/i18n/fr.po
8524
deluge/i18n/fr.po
File diff suppressed because it is too large
Load diff
8378
deluge/i18n/fy.po
8378
deluge/i18n/fy.po
File diff suppressed because it is too large
Load diff
8296
deluge/i18n/ga.po
8296
deluge/i18n/ga.po
File diff suppressed because it is too large
Load diff
8500
deluge/i18n/gl.po
8500
deluge/i18n/gl.po
File diff suppressed because it is too large
Load diff
8480
deluge/i18n/he.po
8480
deluge/i18n/he.po
File diff suppressed because it is too large
Load diff
8489
deluge/i18n/hi.po
8489
deluge/i18n/hi.po
File diff suppressed because it is too large
Load diff
8518
deluge/i18n/hr.po
8518
deluge/i18n/hr.po
File diff suppressed because it is too large
Load diff
8484
deluge/i18n/hu.po
8484
deluge/i18n/hu.po
File diff suppressed because it is too large
Load diff
8359
deluge/i18n/id.po
8359
deluge/i18n/id.po
File diff suppressed because it is too large
Load diff
8443
deluge/i18n/is.po
8443
deluge/i18n/is.po
File diff suppressed because it is too large
Load diff
8514
deluge/i18n/it.po
8514
deluge/i18n/it.po
File diff suppressed because it is too large
Load diff
8300
deluge/i18n/iu.po
8300
deluge/i18n/iu.po
File diff suppressed because it is too large
Load diff
8488
deluge/i18n/ja.po
8488
deluge/i18n/ja.po
File diff suppressed because it is too large
Load diff
8498
deluge/i18n/ka.po
8498
deluge/i18n/ka.po
File diff suppressed because it is too large
Load diff
8504
deluge/i18n/kk.po
8504
deluge/i18n/kk.po
File diff suppressed because it is too large
Load diff
8312
deluge/i18n/km.po
8312
deluge/i18n/km.po
File diff suppressed because it is too large
Load diff
8335
deluge/i18n/kn.po
8335
deluge/i18n/kn.po
File diff suppressed because it is too large
Load diff
8488
deluge/i18n/ko.po
8488
deluge/i18n/ko.po
File diff suppressed because it is too large
Load diff
8300
deluge/i18n/ku.po
8300
deluge/i18n/ku.po
File diff suppressed because it is too large
Load diff
8300
deluge/i18n/ky.po
8300
deluge/i18n/ky.po
File diff suppressed because it is too large
Load diff
8300
deluge/i18n/la.po
8300
deluge/i18n/la.po
File diff suppressed because it is too large
Load diff
8316
deluge/i18n/lb.po
8316
deluge/i18n/lb.po
File diff suppressed because it is too large
Load diff
8522
deluge/i18n/lt.po
8522
deluge/i18n/lt.po
File diff suppressed because it is too large
Load diff
8520
deluge/i18n/lv.po
8520
deluge/i18n/lv.po
File diff suppressed because it is too large
Load diff
8494
deluge/i18n/mk.po
8494
deluge/i18n/mk.po
File diff suppressed because it is too large
Load diff
8296
deluge/i18n/ml.po
8296
deluge/i18n/ml.po
File diff suppressed because it is too large
Load diff
6214
deluge/i18n/mo.po
Normal file
6214
deluge/i18n/mo.po
Normal file
File diff suppressed because it is too large
Load diff
8512
deluge/i18n/ms.po
8512
deluge/i18n/ms.po
File diff suppressed because it is too large
Load diff
8312
deluge/i18n/nap.po
8312
deluge/i18n/nap.po
File diff suppressed because it is too large
Load diff
8487
deluge/i18n/nb.po
8487
deluge/i18n/nb.po
File diff suppressed because it is too large
Load diff
7024
deluge/i18n/nds.po
7024
deluge/i18n/nds.po
File diff suppressed because it is too large
Load diff
8516
deluge/i18n/nl.po
8516
deluge/i18n/nl.po
File diff suppressed because it is too large
Load diff
8454
deluge/i18n/nn.po
8454
deluge/i18n/nn.po
File diff suppressed because it is too large
Load diff
8467
deluge/i18n/oc.po
8467
deluge/i18n/oc.po
File diff suppressed because it is too large
Load diff
8617
deluge/i18n/pl.po
8617
deluge/i18n/pl.po
File diff suppressed because it is too large
Load diff
8300
deluge/i18n/pms.po
8300
deluge/i18n/pms.po
File diff suppressed because it is too large
Load diff
8510
deluge/i18n/pt.po
8510
deluge/i18n/pt.po
File diff suppressed because it is too large
Load diff
8512
deluge/i18n/pt_BR.po
8512
deluge/i18n/pt_BR.po
File diff suppressed because it is too large
Load diff
8514
deluge/i18n/ro.po
8514
deluge/i18n/ro.po
File diff suppressed because it is too large
Load diff
10139
deluge/i18n/ru.po
10139
deluge/i18n/ru.po
File diff suppressed because it is too large
Load diff
8496
deluge/i18n/si.po
8496
deluge/i18n/si.po
File diff suppressed because it is too large
Load diff
8513
deluge/i18n/sk.po
8513
deluge/i18n/sk.po
File diff suppressed because it is too large
Load diff
8508
deluge/i18n/sl.po
8508
deluge/i18n/sl.po
File diff suppressed because it is too large
Load diff
8514
deluge/i18n/sr.po
8514
deluge/i18n/sr.po
File diff suppressed because it is too large
Load diff
8630
deluge/i18n/sv.po
8630
deluge/i18n/sv.po
File diff suppressed because it is too large
Load diff
8300
deluge/i18n/ta.po
8300
deluge/i18n/ta.po
File diff suppressed because it is too large
Load diff
8300
deluge/i18n/te.po
8300
deluge/i18n/te.po
File diff suppressed because it is too large
Load diff
8296
deluge/i18n/th.po
8296
deluge/i18n/th.po
File diff suppressed because it is too large
Load diff
8300
deluge/i18n/tl.po
8300
deluge/i18n/tl.po
File diff suppressed because it is too large
Load diff
8300
deluge/i18n/tlh.po
8300
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