downloads.py
1 """This module defines the BitTorrent, File and Download classes. 2 3 They respectively hold structured information about 4 torrent files, files and downloads in aria2c. 5 """ 6 7 from __future__ import annotations 8 9 from contextlib import suppress 10 from datetime import datetime, timedelta, timezone 11 from pathlib import Path 12 from typing import TYPE_CHECKING 13 14 from loguru import logger 15 16 from aria2p.client import ClientException 17 from aria2p.utils import bool_or_value, human_readable_bytes, human_readable_timedelta 18 19 if TYPE_CHECKING: 20 from aria2p.api import API 21 from aria2p.options import Options 22 23 24 class BitTorrent: 25 """Information retrieved from a torrent file.""" 26 27 def __init__(self, struct: dict) -> None: 28 """Initialize the object. 29 30 Parameters: 31 struct: A dictionary Python object returned by the JSON-RPC client. 32 """ 33 self._struct = struct or {} 34 35 def __str__(self): 36 return self.info["name"] 37 38 @property 39 def announce_list(self) -> list[list[str]] | None: 40 """List of lists of announce URIs. 41 42 If the torrent contains announce and no announce-list, announce is converted to the announce-list format. 43 44 Returns: 45 The announce URIs. 46 """ 47 return self._struct.get("announceList") 48 49 @property 50 def comment(self) -> str | None: 51 """Return the comment of the torrent. 52 53 comment.utf-8 is used if available. 54 55 Returns: 56 The torrent's comment. 57 """ 58 return self._struct.get("comment") 59 60 @property 61 def creation_date(self) -> datetime: 62 """Return the creation time of the torrent. 63 64 The value is an integer since the epoch, measured in seconds. 65 66 Returns: 67 The creation date. 68 """ 69 return datetime.fromtimestamp(self._struct["creationDate"], tz=timezone.utc) 70 71 @property 72 def mode(self) -> str | None: 73 """File mode of the torrent. 74 75 The value is either single or multi. 76 77 Returns: 78 The file mode. 79 """ 80 return self._struct.get("mode") 81 82 @property 83 def info(self) -> dict | None: 84 """Struct which contains data from Info dictionary. 85 86 It contains the `name` key: name in info dictionary. `name.utf-8` is used if available. 87 88 Returns: 89 The torrent's info. 90 """ 91 return self._struct.get("info") 92 93 94 class File: 95 """Information about a download's file.""" 96 97 def __init__(self, struct: dict) -> None: 98 """Initialize the object. 99 100 Parameters: 101 struct: A dictionary Python object returned by the JSON-RPC client. 102 """ 103 self._struct = struct or {} 104 105 def __str__(self): 106 return str(self.path) 107 108 def __eq__(self, other: object) -> bool: 109 if isinstance(other, File): 110 return self.path == other.path 111 return NotImplemented 112 113 @property 114 def index(self) -> int: 115 """Index of the file, starting at 1, in the same order as files appear in the multi-file torrent. 116 117 Returns: 118 The index of the file. 119 """ 120 return int(self._struct["index"]) 121 122 @property 123 def path(self) -> Path: 124 """File path. 125 126 Returns: 127 The file path. 128 """ 129 return Path(self._struct["path"]) 130 131 @property 132 def is_metadata(self) -> bool: 133 """Return True if this file is aria2 metadata and not an actual file. 134 135 Returns: 136 If the file is metadata. 137 """ 138 return str(self.path).startswith("[METADATA]") 139 140 @property 141 def length(self) -> int: 142 """Return the file size in bytes. 143 144 Returns: 145 The file size in bytes. 146 """ 147 return int(self._struct["length"]) 148 149 def length_string(self, human_readable: bool = True) -> str: # noqa: FBT001,FBT002 150 """Return the length as string. 151 152 Parameters: 153 human_readable: Return in human readable format or not. 154 155 Returns: 156 The length string. 157 """ 158 if human_readable: 159 return human_readable_bytes(self.length, delim=" ") 160 return str(self.length) + " B" 161 162 @property 163 def completed_length(self) -> int: 164 """Completed length of this file in bytes. 165 166 Please note that it is possible that sum of completedLength is less than the completedLength returned by the 167 aria2.tellStatus() method. This is because completedLength in aria2.getFiles() only includes completed 168 pieces. On the other hand, completedLength in aria2.tellStatus() also includes partially completed pieces. 169 170 Returns: 171 The completed length. 172 """ 173 return int(self._struct["completedLength"]) 174 175 def completed_length_string(self, human_readable: bool = True) -> str: # noqa: FBT001,FBT002 176 """Return the completed length as string. 177 178 Parameters: 179 human_readable: Return in human readable format or not. 180 181 Returns: 182 The completed length string. 183 """ 184 if human_readable: 185 return human_readable_bytes(self.completed_length, delim=" ") 186 return str(self.completed_length) + " B" 187 188 @property 189 def selected(self) -> bool: 190 """Return True if this file is selected by [`--select-file`][aria2p.options.Options.select_file] option. 191 192 If [`--select-file`][aria2p.options.Options.select_file] is not specified 193 or this is single-file torrent or not a torrent download at all, this value is always true. 194 Otherwise false. 195 196 Returns: 197 If this file is selected. 198 """ 199 return bool_or_value(self._struct["selected"]) 200 201 @property 202 def uris(self) -> list[dict]: 203 """Return a list of URIs for this file. 204 205 The element type is the same struct 206 used in the [`client.get_uris()`][aria2p.client.Client.get_uris] method. 207 208 Returns: 209 The list of URIs. 210 """ 211 return self._struct.get("uris", []) 212 213 214 class Download: 215 """Class containing all information about a download, as retrieved with the client.""" 216 217 def __init__(self, api: API, struct: dict) -> None: 218 """Initialize the object. 219 220 Parameters: 221 api: The reference to an [`API`][aria2p.api.API] instance. 222 struct: A dictionary Python object returned by the JSON-RPC client. 223 """ 224 self.api = api 225 self._struct = struct or {} 226 self._files: list[File] = [] 227 self._root_files_paths: list[Path] = [] 228 self._bittorrent: BitTorrent | None = None 229 self._name = "" 230 self._options: Options | None = None 231 self._followed_by: list[Download] | None = None 232 self._following: Download | None = None 233 self._belongs_to: Download | None = None 234 235 def __str__(self): 236 return self.name 237 238 def __eq__(self, other: object) -> bool: 239 if isinstance(other, Download): 240 return self.gid == other.gid 241 return NotImplemented 242 243 def update(self) -> None: 244 """Update the internal values of the download with more recent values.""" 245 self._struct = self.api.client.tell_status(self.gid) 246 247 self._files = [] 248 self._name = "" 249 self._bittorrent = None 250 self._followed_by = None 251 self._following = None 252 self._belongs_to = None 253 self._options = None 254 255 @property 256 def live(self) -> Download: 257 """Return the same object with updated data. 258 259 Returns: 260 Itself. 261 """ 262 self.update() 263 return self 264 265 @property 266 def name(self) -> str: 267 """Return the name of the download. 268 269 Name is the name of the file if single-file, first file's directory name if multi-file. 270 271 Returns: 272 The download name. 273 """ 274 if not self._name: 275 if self.bittorrent and self.bittorrent.info: 276 self._name = self.bittorrent.info["name"] 277 elif self.files[0].is_metadata: 278 self._name = str(self.files[0].path) 279 else: 280 file_path = str(self.files[0].path.absolute()) 281 dir_path = str(self.dir.absolute()) 282 if file_path.startswith(dir_path): 283 start_pos = len(dir_path) + 1 284 with suppress(IndexError): 285 self._name = Path(file_path[start_pos:]).parts[0] 286 else: 287 with suppress(IndexError): 288 self._name = self.files[0].uris[0]["uri"].split("/")[-1] 289 return self._name 290 291 @property 292 def control_file_path(self) -> Path: 293 """Return the path to the aria2 control file for this download. 294 295 Returns: 296 The control file path. 297 """ 298 return self.dir / (self.name + ".aria2") 299 300 @property 301 def root_files_paths(self) -> list[Path]: 302 """Return the unique set of directories/files for this download. 303 304 Instead of returning all the leaves like self.files, 305 return the relative root directories if any, and relative root files. 306 307 This property is useful when we need to list the directories and files 308 in order to move or copy them. We don't want to copy files one by one, 309 but rather entire directories at once when possible. 310 311 Returns: 312 The root file paths. 313 314 Examples: 315 Download directory is `/a/b`. 316 317 >>> self.files 318 ["/a/b/c/1.txt", "/a/b/c/2.txt", "/a/b/3.txt"] 319 >>> self.root_files_paths 320 ["/a/b/c", "/a/b/3.txt"] 321 """ 322 if not self._root_files_paths: 323 paths = [] 324 for file in self.files: 325 if file.is_metadata: 326 continue 327 try: 328 relative_path = file.path.relative_to(self.dir) 329 except ValueError as error: 330 logger.warning(f"Can't determine file path '{file.path}' relative to '{self.dir}'") 331 logger.opt(exception=True).trace(error) 332 else: 333 path = self.dir / relative_path.parts[0] 334 if path not in paths: 335 paths.append(path) 336 self._root_files_paths = paths 337 return self._root_files_paths 338 339 @property 340 def options(self) -> Options: 341 """Options specific to this download. 342 343 Returns: 344 The download options. 345 """ 346 if not self._options: 347 self.update_options() 348 return self._options # type: ignore 349 350 @options.setter 351 def options(self, value: Options) -> None: 352 self._options = value 353 354 def update_options(self) -> None: 355 """Re-fetch the options from the remote.""" 356 self._options = self.api.get_options(downloads=[self])[0] 357 358 @property 359 def gid(self) -> str: 360 """GID of the download. 361 362 Returns: 363 The download GID. 364 """ 365 return self._struct["gid"] 366 367 @property 368 def status(self) -> str: 369 """Return the status of the download. 370 371 Returns: 372 `active`, `waiting`, `paused`, `error`, `complete` or `removed`. 373 """ 374 return self._struct["status"] 375 376 @property 377 def is_active(self) -> bool: 378 """Return True if download is active. 379 380 Returns: 381 If this download is active. 382 """ 383 return self.status == "active" 384 385 @property 386 def is_waiting(self) -> bool: 387 """Return True if download is waiting. 388 389 Returns: 390 If this download is waiting. 391 """ 392 return self.status == "waiting" 393 394 @property 395 def is_paused(self) -> bool: 396 """Return True if download is paused. 397 398 Returns: 399 If this download is paused. 400 """ 401 return self.status == "paused" 402 403 @property 404 def has_failed(self) -> bool: 405 """Return True if download has errored. 406 407 Returns: 408 If this download has failed. 409 """ 410 return self.status == "error" 411 412 @property 413 def is_complete(self) -> bool: 414 """Return True if download is complete. 415 416 Returns: 417 If this download is complete. 418 """ 419 return self.status == "complete" 420 421 @property 422 def is_removed(self) -> bool: 423 """Return True if download was removed. 424 425 Returns: 426 If this download was removed. 427 """ 428 return self.status == "removed" 429 430 @property 431 def is_metadata(self) -> bool: 432 """Return True if this download is only composed of metadata, and no actual files. 433 434 Returns: 435 If this is a metadata download. 436 """ 437 return all(_.is_metadata for _ in self.files) 438 439 @property 440 def is_torrent(self) -> bool: 441 """Return true if this download is a torrent. 442 443 Returns: 444 If this is a torrent downlaod. 445 """ 446 return "bittorrent" in self._struct 447 448 @property 449 def total_length(self) -> int: 450 """Total length of the download in bytes. 451 452 Returns: 453 The total length in bytes. 454 """ 455 return int(self._struct["totalLength"]) 456 457 def total_length_string(self, human_readable: bool = True) -> str: # noqa: FBT001,FBT002 458 """Return the total length as string. 459 460 Parameters: 461 human_readable: Return in human readable format or not. 462 463 Returns: 464 The total length string. 465 """ 466 if human_readable: 467 return human_readable_bytes(self.total_length, delim=" ") 468 return str(self.total_length) + " B" 469 470 @property 471 def completed_length(self) -> int: 472 """Completed length of the download in bytes. 473 474 Returns: 475 The completed length in bytes. 476 """ 477 return int(self._struct["completedLength"]) 478 479 def completed_length_string(self, human_readable: bool = True) -> str: # noqa: FBT001,FBT002 480 """Return the completed length as string. 481 482 Parameters: 483 human_readable: Return in human readable format or not. 484 485 Returns: 486 The completed length string. 487 """ 488 if human_readable: 489 return human_readable_bytes(self.completed_length, delim=" ") 490 return str(self.completed_length) + " B" 491 492 @property 493 def upload_length(self) -> int: 494 """Return the uploaded length of the download in bytes. 495 496 Returns: 497 The uploaded length in bytes. 498 """ 499 return int(self._struct["uploadLength"]) 500 501 def upload_length_string(self, human_readable: bool = True) -> str: # noqa: FBT001,FBT002 502 """Return the upload length as string. 503 504 Parameters: 505 human_readable: Return in human readable format or not. 506 507 Returns: 508 The upload length string. 509 """ 510 if human_readable: 511 return human_readable_bytes(self.upload_length, delim=" ") 512 return str(self.upload_length) + " B" 513 514 @property 515 def bitfield(self) -> str | None: 516 """Hexadecimal representation of the download progress. 517 518 The highest bit corresponds to the piece at index 0. Any set bits indicate loaded pieces, while unset bits 519 indicate not yet loaded and/or missing pieces. Any overflow bits at the end are set to zero. When the 520 download was not started yet, this key will not be included in the response. 521 522 Returns: 523 The hexadecimal representation of the download progress. 524 """ 525 return self._struct.get("bitfield") 526 527 @property 528 def download_speed(self) -> int: 529 """Download speed of this download measured in bytes/sec. 530 531 Returns: 532 The download speed in bytes/sec. 533 """ 534 return int(self._struct["downloadSpeed"]) 535 536 def download_speed_string(self, human_readable: bool = True) -> str: # noqa: FBT001,FBT002 537 """Return the download speed as string. 538 539 Parameters: 540 human_readable: Return in human readable format or not. 541 542 Returns: 543 The download speed string. 544 """ 545 if human_readable: 546 return human_readable_bytes(self.download_speed, delim=" ", postfix="/s") 547 return str(self.download_speed) + " B/s" 548 549 @property 550 def upload_speed(self) -> int: 551 """Upload speed of this download measured in bytes/sec. 552 553 Returns: 554 The upload speed in bytes/sec. 555 """ 556 return int(self._struct["uploadSpeed"]) 557 558 def upload_speed_string(self, human_readable: bool = True) -> str: # noqa: FBT001,FBT002 559 """Return the upload speed as string. 560 561 Parameters: 562 human_readable: Return in human readable format or not. 563 564 Returns: 565 The upload speed string. 566 """ 567 if human_readable: 568 return human_readable_bytes(self.upload_speed, delim=" ", postfix="/s") 569 return str(self.upload_speed) + " B/s" 570 571 @property 572 def info_hash(self) -> str | None: 573 """Return the InfoHash. 574 575 BitTorrent only. 576 577 Returns: 578 The InfoHash. 579 """ 580 return self._struct.get("infoHash") 581 582 @property 583 def num_seeders(self) -> int: 584 """Return the number of seeders aria2 has connected to. 585 586 BitTorrent only. 587 588 Returns: 589 The numbers of seeders. 590 """ 591 return int(self._struct["numSeeders"]) 592 593 @property 594 def seeder(self) -> bool: 595 """Return True if the local endpoint is a seeder, otherwise false. 596 597 BitTorrent only. 598 599 Returns: 600 If the local endpoint is a seeder. 601 """ 602 return bool_or_value(self._struct.get("seeder")) 603 604 @property 605 def piece_length(self) -> int: 606 """Piece length in bytes. 607 608 Returns: 609 The piece length in bytes. 610 """ 611 return int(self._struct["pieceLength"]) 612 613 def piece_length_string(self, human_readable: bool = True) -> str: # noqa: FBT001,FBT002 614 """Return the piece length as string. 615 616 Parameters: 617 human_readable: Return in human readable format or not. 618 619 Returns: 620 The piece length string. 621 """ 622 if human_readable: 623 return human_readable_bytes(self.piece_length, delim=" ") 624 return str(self.piece_length) + " B" 625 626 @property 627 def num_pieces(self) -> int: 628 """Return the number of pieces. 629 630 Returns: 631 The number of pieces. 632 """ 633 return int(self._struct["numPieces"]) 634 635 @property 636 def connections(self) -> int: 637 """Return the number of peers/servers aria2 has connected to. 638 639 Returns: 640 The number of connected peers/servers. 641 """ 642 return int(self._struct["connections"]) 643 644 @property 645 def error_code(self) -> str | None: 646 """Return the code of the last error for this item, if any. 647 648 The value is a string. The error codes are defined in the EXIT STATUS section. This value is only available 649 for stopped/completed downloads. 650 651 Returns: 652 The error code. 653 """ 654 return self._struct.get("errorCode") 655 656 @property 657 def error_message(self) -> str | None: 658 """Return the (hopefully) human readable error message associated to errorCode. 659 660 Returns: 661 The error message. 662 """ 663 return self._struct.get("errorMessage") 664 665 @property 666 def followed_by_ids(self) -> list[str]: 667 """List of GIDs which are generated as the result of this download. 668 669 For example, when aria2 downloads a Metalink file, it generates downloads described in the Metalink (see the 670 --follow-metalink option). This value is useful to track auto-generated downloads. If there are no such 671 downloads, this key will not be included in the response. 672 673 Returns: 674 The children downloads IDs. 675 """ 676 return self._struct.get("followedBy", []) 677 678 @property 679 def followed_by(self) -> list[Download]: 680 """List of downloads generated as the result of this download. 681 682 Returns: 683 A list of instances of [`Download`][aria2p.downloads.Download]. 684 """ 685 if self._followed_by is None: 686 result = [] 687 for gid in self.followed_by_ids: 688 try: 689 result.append(self.api.get_download(gid)) 690 except ClientException as error: 691 logger.warning( 692 f"Can't find download with GID {gid}, try to update download {self.gid} ({id(self)}", 693 ) 694 logger.opt(exception=True).trace(error) 695 self._followed_by = result 696 return self._followed_by 697 698 @property 699 def following_id(self) -> str | None: 700 """Return the reverse link for followedBy. 701 702 A download included in followedBy has this object's GID in its following value. 703 704 Returns: 705 The parent download ID. 706 """ 707 return self._struct.get("following") 708 709 @property 710 def following(self) -> Download | None: 711 """Return the download this download is following. 712 713 Returns: 714 An instance of [`Download`][aria2p.downloads.Download]. 715 """ 716 if not self._following: 717 following_id = self.following_id 718 if following_id: 719 try: 720 self._following = self.api.get_download(following_id) 721 except ClientException as error: 722 logger.warning( 723 f"Can't find download with GID {following_id}, try to update download {self.gid} ({id(self)}", 724 ) 725 logger.opt(exception=True).trace(error) 726 self._following = None 727 return self._following 728 729 @property 730 def belongs_to_id(self) -> str | None: 731 """GID of a parent download. 732 733 Some downloads are a part of another download. For example, if a file in a Metalink has BitTorrent resources, 734 The downloads of ".torrent" files are parts of that parent. If this download has no parent, this key will not 735 be included in the response. 736 737 Returns: 738 The GID of the parent download. 739 """ 740 return self._struct.get("belongsTo") 741 742 @property 743 def belongs_to(self) -> Download | None: 744 """Parent download. 745 746 Returns: 747 An instance of [`Download`][aria2p.downloads.Download]. 748 """ 749 if not self._belongs_to: 750 belongs_to_id = self.belongs_to_id 751 if belongs_to_id: 752 try: 753 self._belongs_to = self.api.get_download(belongs_to_id) 754 except ClientException as error: 755 logger.warning( 756 f"Can't find download with GID {belongs_to_id}, try to update download {self.gid} ({id(self)})", 757 ) 758 logger.opt(exception=True).trace(error) 759 self._belongs_to = None 760 return self._belongs_to 761 762 @property 763 def dir(self) -> Path: 764 """Directory to save files. 765 766 Returns: 767 The directory where the files are saved. 768 """ 769 return Path(self._struct["dir"]) 770 771 @property 772 def files(self) -> list[File]: 773 """Return the list of files. 774 775 The elements of this list are the same structs used in aria2.getFiles() method. 776 777 Returns: 778 The files of this download. 779 """ 780 if not self._files: 781 self._files = [File(struct) for struct in self._struct.get("files", [])] 782 return self._files 783 784 @property 785 def bittorrent(self) -> BitTorrent | None: 786 """Struct which contains information retrieved from the .torrent (file). 787 788 BitTorrent only. 789 790 Returns: 791 A [BitTorrent][aria2p.downloads.BitTorrent] instance or `None`. 792 """ 793 if not self._bittorrent and "bittorrent" in self._struct: 794 self._bittorrent = BitTorrent(self._struct.get("bittorrent", {})) 795 return self._bittorrent 796 797 @property 798 def verified_length(self) -> int: 799 """Return the number of verified number of bytes while the files are being hash checked. 800 801 This key exists only when this download is being hash checked. 802 803 Returns: 804 The verified length. 805 """ 806 return int(self._struct.get("verifiedLength", 0)) 807 808 def verified_length_string(self, human_readable: bool = True) -> str: # noqa: FBT001,FBT002 809 """Return the verified length as string. 810 811 Parameters: 812 human_readable: Return in human readable format or not. 813 814 Returns: 815 The verified length string. 816 """ 817 if human_readable: 818 return human_readable_bytes(self.verified_length, delim=" ") 819 return str(self.verified_length) + " B" 820 821 @property 822 def verify_integrity_pending(self) -> bool | None: 823 """Return True if this download is waiting for the hash check in a queue. 824 825 This key exists only when this download is in the queue. 826 827 Returns: 828 Whether this download is waiting for the hash check. 829 """ 830 return bool_or_value(self._struct.get("verifyIntegrityPending")) 831 832 @property 833 def progress(self) -> float: 834 """Return the progress of the download as float. 835 836 Returns: 837 Progress percentage. 838 """ 839 try: 840 return self.completed_length / self.total_length * 100 841 except ZeroDivisionError: 842 return 0.0 843 844 def progress_string(self, digits: int = 2) -> str: 845 """Return the progress percentage as string. 846 847 Parameters: 848 digits: Number of decimal digits to use. 849 850 Returns: 851 The progress percentage. 852 """ 853 return f"{self.progress:.{digits}f}%" 854 855 @property 856 def eta(self) -> timedelta: 857 """Return the Estimated Time of Arrival (a timedelta). 858 859 Returns: 860 ETA or `timedelta.max` if unknown. 861 """ 862 try: 863 return timedelta(seconds=int((self.total_length - self.completed_length) / self.download_speed)) 864 except ZeroDivisionError: 865 return timedelta.max 866 867 def eta_string(self, precision: int = 0) -> str: 868 """Return the Estimated Time of Arrival as a string. 869 870 Parameters: 871 precision: The precision to use, see [aria2p.utils.human_readable_timedelta]. 872 873 Returns: 874 The Estimated Time of Arrival as a string. 875 """ 876 eta = self.eta 877 878 if eta == timedelta.max: 879 return "-" 880 881 return human_readable_timedelta(eta, precision=precision) 882 883 def move(self, pos: int) -> int: 884 """Move the download in the queue, relatively. 885 886 Parameters: 887 pos: Number of times to move. 888 889 Returns: 890 The new position of the download. 891 """ 892 return self.api.move(self, pos) 893 894 def move_to(self, pos: int) -> int: 895 """Move the download in the queue, absolutely. 896 897 Parameters: 898 pos: The absolute position in the queue to take. 899 900 Returns: 901 The new position of the download. 902 """ 903 return self.api.move_to(self, pos) 904 905 def move_up(self, pos: int = 1) -> int: 906 """Move the download up in the queue. 907 908 Parameters: 909 pos: Number of times to move up. 910 911 Returns: 912 The new position of the download. 913 """ 914 return self.api.move_up(self, pos) 915 916 def move_down(self, pos: int = 1) -> int: 917 """Move the download down in the queue. 918 919 Parameters: 920 pos: Number of times to move down. 921 922 Returns: 923 The new position of the download. 924 """ 925 return self.api.move_down(self, pos) 926 927 def move_to_top(self) -> int: 928 """Move the download to the top of the queue. 929 930 Returns: 931 The new position of the download. 932 """ 933 return self.api.move_to_top(self) 934 935 def move_to_bottom(self) -> int: 936 """Move the download to the bottom of the queue. 937 938 Returns: 939 The new position of the download. 940 """ 941 return self.api.move_to_bottom(self) 942 943 def remove(self, force: bool = False, files: bool = False) -> bool: # noqa: FBT001,FBT002 944 """Remove the download from the queue (even if active). 945 946 Parameters: 947 force: Whether to force removal. 948 files: Whether to remove files as well. 949 950 Returns: 951 Always True (raises exception otherwise). 952 953 Raises: 954 ClientException: When removal failed. 955 """ 956 result = self.api.remove([self], force=force, files=files)[0] 957 if not result: 958 raise result # type: ignore # we know it's a ClientException 959 return True 960 961 def pause(self, force: bool = False) -> bool: # noqa: FBT001,FBT002 962 """Pause the download. 963 964 Parameters: 965 force: Whether to force pause (don't contact servers). 966 967 Returns: 968 Always True (raises exception otherwise). 969 970 Raises: 971 ClientException: When pausing failed. 972 """ 973 result = self.api.pause([self], force=force)[0] 974 if not result: 975 raise result # type: ignore # we know it's a ClientException 976 return True 977 978 def resume(self) -> bool: 979 """Resume the download. 980 981 Returns: 982 Always True (raises exception otherwise). 983 984 Raises: 985 ClientException: When resuming failed. 986 """ 987 result = self.api.resume([self])[0] 988 if not result: 989 raise result # type: ignore # we know it's a ClientException 990 return True 991 992 def purge(self) -> bool: 993 """Purge itself from the results. 994 995 Returns: 996 Success or failure of the operation. 997 """ 998 return self.api.client.remove_download_result(self.gid) == "OK" 999 1000 def move_files(self, to_directory: str | Path, force: bool = False) -> bool: # noqa: FBT001,FBT002 1001 """Move downloaded files to another directory. 1002 1003 Parameters: 1004 to_directory: The target directory to move files to. 1005 force: Whether to move files even if download is not complete. 1006 1007 Returns: 1008 Success or failure of the operation. 1009 """ 1010 return self.api.move_files([self], to_directory, force)[0] 1011 1012 def copy_files(self, to_directory: str | Path, force: bool = False) -> bool: # noqa: FBT001,FBT002 1013 """Copy downloaded files to another directory. 1014 1015 Parameters: 1016 to_directory: The target directory to copy files into. 1017 force: Whether to move files even if download is not complete. 1018 1019 Returns: 1020 Success or failure of the operation. 1021 """ 1022 return self.api.copy_files([self], to_directory, force)[0]