mirror of
https://github.com/codex-storage/deluge.git
synced 2025-01-12 12:34:43 +00:00
Fix #229 add 'date added' column, with patch from Lajnold
This commit is contained in:
parent
a2a3bd2148
commit
e5cbca13dc
@ -68,7 +68,7 @@ STATUS_KEYS = ['active_time', 'compact', 'distributed_copies', 'download_payload
|
|||||||
'move_on_completed_path', 'name', 'next_announce', 'num_files', 'num_peers', 'num_pieces',
|
'move_on_completed_path', 'name', 'next_announce', 'num_files', 'num_peers', 'num_pieces',
|
||||||
'num_seeds', 'paused', 'peers', 'piece_length', 'prioritize_first_last', 'private', 'progress',
|
'num_seeds', 'paused', 'peers', 'piece_length', 'prioritize_first_last', 'private', 'progress',
|
||||||
'queue', 'ratio', 'remove_at_ratio', 'save_path', 'seed_rank', 'seeding_time', 'state', 'stop_at_ratio',
|
'queue', 'ratio', 'remove_at_ratio', 'save_path', 'seed_rank', 'seeding_time', 'state', 'stop_at_ratio',
|
||||||
'stop_ratio', 'total_done', 'total_payload_download', 'total_payload_upload', 'total_peers',
|
'stop_ratio', 'time_added', 'total_done', 'total_payload_download', 'total_payload_upload', 'total_peers',
|
||||||
'total_seeds', 'total_size', 'total_uploaded', 'total_wanted', 'tracker', 'tracker_host',
|
'total_seeds', 'total_size', 'total_uploaded', 'total_wanted', 'tracker', 'tracker_host',
|
||||||
'tracker_status', 'trackers', 'upload_payload_rate']
|
'tracker_status', 'trackers', 'upload_payload_rate']
|
||||||
|
|
||||||
|
@ -34,6 +34,7 @@
|
|||||||
"""Internal Torrent class"""
|
"""Internal Torrent class"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
from urlparse import urlparse
|
from urlparse import urlparse
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -198,6 +199,11 @@ class Torrent:
|
|||||||
# The tracker status
|
# The tracker status
|
||||||
self.tracker_status = ""
|
self.tracker_status = ""
|
||||||
|
|
||||||
|
if state:
|
||||||
|
self.time_added = state.time_added
|
||||||
|
else:
|
||||||
|
self.time_added = time.time()
|
||||||
|
|
||||||
log.debug("Torrent object created.")
|
log.debug("Torrent object created.")
|
||||||
|
|
||||||
## Options methods ##
|
## Options methods ##
|
||||||
@ -566,7 +572,8 @@ class Torrent:
|
|||||||
"stop_at_ratio": self.options["stop_at_ratio"],
|
"stop_at_ratio": self.options["stop_at_ratio"],
|
||||||
"remove_at_ratio": self.options["remove_at_ratio"],
|
"remove_at_ratio": self.options["remove_at_ratio"],
|
||||||
"move_on_completed": self.options["move_completed"],
|
"move_on_completed": self.options["move_completed"],
|
||||||
"move_on_completed_path": self.options["move_completed_path"]
|
"move_on_completed_path": self.options["move_completed_path"],
|
||||||
|
"time_added": self.time_added
|
||||||
}
|
}
|
||||||
|
|
||||||
def ti_name():
|
def ti_name():
|
||||||
|
@ -2,19 +2,19 @@
|
|||||||
# torrentmanager.py
|
# torrentmanager.py
|
||||||
#
|
#
|
||||||
# Copyright (C) 2007, 2008 Andrew Resch ('andar') <andrewresch@gmail.com>
|
# Copyright (C) 2007, 2008 Andrew Resch ('andar') <andrewresch@gmail.com>
|
||||||
#
|
#
|
||||||
# Deluge is free software.
|
# Deluge is free software.
|
||||||
#
|
#
|
||||||
# You may redistribute it and/or modify it under the terms of the
|
# You may redistribute it and/or modify it under the terms of the
|
||||||
# GNU General Public License, as published by the Free Software
|
# GNU General Public License, as published by the Free Software
|
||||||
# Foundation; either version 3 of the License, or (at your option)
|
# Foundation; either version 3 of the License, or (at your option)
|
||||||
# any later version.
|
# any later version.
|
||||||
#
|
#
|
||||||
# deluge is distributed in the hope that it will be useful,
|
# deluge is distributed in the hope that it will be useful,
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||||
# See the GNU General Public License for more details.
|
# See the GNU General Public License for more details.
|
||||||
#
|
#
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with deluge. If not, write to:
|
# along with deluge. If not, write to:
|
||||||
# The Free Software Foundation, Inc.,
|
# The Free Software Foundation, Inc.,
|
||||||
@ -53,7 +53,7 @@ from deluge.core.torrent import TorrentOptions
|
|||||||
import deluge.core.oldstateupgrader
|
import deluge.core.oldstateupgrader
|
||||||
|
|
||||||
from deluge.log import LOG as log
|
from deluge.log import LOG as log
|
||||||
|
|
||||||
class TorrentState:
|
class TorrentState:
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
torrent_id=None,
|
torrent_id=None,
|
||||||
@ -61,7 +61,7 @@ class TorrentState:
|
|||||||
total_uploaded=0,
|
total_uploaded=0,
|
||||||
trackers=None,
|
trackers=None,
|
||||||
compact=False,
|
compact=False,
|
||||||
paused=False,
|
paused=False,
|
||||||
save_path=None,
|
save_path=None,
|
||||||
max_connections=-1,
|
max_connections=-1,
|
||||||
max_upload_slots=-1,
|
max_upload_slots=-1,
|
||||||
@ -75,7 +75,8 @@ class TorrentState:
|
|||||||
stop_ratio=2.00,
|
stop_ratio=2.00,
|
||||||
stop_at_ratio=False,
|
stop_at_ratio=False,
|
||||||
remove_at_ratio=False,
|
remove_at_ratio=False,
|
||||||
magnet=None
|
magnet=None,
|
||||||
|
time_added=-1
|
||||||
):
|
):
|
||||||
self.torrent_id = torrent_id
|
self.torrent_id = torrent_id
|
||||||
self.filename = filename
|
self.filename = filename
|
||||||
@ -84,6 +85,7 @@ class TorrentState:
|
|||||||
self.queue = queue
|
self.queue = queue
|
||||||
self.is_finished = is_finished
|
self.is_finished = is_finished
|
||||||
self.magnet = magnet
|
self.magnet = magnet
|
||||||
|
self.time_added = time_added
|
||||||
|
|
||||||
# Options
|
# Options
|
||||||
self.compact = compact
|
self.compact = compact
|
||||||
@ -108,7 +110,7 @@ class TorrentManager(component.Component):
|
|||||||
"""TorrentManager contains a list of torrents in the current libtorrent
|
"""TorrentManager contains a list of torrents in the current libtorrent
|
||||||
session. This object is also responsible for saving the state of the
|
session. This object is also responsible for saving the state of the
|
||||||
session for use on restart."""
|
session for use on restart."""
|
||||||
|
|
||||||
def __init__(self, session, alerts):
|
def __init__(self, session, alerts):
|
||||||
component.Component.__init__(self, "TorrentManager", interval=5000, depend=["PluginManager"])
|
component.Component.__init__(self, "TorrentManager", interval=5000, depend=["PluginManager"])
|
||||||
log.debug("TorrentManager init..")
|
log.debug("TorrentManager init..")
|
||||||
@ -121,12 +123,12 @@ class TorrentManager(component.Component):
|
|||||||
|
|
||||||
# Create the torrents dict { torrent_id: Torrent }
|
# Create the torrents dict { torrent_id: Torrent }
|
||||||
self.torrents = {}
|
self.torrents = {}
|
||||||
|
|
||||||
# This is a list of torrent_id when we shutdown the torrentmanager.
|
# This is a list of torrent_id when we shutdown the torrentmanager.
|
||||||
# We use this list to determine if all active torrents have been paused
|
# We use this list to determine if all active torrents have been paused
|
||||||
# and that their resume data has been written.
|
# and that their resume data has been written.
|
||||||
self.shutdown_torrent_pause_list = []
|
self.shutdown_torrent_pause_list = []
|
||||||
|
|
||||||
# Register set functions
|
# Register set functions
|
||||||
self.config.register_set_function("max_connections_per_torrent",
|
self.config.register_set_function("max_connections_per_torrent",
|
||||||
self.on_set_max_connections_per_torrent)
|
self.on_set_max_connections_per_torrent)
|
||||||
@ -136,9 +138,9 @@ class TorrentManager(component.Component):
|
|||||||
self.on_set_max_upload_speed_per_torrent)
|
self.on_set_max_upload_speed_per_torrent)
|
||||||
self.config.register_set_function("max_download_speed_per_torrent",
|
self.config.register_set_function("max_download_speed_per_torrent",
|
||||||
self.on_set_max_download_speed_per_torrent)
|
self.on_set_max_download_speed_per_torrent)
|
||||||
|
|
||||||
# Register alert functions
|
# Register alert functions
|
||||||
self.alerts.register_handler("torrent_finished_alert",
|
self.alerts.register_handler("torrent_finished_alert",
|
||||||
self.on_alert_torrent_finished)
|
self.on_alert_torrent_finished)
|
||||||
self.alerts.register_handler("torrent_paused_alert",
|
self.alerts.register_handler("torrent_paused_alert",
|
||||||
self.on_alert_torrent_paused)
|
self.on_alert_torrent_paused)
|
||||||
@ -155,7 +157,7 @@ class TorrentManager(component.Component):
|
|||||||
self.on_alert_tracker_error)
|
self.on_alert_tracker_error)
|
||||||
self.alerts.register_handler("storage_moved_alert",
|
self.alerts.register_handler("storage_moved_alert",
|
||||||
self.on_alert_storage_moved)
|
self.on_alert_storage_moved)
|
||||||
self.alerts.register_handler("torrent_resumed_alert",
|
self.alerts.register_handler("torrent_resumed_alert",
|
||||||
self.on_alert_torrent_resumed)
|
self.on_alert_torrent_resumed)
|
||||||
self.alerts.register_handler("state_changed_alert",
|
self.alerts.register_handler("state_changed_alert",
|
||||||
self.on_alert_state_changed)
|
self.on_alert_state_changed)
|
||||||
@ -167,16 +169,16 @@ class TorrentManager(component.Component):
|
|||||||
self.on_alert_file_renamed)
|
self.on_alert_file_renamed)
|
||||||
self.alerts.register_handler("metadata_received_alert",
|
self.alerts.register_handler("metadata_received_alert",
|
||||||
self.on_alert_metadata_received)
|
self.on_alert_metadata_received)
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
# Get the pluginmanager reference
|
# Get the pluginmanager reference
|
||||||
self.plugins = component.get("PluginManager")
|
self.plugins = component.get("PluginManager")
|
||||||
|
|
||||||
self.signals = component.get("SignalManager")
|
self.signals = component.get("SignalManager")
|
||||||
|
|
||||||
# Run the old state upgrader before loading state
|
# Run the old state upgrader before loading state
|
||||||
deluge.core.oldstateupgrader.OldStateUpgrader()
|
deluge.core.oldstateupgrader.OldStateUpgrader()
|
||||||
|
|
||||||
# Try to load the state from file
|
# Try to load the state from file
|
||||||
self.load_state()
|
self.load_state()
|
||||||
|
|
||||||
@ -187,7 +189,7 @@ class TorrentManager(component.Component):
|
|||||||
def stop(self):
|
def stop(self):
|
||||||
# Save state on shutdown
|
# Save state on shutdown
|
||||||
self.save_state()
|
self.save_state()
|
||||||
|
|
||||||
for key in self.torrents.keys():
|
for key in self.torrents.keys():
|
||||||
if not self.torrents[key].handle.is_paused():
|
if not self.torrents[key].handle.is_paused():
|
||||||
# We set auto_managed false to prevent lt from resuming the torrent
|
# We set auto_managed false to prevent lt from resuming the torrent
|
||||||
@ -209,7 +211,7 @@ class TorrentManager(component.Component):
|
|||||||
time.sleep(0.01)
|
time.sleep(0.01)
|
||||||
# Wait for all alerts
|
# Wait for all alerts
|
||||||
self.alerts.handle_alerts(True)
|
self.alerts.handle_alerts(True)
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
for torrent_id, torrent in self.torrents.items():
|
for torrent_id, torrent in self.torrents.items():
|
||||||
if self.config["stop_seed_at_ratio"] or torrent.options["stop_at_ratio"]:
|
if self.config["stop_seed_at_ratio"] or torrent.options["stop_at_ratio"]:
|
||||||
@ -226,11 +228,11 @@ class TorrentManager(component.Component):
|
|||||||
def __getitem__(self, torrent_id):
|
def __getitem__(self, torrent_id):
|
||||||
"""Return the Torrent with torrent_id"""
|
"""Return the Torrent with torrent_id"""
|
||||||
return self.torrents[torrent_id]
|
return self.torrents[torrent_id]
|
||||||
|
|
||||||
def get_torrent_list(self):
|
def get_torrent_list(self):
|
||||||
"""Returns a list of torrent_ids"""
|
"""Returns a list of torrent_ids"""
|
||||||
return self.torrents.keys()
|
return self.torrents.keys()
|
||||||
|
|
||||||
def get_torrent_info_from_file(self, filepath):
|
def get_torrent_info_from_file(self, filepath):
|
||||||
"""Returns a torrent_info for the file specified or None"""
|
"""Returns a torrent_info for the file specified or None"""
|
||||||
torrent_info = None
|
torrent_info = None
|
||||||
@ -242,16 +244,16 @@ class TorrentManager(component.Component):
|
|||||||
_file.close()
|
_file.close()
|
||||||
except (IOError, RuntimeError), e:
|
except (IOError, RuntimeError), e:
|
||||||
log.warning("Unable to open %s: %s", filepath, e)
|
log.warning("Unable to open %s: %s", filepath, e)
|
||||||
|
|
||||||
return torrent_info
|
return torrent_info
|
||||||
|
|
||||||
def get_resume_data_from_file(self, torrent_id):
|
def get_resume_data_from_file(self, torrent_id):
|
||||||
"""Returns an entry with the resume data or None"""
|
"""Returns an entry with the resume data or None"""
|
||||||
fastresume = ""
|
fastresume = ""
|
||||||
try:
|
try:
|
||||||
_file = open(
|
_file = open(
|
||||||
os.path.join(
|
os.path.join(
|
||||||
self.config["state_location"],
|
self.config["state_location"],
|
||||||
torrent_id + ".fastresume"),
|
torrent_id + ".fastresume"),
|
||||||
"rb")
|
"rb")
|
||||||
fastresume = _file.read()
|
fastresume = _file.read()
|
||||||
@ -260,18 +262,18 @@ class TorrentManager(component.Component):
|
|||||||
log.debug("Unable to load .fastresume: %s", e)
|
log.debug("Unable to load .fastresume: %s", e)
|
||||||
|
|
||||||
return str(fastresume)
|
return str(fastresume)
|
||||||
|
|
||||||
def add(self, torrent_info=None, state=None, options=None, save_state=True,
|
def add(self, torrent_info=None, state=None, options=None, save_state=True,
|
||||||
filedump=None, filename=None, magnet=None):
|
filedump=None, filename=None, magnet=None):
|
||||||
"""Add a torrent to the manager and returns it's torrent_id"""
|
"""Add a torrent to the manager and returns it's torrent_id"""
|
||||||
|
|
||||||
if torrent_info is None and state is None and filedump is None and magnet is None:
|
if torrent_info is None and state is None and filedump is None and magnet is None:
|
||||||
log.debug("You must specify a valid torrent_info, torrent state or magnet.")
|
log.debug("You must specify a valid torrent_info, torrent state or magnet.")
|
||||||
return
|
return
|
||||||
|
|
||||||
log.debug("torrentmanager.add")
|
log.debug("torrentmanager.add")
|
||||||
add_torrent_params = {}
|
add_torrent_params = {}
|
||||||
|
|
||||||
if filedump is not None:
|
if filedump is not None:
|
||||||
try:
|
try:
|
||||||
torrent_info = lt.torrent_info(lt.bdecode(filedump))
|
torrent_info = lt.torrent_info(lt.bdecode(filedump))
|
||||||
@ -294,18 +296,18 @@ class TorrentManager(component.Component):
|
|||||||
options["download_location"] = state.save_path
|
options["download_location"] = state.save_path
|
||||||
options["auto_managed"] = state.auto_managed
|
options["auto_managed"] = state.auto_managed
|
||||||
options["add_paused"] = state.paused
|
options["add_paused"] = state.paused
|
||||||
|
|
||||||
if not state.magnet:
|
if not state.magnet:
|
||||||
add_torrent_params["ti"] =\
|
add_torrent_params["ti"] =\
|
||||||
self.get_torrent_info_from_file(
|
self.get_torrent_info_from_file(
|
||||||
os.path.join(self.config["state_location"], state.torrent_id + ".torrent"))
|
os.path.join(self.config["state_location"], state.torrent_id + ".torrent"))
|
||||||
|
|
||||||
if not add_torrent_params["ti"]:
|
if not add_torrent_params["ti"]:
|
||||||
log.error("Unable to add torrent!")
|
log.error("Unable to add torrent!")
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
magnet = state.magnet
|
magnet = state.magnet
|
||||||
|
|
||||||
add_torrent_params["resume_data"] = self.get_resume_data_from_file(state.torrent_id)
|
add_torrent_params["resume_data"] = self.get_resume_data_from_file(state.torrent_id)
|
||||||
else:
|
else:
|
||||||
# We have a torrent_info object so we're not loading from state.
|
# We have a torrent_info object so we're not loading from state.
|
||||||
@ -319,16 +321,16 @@ class TorrentManager(component.Component):
|
|||||||
|
|
||||||
add_torrent_params["ti"] = torrent_info
|
add_torrent_params["ti"] = torrent_info
|
||||||
add_torrent_params["resume_data"] = ""
|
add_torrent_params["resume_data"] = ""
|
||||||
|
|
||||||
#log.info("Adding torrent: %s", filename)
|
#log.info("Adding torrent: %s", filename)
|
||||||
log.debug("options: %s", options)
|
log.debug("options: %s", options)
|
||||||
|
|
||||||
# Set the right storage_mode
|
# Set the right storage_mode
|
||||||
if options["compact_allocation"]:
|
if options["compact_allocation"]:
|
||||||
storage_mode = lt.storage_mode_t(2)
|
storage_mode = lt.storage_mode_t(2)
|
||||||
else:
|
else:
|
||||||
storage_mode = lt.storage_mode_t(1)
|
storage_mode = lt.storage_mode_t(1)
|
||||||
|
|
||||||
# Fill in the rest of the add_torrent_params dictionary
|
# Fill in the rest of the add_torrent_params dictionary
|
||||||
add_torrent_params["save_path"] = options["download_location"].encode("utf8")
|
add_torrent_params["save_path"] = options["download_location"].encode("utf8")
|
||||||
|
|
||||||
@ -336,11 +338,11 @@ class TorrentManager(component.Component):
|
|||||||
add_torrent_params["paused"] = True
|
add_torrent_params["paused"] = True
|
||||||
add_torrent_params["auto_managed"] = False
|
add_torrent_params["auto_managed"] = False
|
||||||
add_torrent_params["duplicate_is_error"] = True
|
add_torrent_params["duplicate_is_error"] = True
|
||||||
|
|
||||||
# We need to pause the AlertManager momentarily to prevent alerts
|
# We need to pause the AlertManager momentarily to prevent alerts
|
||||||
# for this torrent being generated before a Torrent object is created.
|
# for this torrent being generated before a Torrent object is created.
|
||||||
component.pause("AlertManager")
|
component.pause("AlertManager")
|
||||||
|
|
||||||
handle = None
|
handle = None
|
||||||
try:
|
try:
|
||||||
if magnet:
|
if magnet:
|
||||||
@ -349,13 +351,13 @@ class TorrentManager(component.Component):
|
|||||||
handle = self.session.add_torrent(add_torrent_params)
|
handle = self.session.add_torrent(add_torrent_params)
|
||||||
except RuntimeError, e:
|
except RuntimeError, e:
|
||||||
log.warning("Error adding torrent: %s", e)
|
log.warning("Error adding torrent: %s", e)
|
||||||
|
|
||||||
if not handle or not handle.is_valid():
|
if not handle or not handle.is_valid():
|
||||||
log.debug("torrent handle is invalid!")
|
log.debug("torrent handle is invalid!")
|
||||||
# The torrent was not added to the session
|
# The torrent was not added to the session
|
||||||
component.resume("AlertManager")
|
component.resume("AlertManager")
|
||||||
return
|
return
|
||||||
|
|
||||||
log.debug("handle id: %s", str(handle.info_hash()))
|
log.debug("handle id: %s", str(handle.info_hash()))
|
||||||
# Set auto_managed to False because the torrent is paused
|
# Set auto_managed to False because the torrent is paused
|
||||||
handle.auto_managed(False)
|
handle.auto_managed(False)
|
||||||
@ -365,7 +367,7 @@ class TorrentManager(component.Component):
|
|||||||
self.torrents[torrent.torrent_id] = torrent
|
self.torrents[torrent.torrent_id] = torrent
|
||||||
if self.config["queue_new_to_top"]:
|
if self.config["queue_new_to_top"]:
|
||||||
handle.queue_position_top()
|
handle.queue_position_top()
|
||||||
|
|
||||||
component.resume("AlertManager")
|
component.resume("AlertManager")
|
||||||
|
|
||||||
# Resume the torrent if needed
|
# Resume the torrent if needed
|
||||||
@ -375,7 +377,7 @@ class TorrentManager(component.Component):
|
|||||||
# Write the .torrent file to the state directory
|
# Write the .torrent file to the state directory
|
||||||
if filedump:
|
if filedump:
|
||||||
try:
|
try:
|
||||||
save_file = open(os.path.join(self.config["state_location"],
|
save_file = open(os.path.join(self.config["state_location"],
|
||||||
torrent.torrent_id + ".torrent"),
|
torrent.torrent_id + ".torrent"),
|
||||||
"wb")
|
"wb")
|
||||||
save_file.write(filedump)
|
save_file.write(filedump)
|
||||||
@ -398,10 +400,10 @@ class TorrentManager(component.Component):
|
|||||||
if save_state:
|
if save_state:
|
||||||
# Save the session state
|
# Save the session state
|
||||||
self.save_state()
|
self.save_state()
|
||||||
|
|
||||||
# Emit the torrent_added signal
|
# Emit the torrent_added signal
|
||||||
self.signals.emit("torrent_added", torrent.torrent_id)
|
self.signals.emit("torrent_added", torrent.torrent_id)
|
||||||
|
|
||||||
return torrent.torrent_id
|
return torrent.torrent_id
|
||||||
|
|
||||||
def load_torrent(self, torrent_id):
|
def load_torrent(self, torrent_id):
|
||||||
@ -412,16 +414,16 @@ class TorrentManager(component.Component):
|
|||||||
log.debug("Attempting to open %s for add.", torrent_id)
|
log.debug("Attempting to open %s for add.", torrent_id)
|
||||||
_file = open(
|
_file = open(
|
||||||
os.path.join(
|
os.path.join(
|
||||||
self.config["state_location"], torrent_id + ".torrent"),
|
self.config["state_location"], torrent_id + ".torrent"),
|
||||||
"rb")
|
"rb")
|
||||||
filedump = lt.bdecode(_file.read())
|
filedump = lt.bdecode(_file.read())
|
||||||
_file.close()
|
_file.close()
|
||||||
except (IOError, RuntimeError), e:
|
except (IOError, RuntimeError), e:
|
||||||
log.warning("Unable to open %s: %s", torrent_id, e)
|
log.warning("Unable to open %s: %s", torrent_id, e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return filedump
|
return filedump
|
||||||
|
|
||||||
def remove(self, torrent_id, remove_torrent=False, remove_data=False):
|
def remove(self, torrent_id, remove_torrent=False, remove_data=False):
|
||||||
"""Remove a torrent from the manager"""
|
"""Remove a torrent from the manager"""
|
||||||
try:
|
try:
|
||||||
@ -430,17 +432,17 @@ class TorrentManager(component.Component):
|
|||||||
# Remove data if set
|
# Remove data if set
|
||||||
if remove_data:
|
if remove_data:
|
||||||
option = 1
|
option = 1
|
||||||
self.session.remove_torrent(self.torrents[torrent_id].handle,
|
self.session.remove_torrent(self.torrents[torrent_id].handle,
|
||||||
option)
|
option)
|
||||||
except (RuntimeError, KeyError), e:
|
except (RuntimeError, KeyError), e:
|
||||||
log.warning("Error removing torrent: %s", e)
|
log.warning("Error removing torrent: %s", e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Remove the .torrent file if requested
|
# Remove the .torrent file if requested
|
||||||
if remove_torrent:
|
if remove_torrent:
|
||||||
try:
|
try:
|
||||||
torrent_file = os.path.join(
|
torrent_file = os.path.join(
|
||||||
self.config["torrentfiles_location"],
|
self.config["torrentfiles_location"],
|
||||||
self.torrents[torrent_id].filename)
|
self.torrents[torrent_id].filename)
|
||||||
os.remove(torrent_file)
|
os.remove(torrent_file)
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
@ -448,24 +450,24 @@ class TorrentManager(component.Component):
|
|||||||
|
|
||||||
# Remove the .fastresume if it exists
|
# Remove the .fastresume if it exists
|
||||||
self.torrents[torrent_id].delete_fastresume()
|
self.torrents[torrent_id].delete_fastresume()
|
||||||
|
|
||||||
# Remove the .torrent file in the state
|
# Remove the .torrent file in the state
|
||||||
self.torrents[torrent_id].delete_torrentfile()
|
self.torrents[torrent_id].delete_torrentfile()
|
||||||
|
|
||||||
# Remove the torrent from deluge's session
|
# Remove the torrent from deluge's session
|
||||||
try:
|
try:
|
||||||
del self.torrents[torrent_id]
|
del self.torrents[torrent_id]
|
||||||
except KeyError, ValueError:
|
except KeyError, ValueError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Save the session state
|
# Save the session state
|
||||||
self.save_state()
|
self.save_state()
|
||||||
|
|
||||||
# Emit the signal to the clients
|
# Emit the signal to the clients
|
||||||
self.signals.emit("torrent_removed", torrent_id)
|
self.signals.emit("torrent_removed", torrent_id)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def load_state(self):
|
def load_state(self):
|
||||||
"""Load the state of the TorrentManager from the torrents.state file"""
|
"""Load the state of the TorrentManager from the torrents.state file"""
|
||||||
state = TorrentManagerState()
|
state = TorrentManagerState()
|
||||||
@ -478,7 +480,7 @@ class TorrentManager(component.Component):
|
|||||||
state_file.close()
|
state_file.close()
|
||||||
except (EOFError, IOError, Exception), e:
|
except (EOFError, IOError, Exception), e:
|
||||||
log.warning("Unable to load state file: %s", e)
|
log.warning("Unable to load state file: %s", e)
|
||||||
|
|
||||||
# Try to use an old state
|
# Try to use an old state
|
||||||
try:
|
try:
|
||||||
if dir(state.torrents[0]) != dir(TorrentState()):
|
if dir(state.torrents[0]) != dir(TorrentState()):
|
||||||
@ -487,7 +489,7 @@ class TorrentManager(component.Component):
|
|||||||
setattr(s, attr, getattr(TorrentState(), attr, None))
|
setattr(s, attr, getattr(TorrentState(), attr, None))
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
log.warning("Unable to update state file to a compatible version: %s", e)
|
log.warning("Unable to update state file to a compatible version: %s", e)
|
||||||
|
|
||||||
# Reorder the state.torrents list to add torrents in the correct queue
|
# Reorder the state.torrents list to add torrents in the correct queue
|
||||||
# order.
|
# order.
|
||||||
ordered_state = []
|
ordered_state = []
|
||||||
@ -498,14 +500,14 @@ class TorrentManager(component.Component):
|
|||||||
break
|
break
|
||||||
if torrent_state not in ordered_state:
|
if torrent_state not in ordered_state:
|
||||||
ordered_state.append(torrent_state)
|
ordered_state.append(torrent_state)
|
||||||
|
|
||||||
for torrent_state in ordered_state:
|
for torrent_state in ordered_state:
|
||||||
try:
|
try:
|
||||||
self.add(state=torrent_state, save_state=False)
|
self.add(state=torrent_state, save_state=False)
|
||||||
except AttributeError, e:
|
except AttributeError, e:
|
||||||
log.error("Torrent state file is either corrupt or incompatible!")
|
log.error("Torrent state file is either corrupt or incompatible!")
|
||||||
break
|
break
|
||||||
|
|
||||||
# Run the post_session_load plugin hooks
|
# Run the post_session_load plugin hooks
|
||||||
self.plugins.run_post_session_load()
|
self.plugins.run_post_session_load()
|
||||||
|
|
||||||
@ -517,14 +519,14 @@ class TorrentManager(component.Component):
|
|||||||
paused = False
|
paused = False
|
||||||
if torrent.state == "Paused":
|
if torrent.state == "Paused":
|
||||||
paused = True
|
paused = True
|
||||||
|
|
||||||
torrent_state = TorrentState(
|
torrent_state = TorrentState(
|
||||||
torrent.torrent_id,
|
torrent.torrent_id,
|
||||||
torrent.filename,
|
torrent.filename,
|
||||||
torrent.get_status(["total_uploaded"])["total_uploaded"],
|
torrent.get_status(["total_uploaded"])["total_uploaded"],
|
||||||
torrent.trackers,
|
torrent.trackers,
|
||||||
torrent.options["compact_allocation"],
|
torrent.options["compact_allocation"],
|
||||||
paused,
|
paused,
|
||||||
torrent.options["download_location"],
|
torrent.options["download_location"],
|
||||||
torrent.options["max_connections"],
|
torrent.options["max_connections"],
|
||||||
torrent.options["max_upload_slots"],
|
torrent.options["max_upload_slots"],
|
||||||
@ -538,21 +540,22 @@ class TorrentManager(component.Component):
|
|||||||
torrent.options["stop_ratio"],
|
torrent.options["stop_ratio"],
|
||||||
torrent.options["stop_at_ratio"],
|
torrent.options["stop_at_ratio"],
|
||||||
torrent.options["remove_at_ratio"],
|
torrent.options["remove_at_ratio"],
|
||||||
torrent.magnet
|
torrent.magnet,
|
||||||
|
torrent.time_added
|
||||||
)
|
)
|
||||||
state.torrents.append(torrent_state)
|
state.torrents.append(torrent_state)
|
||||||
|
|
||||||
# Pickle the TorrentManagerState object
|
# Pickle the TorrentManagerState object
|
||||||
try:
|
try:
|
||||||
log.debug("Saving torrent state file.")
|
log.debug("Saving torrent state file.")
|
||||||
state_file = open(
|
state_file = open(
|
||||||
os.path.join(self.config["state_location"], "torrents.state"),
|
os.path.join(self.config["state_location"], "torrents.state"),
|
||||||
"wb")
|
"wb")
|
||||||
cPickle.dump(state, state_file)
|
cPickle.dump(state, state_file)
|
||||||
state_file.close()
|
state_file.close()
|
||||||
except IOError:
|
except IOError:
|
||||||
log.warning("Unable to save state file.")
|
log.warning("Unable to save state file.")
|
||||||
|
|
||||||
# We return True so that the timer thread will continue
|
# We return True so that the timer thread will continue
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -560,7 +563,7 @@ class TorrentManager(component.Component):
|
|||||||
"""Saves resume data for all the torrents"""
|
"""Saves resume data for all the torrents"""
|
||||||
for torrent in self.torrents.values():
|
for torrent in self.torrents.values():
|
||||||
torrent.save_resume_data()
|
torrent.save_resume_data()
|
||||||
|
|
||||||
def queue_top(self, torrent_id):
|
def queue_top(self, torrent_id):
|
||||||
"""Queue torrent to top"""
|
"""Queue torrent to top"""
|
||||||
if self.torrents[torrent_id].get_queue_position() == 0:
|
if self.torrents[torrent_id].get_queue_position() == 0:
|
||||||
@ -581,7 +584,7 @@ class TorrentManager(component.Component):
|
|||||||
"""Queue torrent down one position"""
|
"""Queue torrent down one position"""
|
||||||
if self.torrents[torrent_id].get_queue_position() == (len(self.torrents) - 1):
|
if self.torrents[torrent_id].get_queue_position() == (len(self.torrents) - 1):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.torrents[torrent_id].handle.queue_position_down()
|
self.torrents[torrent_id].handle.queue_position_down()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -592,13 +595,13 @@ class TorrentManager(component.Component):
|
|||||||
|
|
||||||
self.torrents[torrent_id].handle.queue_position_bottom()
|
self.torrents[torrent_id].handle.queue_position_bottom()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def on_set_max_connections_per_torrent(self, key, value):
|
def on_set_max_connections_per_torrent(self, key, value):
|
||||||
"""Sets the per-torrent connection limit"""
|
"""Sets the per-torrent connection limit"""
|
||||||
log.debug("max_connections_per_torrent set to %s..", value)
|
log.debug("max_connections_per_torrent set to %s..", value)
|
||||||
for key in self.torrents.keys():
|
for key in self.torrents.keys():
|
||||||
self.torrents[key].set_max_connections(value)
|
self.torrents[key].set_max_connections(value)
|
||||||
|
|
||||||
def on_set_max_upload_slots_per_torrent(self, key, value):
|
def on_set_max_upload_slots_per_torrent(self, key, value):
|
||||||
"""Sets the per-torrent upload slot limit"""
|
"""Sets the per-torrent upload slot limit"""
|
||||||
log.debug("max_upload_slots_per_torrent set to %s..", value)
|
log.debug("max_upload_slots_per_torrent set to %s..", value)
|
||||||
@ -609,7 +612,7 @@ class TorrentManager(component.Component):
|
|||||||
log.debug("max_upload_speed_per_torrent set to %s..", value)
|
log.debug("max_upload_speed_per_torrent set to %s..", value)
|
||||||
for key in self.torrents.keys():
|
for key in self.torrents.keys():
|
||||||
self.torrents[key].set_max_upload_speed(value)
|
self.torrents[key].set_max_upload_speed(value)
|
||||||
|
|
||||||
def on_set_max_download_speed_per_torrent(self, key, value):
|
def on_set_max_download_speed_per_torrent(self, key, value):
|
||||||
log.debug("max_download_speed_per_torrent set to %s..", value)
|
log.debug("max_download_speed_per_torrent set to %s..", value)
|
||||||
for key in self.torrents.keys():
|
for key in self.torrents.keys():
|
||||||
@ -637,7 +640,7 @@ class TorrentManager(component.Component):
|
|||||||
torrent.update_state()
|
torrent.update_state()
|
||||||
torrent.save_resume_data()
|
torrent.save_resume_data()
|
||||||
component.get("SignalManager").emit("torrent_finished", torrent_id)
|
component.get("SignalManager").emit("torrent_finished", torrent_id)
|
||||||
|
|
||||||
def on_alert_torrent_paused(self, alert):
|
def on_alert_torrent_paused(self, alert):
|
||||||
log.debug("on_alert_torrent_paused")
|
log.debug("on_alert_torrent_paused")
|
||||||
# Get the torrent_id
|
# Get the torrent_id
|
||||||
@ -645,20 +648,20 @@ class TorrentManager(component.Component):
|
|||||||
# Set the torrent state
|
# Set the torrent state
|
||||||
self.torrents[torrent_id].update_state()
|
self.torrents[torrent_id].update_state()
|
||||||
component.get("SignalManager").emit("torrent_paused", torrent_id)
|
component.get("SignalManager").emit("torrent_paused", torrent_id)
|
||||||
|
|
||||||
# Write the fastresume file
|
# Write the fastresume file
|
||||||
self.torrents[torrent_id].save_resume_data()
|
self.torrents[torrent_id].save_resume_data()
|
||||||
|
|
||||||
if torrent_id in self.shutdown_torrent_pause_list:
|
if torrent_id in self.shutdown_torrent_pause_list:
|
||||||
self.shutdown_torrent_pause_list.remove(torrent_id)
|
self.shutdown_torrent_pause_list.remove(torrent_id)
|
||||||
|
|
||||||
def on_alert_torrent_checked(self, alert):
|
def on_alert_torrent_checked(self, alert):
|
||||||
log.debug("on_alert_torrent_checked")
|
log.debug("on_alert_torrent_checked")
|
||||||
# Get the torrent_id
|
# Get the torrent_id
|
||||||
torrent_id = str(alert.handle.info_hash())
|
torrent_id = str(alert.handle.info_hash())
|
||||||
# Set the torrent state
|
# Set the torrent state
|
||||||
self.torrents[torrent_id].update_state()
|
self.torrents[torrent_id].update_state()
|
||||||
|
|
||||||
def on_alert_tracker_reply(self, alert):
|
def on_alert_tracker_reply(self, alert):
|
||||||
log.debug("on_alert_tracker_reply: %s", alert.message())
|
log.debug("on_alert_tracker_reply: %s", alert.message())
|
||||||
# Get the torrent_id
|
# Get the torrent_id
|
||||||
@ -669,7 +672,7 @@ class TorrentManager(component.Component):
|
|||||||
self.torrents[torrent_id].set_tracker_status(_("Announce OK"))
|
self.torrents[torrent_id].set_tracker_status(_("Announce OK"))
|
||||||
except KeyError:
|
except KeyError:
|
||||||
log.debug("torrent_id doesn't exist.")
|
log.debug("torrent_id doesn't exist.")
|
||||||
|
|
||||||
# Check to see if we got any peer information from the tracker
|
# Check to see if we got any peer information from the tracker
|
||||||
if alert.handle.status().num_complete == -1 or \
|
if alert.handle.status().num_complete == -1 or \
|
||||||
alert.handle.status().num_incomplete == -1:
|
alert.handle.status().num_incomplete == -1:
|
||||||
@ -684,7 +687,7 @@ class TorrentManager(component.Component):
|
|||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
log.debug("Invalid torrent handle.")
|
log.debug("Invalid torrent handle.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Set the tracker status for the torrent
|
# Set the tracker status for the torrent
|
||||||
try:
|
try:
|
||||||
self.torrents[torrent_id].set_tracker_status(_("Announce Sent"))
|
self.torrents[torrent_id].set_tracker_status(_("Announce Sent"))
|
||||||
@ -702,7 +705,7 @@ class TorrentManager(component.Component):
|
|||||||
self.torrents[torrent_id].set_tracker_status(tracker_status)
|
self.torrents[torrent_id].set_tracker_status(tracker_status)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
log.debug("torrent_id doesn't exist.")
|
log.debug("torrent_id doesn't exist.")
|
||||||
|
|
||||||
def on_alert_tracker_warning(self, alert):
|
def on_alert_tracker_warning(self, alert):
|
||||||
log.debug("on_alert_tracker_warning")
|
log.debug("on_alert_tracker_warning")
|
||||||
# Get the torrent_id
|
# Get the torrent_id
|
||||||
@ -722,7 +725,7 @@ class TorrentManager(component.Component):
|
|||||||
torrent.set_tracker_status(tracker_status)
|
torrent.set_tracker_status(tracker_status)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
log.debug("torrent_id doesn't exist.")
|
log.debug("torrent_id doesn't exist.")
|
||||||
|
|
||||||
def on_alert_storage_moved(self, alert):
|
def on_alert_storage_moved(self, alert):
|
||||||
log.debug("on_alert_storage_moved")
|
log.debug("on_alert_storage_moved")
|
||||||
log.debug("save_path: %s", alert.handle.save_path())
|
log.debug("save_path: %s", alert.handle.save_path())
|
||||||
@ -739,7 +742,7 @@ class TorrentManager(component.Component):
|
|||||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||||
torrent.is_finished = torrent.handle.is_seed()
|
torrent.is_finished = torrent.handle.is_seed()
|
||||||
torrent.update_state()
|
torrent.update_state()
|
||||||
|
|
||||||
def on_alert_state_changed(self, alert):
|
def on_alert_state_changed(self, alert):
|
||||||
log.debug("on_alert_state_changed")
|
log.debug("on_alert_state_changed")
|
||||||
torrent_id = str(alert.handle.info_hash())
|
torrent_id = str(alert.handle.info_hash())
|
||||||
@ -750,7 +753,7 @@ class TorrentManager(component.Component):
|
|||||||
log.debug("on_alert_save_resume_data")
|
log.debug("on_alert_save_resume_data")
|
||||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||||
torrent.write_resume_data(alert.resume_data)
|
torrent.write_resume_data(alert.resume_data)
|
||||||
|
|
||||||
def on_alert_save_resume_data_failed(self, alert):
|
def on_alert_save_resume_data_failed(self, alert):
|
||||||
log.debug("on_alert_save_resume_data_failed: %s", alert.message())
|
log.debug("on_alert_save_resume_data_failed: %s", alert.message())
|
||||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||||
@ -764,7 +767,7 @@ class TorrentManager(component.Component):
|
|||||||
old_folder = os.path.split(torrent.files[alert.index]["path"])[0]
|
old_folder = os.path.split(torrent.files[alert.index]["path"])[0]
|
||||||
new_folder = os.path.split(alert.name)[0]
|
new_folder = os.path.split(alert.name)[0]
|
||||||
torrent.files[alert.index]["path"] = alert.name
|
torrent.files[alert.index]["path"] = alert.name
|
||||||
|
|
||||||
if alert.index in torrent.waiting_on_folder_rename:
|
if alert.index in torrent.waiting_on_folder_rename:
|
||||||
if torrent.waiting_on_folder_rename == [alert.index]:
|
if torrent.waiting_on_folder_rename == [alert.index]:
|
||||||
# This is the last alert we were waiting for, time to send signal
|
# This is the last alert we were waiting for, time to send signal
|
||||||
@ -778,4 +781,3 @@ class TorrentManager(component.Component):
|
|||||||
log.debug("on_alert_metadata_received")
|
log.debug("on_alert_metadata_received")
|
||||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||||
torrent.write_torrentfile()
|
torrent.write_torrentfile()
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -34,6 +34,7 @@
|
|||||||
|
|
||||||
import cPickle
|
import cPickle
|
||||||
import os.path
|
import os.path
|
||||||
|
import time
|
||||||
|
|
||||||
import pygtk
|
import pygtk
|
||||||
pygtk.require('2.0')
|
pygtk.require('2.0')
|
||||||
@ -88,6 +89,14 @@ def cell_data_ratio(column, cell, model, row, data):
|
|||||||
|
|
||||||
cell.set_property('text', ratio_str)
|
cell.set_property('text', ratio_str)
|
||||||
|
|
||||||
|
def cell_data_date(column, cell, model, row, data):
|
||||||
|
"""Display value as date, eg 2008/05/05"""
|
||||||
|
time_val = model.get_value(row, data)
|
||||||
|
time_str = ""
|
||||||
|
if time_val > -1:
|
||||||
|
time_str = time.strftime("%d/%m/%y", time.localtime(time_val))
|
||||||
|
cell.set_property('text', time_str)
|
||||||
|
|
||||||
class ListViewColumnState:
|
class ListViewColumnState:
|
||||||
"""Used for saving/loading column state"""
|
"""Used for saving/loading column state"""
|
||||||
def __init__(self, name, position, width, visible, sort, sort_order):
|
def __init__(self, name, position, width, visible, sort, sort_order):
|
||||||
|
@ -33,6 +33,7 @@
|
|||||||
# statement from all source files in the program, then also delete it here.
|
# statement from all source files in the program, then also delete it here.
|
||||||
|
|
||||||
import gtk, gtk.glade
|
import gtk, gtk.glade
|
||||||
|
import time
|
||||||
|
|
||||||
from deluge.ui.client import aclient as client
|
from deluge.ui.client import aclient as client
|
||||||
import deluge.component as component
|
import deluge.component as component
|
||||||
@ -60,6 +61,11 @@ def fspeed(value, max_value=-1):
|
|||||||
else:
|
else:
|
||||||
return deluge.common.fspeed(value)
|
return deluge.common.fspeed(value)
|
||||||
|
|
||||||
|
def fdate(value):
|
||||||
|
if value < 0:
|
||||||
|
return ""
|
||||||
|
return time.strftime("%d/%m/%y", time.localtime(value))
|
||||||
|
|
||||||
class StatisticsTab(Tab):
|
class StatisticsTab(Tab):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
Tab.__init__(self)
|
Tab.__init__(self)
|
||||||
@ -88,7 +94,8 @@ class StatisticsTab(Tab):
|
|||||||
(glade.get_widget("summary_seed_time"), deluge.common.ftime, ("seeding_time",)),
|
(glade.get_widget("summary_seed_time"), deluge.common.ftime, ("seeding_time",)),
|
||||||
(glade.get_widget("summary_seed_rank"), str, ("seed_rank",)),
|
(glade.get_widget("summary_seed_rank"), str, ("seed_rank",)),
|
||||||
(glade.get_widget("summary_auto_managed"), str, ("is_auto_managed",)),
|
(glade.get_widget("summary_auto_managed"), str, ("is_auto_managed",)),
|
||||||
(glade.get_widget("progressbar"), fpcnt, ("progress",))
|
(glade.get_widget("progressbar"), fpcnt, ("progress",)),
|
||||||
|
(glade.get_widget("summary_date_added"), fdate, ("time_added",))
|
||||||
]
|
]
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
@ -110,7 +117,7 @@ class StatisticsTab(Tab):
|
|||||||
"total_seeds", "eta", "ratio", "next_announce",
|
"total_seeds", "eta", "ratio", "next_announce",
|
||||||
"tracker_status", "max_connections", "max_upload_slots",
|
"tracker_status", "max_connections", "max_upload_slots",
|
||||||
"max_upload_speed", "max_download_speed", "active_time",
|
"max_upload_speed", "max_download_speed", "active_time",
|
||||||
"seeding_time", "seed_rank", "is_auto_managed"]
|
"seeding_time", "seed_rank", "is_auto_managed", "time_added"]
|
||||||
|
|
||||||
client.get_torrent_status(
|
client.get_torrent_status(
|
||||||
self._on_get_torrent_status, selected, status_keys)
|
self._on_get_torrent_status, selected, status_keys)
|
||||||
|
@ -180,6 +180,10 @@ class TorrentView(listview.ListView, component.Component):
|
|||||||
listview.cell_data_ratio,
|
listview.cell_data_ratio,
|
||||||
[float],
|
[float],
|
||||||
status_field=["distributed_copies"])
|
status_field=["distributed_copies"])
|
||||||
|
self.add_func_column(_("Added"),
|
||||||
|
listview.cell_data_date,
|
||||||
|
[float],
|
||||||
|
status_field=["time_added"])
|
||||||
self.add_text_column(_("Tracker"), status_field=["tracker_host"])
|
self.add_text_column(_("Tracker"), status_field=["tracker_host"])
|
||||||
|
|
||||||
# Set filter to None for now
|
# Set filter to None for now
|
||||||
|
Loading…
x
Reference in New Issue
Block a user