def main(argv):
    # Validate the configuration.
    for abbr, replacement in TRACKER_ABBR.items():
        if not isinstance(abbr, str):
            print("Configuration error: invalid tracker abbrevation: '%s' "
                  "(must be a string instead)" % abbr,
                  file=sys.stderr)
            return 1
        if not isinstance(replacement, (str, list)):
            print("Configuration error: invalid tracker abbreviation: '%s' "
                  "(must be a string or list of strings instead)" % str(replacement),
                  file=sys.stderr)
            return 1

    # Create OptionParser.
    kwargs =  {
              'usage':
              "%prog [options] <file-or-directory> <main-tracker-url> "
              "[<backup-tracker-url> ...]",

              'version':
              "%%prog v%s" % VERSION,

              'description':
              "py3createtorrent is a comprehensive command line utility for "
              "creating torrents."
              }

    parser = optparse.OptionParser(**kwargs)

    # Add options to the OptionParser.
    # Note: Commonly used options are added first.
    parser.add_option("-p", "--piece-length", type="int", action="store",
                      dest="piece_length", default=0,
                      help="piece size in KiB. 0 = automatic selection (default).")

    parser.add_option("-P", "--private", action="store_true",
                      dest="private", default=False,
                      help="create private torrent")

    parser.add_option("-c", "--comment", type="string", action="store",
                      dest="comment", default=False,
                      help="include comment")

    parser.add_option("-f", "--force", action="store_true",
                      dest="force", default=False,
                      help="dont ask anything, just do it")

    parser.add_option("-v", "--verbose", action="store_true",
                      dest="verbose", default=False,
                      help="verbose mode")

    parser.add_option("-q", "--quiet", action="store_true",
                      dest="quiet", default=False,
                      help="be quiet, e.g. don't print summary")

    parser.add_option("-o", "--output", type="string", action="store",
                      dest="output", default=None, metavar="PATH",
                      help="custom output location (directory or complete "
                           "path). default = current directory.")

    parser.add_option("-e", "--exclude", type="string", action="append",
                      dest="exclude", default=[], metavar="PATH",
                      help="exclude path (can be repeated)")

    parser.add_option("--exclude-pattern", type="string", action="append",
                      dest="exclude_pattern", default=[], metavar="REGEXP",
                      help="exclude paths matching the regular expression "
                           "(can be repeated)")
                           
    parser.add_option("--exclude-pattern-ci", type="string", action="append",
                      dest="exclude_pattern_ci", default=[], metavar="REGEXP",
                      help="exclude paths matching the case-insensitive regular "
                           "expression (can be repeated)")

    parser.add_option("-d", "--date", type="int", action="store",
                      dest="date", default=-1, metavar="TIMESTAMP",
                      help="set creation date (unix timestamp). -1 = now "
                           "(default). -2 = disable.")

    parser.add_option("-n", "--name", type="string", action="store",
                      dest="name", default=None,
                      help="use this file (or directory) name instead of the "
                           "real one")

    parser.add_option("--md5", action="store_true",
                      dest="include_md5", default=False,
                      help="include MD5 hashes in torrent file")

    (options, args) = parser.parse_args(args = argv[1:])

    # Positional arguments must have been provided:
    # -> file / directory plus at least one tracker.
    if len(args) < 2:
        parser.error("You must specify a valid path and at least one tracker.")

    # Ask the user if he really wants to use uncommon piece lengths.
    # (Unless the force option has been set.)
    if not options.force and 0 < options.piece_length < 16:
        if "yes" != input("It is strongly recommended to use a piece length "
                          "greater or equal than 16 KiB! Do you really want "
                          "to continue? yes/no: "):
            parser.error("Aborted.")

    if not options.force and options.piece_length > 1024:
        if "yes" != input("It is strongly recommended to use a maximum piece "
                          "length of 1024 KiB! Do you really want to "
                          "continue? yes/no: "):
            parser.error("Aborted.")

    # Verbose and quiet options may not be used together.
    if options.verbose and options.quiet:
        parser.error("Being verbose and quiet exclude each other.")

    global VERBOSE
    VERBOSE = options.verbose

    # ##########################################
    # CALCULATE/SET THE FOLLOWING METAINFO DATA:
    # - info
    #   - pieces (concatenated 20 byte sha1 hashes of all the data)
    #   - files (if multiple files)
    #   - length and md5sum (if single file)
    #   - name (may be overwritten in the next section by the --name option)

    node     = os.path.abspath(args[0])
    trackers = args[1:]

    # Validate the given path.
    if not os.path.isfile(node) and not os.path.isdir(node):
        parser.error("'%s' neither is a file nor a directory." % node)

    # Evaluate / apply the tracker abbreviations.
    trackers = replace_in_list(trackers, TRACKER_ABBR)

    # Remove duplicate trackers.
    trackers = remove_duplicates(trackers)

    # Validate tracker URLs.
    invalid_trackers = False
    regexp = re.compile(r"^(http|https|udp)://", re.I)
    for t in trackers:
        if not regexp.search(t):
            print("Warning: Not a valid tracker URL: %s" % t, file=sys.stderr)
            invalid_trackers = True

    if invalid_trackers and not options.force:
        if "yes" != input("Some tracker URLs are invalid. Continue? yes/no: "):
            parser.error("Aborted.")

    # Parse and validate excluded paths.
    excluded_paths = frozenset([os.path.normcase(os.path.abspath(path)) \
                                for path in options.exclude])

    # Parse exclude patterns.
    excluded_regexps = set(re.compile(regexp) for regexp in options.exclude_pattern)
    excluded_regexps |= set(re.compile(regexp, re.IGNORECASE)
                            for regexp in options.exclude_pattern_ci)

    # Warn the user if he attempts to exclude any paths when creating
    # a torrent for a single file (makes no sense).
    if os.path.isfile(node) and (len(excluded_paths) > 0 or \
       len(excluded_regexps) > 0):
        print("Warning: Excluding paths is not possible when creating a "
              "torrent for a single file.", file=sys.stderr)

    # Warn the user if he attempts to exclude a specific path, that does not
    # even exist.
    for path in excluded_paths:
        if not os.path.exists(path):
            print("Warning: You're excluding a path that does not exist: '%s'"
                  % path, file=sys.stderr)

    # Get the torrent's files and / or calculate its size.
    if os.path.isfile(node):
        torrent_size = os.path.getsize(node)
    else:
        torrent_files = get_files_in_directory(node,
                                         excluded_paths=excluded_paths,
                                         excluded_regexps=excluded_regexps)
        torrent_size = sum([os.path.getsize(os.path.join(node, file))
                            for file in torrent_files])

    # Torrents for 0 byte data can't be created.
    if torrent_size == 0:
        print("Error: Can't create torrent for 0 byte data.", file=sys.stderr)
        print("Check your files and exclusions!", file=sys.stderr)
        return 1

    # Calculate or parse the piece size.
    if options.piece_length == 0:
        piece_length = calculate_piece_length(torrent_size)
    elif options.piece_length > 0:
        piece_length = options.piece_length * KIB
    else:
        parser.error("Invalid piece size: '%d'" % options.piece_length)

    # Do the main work now.
    # -> prepare the metainfo dictionary.
    if os.path.isfile(node):
        info = create_single_file_info(node, piece_length, options.include_md5)
    else:
        info = create_multi_file_info(node, torrent_files, piece_length, options.include_md5)

    assert len(info['pieces']) % 20 == 0, "len(pieces) not a multiple of 20"

    # ###########################
    # FINISH METAINFO DICTIONARY:
    # - info
    #   - piece length
    #   - name (eventually overwrite)
    #   - private
    # - announce
    # - announce-list (if multiple trackers)
    # - creation date (may be disabled as well)
    # - created by
    # - comment (may be disabled as well (if ADVERTISE = False))

    # Finish sub-dict "info".
    info['piece length'] = piece_length

    if options.private:
        info['private'] = 1

    # Construct outer metainfo dict, which contains the torrent's whole
    # information.
    metainfo =  {
                'info':           info,
                'announce':       trackers[0],
                }

    # Make "announce-list" field, if there are multiple trackers.
    if len(trackers) > 1:
        metainfo['announce-list'] = [[tracker] for tracker in trackers]

    # Set "creation date".
    # The user may specify a custom creation date. He may also decide not
    # to include the creation date field at all.
    if   options.date == -1:
        # use current time
        metainfo['creation date'] = int(time.time())
    elif options.date >= 0:
        # use specified timestamp directly
        metainfo['creation date'] = options.date

    # Add the "created by" field.
    metainfo['created by'] = 'py3createtorrent v%s' % VERSION

    # Add user's comment or advertise py3createtorrent (unless this behaviour
    # has been disabled by the user).
    # The user may also decide not to include the comment field at all
    # by specifying an empty comment.
    if isinstance(options.comment, str):
        if len(options.comment) > 0:
            metainfo['comment'] = options.comment
    elif ADVERTISE:
        metainfo['comment'] = "created with " + metainfo['created by']

    # Add the name field.
    # By default this is the name of directory or file the torrent
    # is being created for.
    if options.name:
        options.name = options.name.strip()

        regexp = re.compile("^[A-Z0-9_\-\., ]+$", re.I)

        if not regexp.match(options.name):
            parser.error("Invalid name: '%s'. Allowed chars: A_Z, a-z, 0-9, "
                         "any of {.,_-} plus spaces." % options.name)

        metainfo['info']['name'] = options.name

    # ###################################################
    # BENCODE METAINFO DICTIONARY AND WRITE TORRENT FILE:
    # - take into consideration the --output option
    # - properly handle KeyboardInterrups while writing the file

    # Respect the custom output location.
    if not options.output:
        # Use current directory.
        output_path = metainfo['info']['name'] + ".torrent"

    else:
        # Use the directory or filename specified by the user.
        options.output = os.path.abspath(options.output)

        # The user specified an output directory:
        if os.path.isdir(options.output):
            output_path = os.path.join(options.output,
                                       metainfo['info']['name']+".torrent")
            if os.path.isfile(output_path):
                if not options.force and os.path.exists(output_path):
                    if "yes" != input("'%s' does already exist. Overwrite? "
                                      "yes/no: " % output_path):
                        parser.error("Aborted.")

        # The user specified a filename:
        else:
            # Is there already a file with this path? -> overwrite?!
            if os.path.isfile(options.output):
                if not options.force and os.path.exists(options.output):
                    if "yes" != input("'%s' does already exist. Overwrite? "
                                      "yes/no: " % options.output):
                        parser.error("Aborted.")

            output_path = options.output


    # Actually write the torrent file now.
    try:
        with open(output_path, "wb") as fh:
            fh.write(bencode(metainfo))
    except IOError as exc:
        print("IOError: " + str(exc), file=sys.stderr)
        print("Could not write the torrent file. Check torrent name and your "
              "privileges.", file=sys.stderr)
        print("Absolute output path: '%s'" % os.path.abspath(output_path),
              file=sys.stderr)
        return 1
    except KeyboardInterrupt:
        # Properly handle KeyboardInterrupts.
        # todo: open()'s context manager may already do this on his own?
        if os.path.exists(output_path):
            os.remove(output_path)

    # #########################
    # PREPARE AND PRINT SUMMARY
    # - but check quiet option

    # If the quiet option has been set, we're already finished here,
    # because we don't print a summary in this case.
    if options.quiet:
        return 0

    # Print summary!
    print("Successfully created torrent:")

    # Create the list of backup trackers.
    backup_trackers = ""
    if 'announce-list' in metainfo:
        _backup_trackers = metainfo['announce-list'][1:]
        _backup_trackers.sort(key=lambda x: x[0].lower())

        for tracker in _backup_trackers:
            backup_trackers += "    " + tracker[0] + "\n"
        backup_trackers = backup_trackers.rstrip()
    else:
        backup_trackers = "    (none)"

    # Calculate piece count.
    piece_count = math.ceil(torrent_size / metainfo['info']['piece length'])

    # Make torrent size human readable.
    if torrent_size > 10*MIB:
        size = "%.2f MiB" % (torrent_size / MIB)
    else:
        size = "%d KiB" % (torrent_size / KIB)

    # Make creation date human readable (ISO format).
    if 'creation date' in metainfo:
        creation_date = datetime.datetime.fromtimestamp(metainfo['creation \
date']).isoformat(' ')
    else:
        creation_date = "(none)"

    # Now actually print the summary table.
    print("  Name:             %s\n"
          "  Size:             %s\n"
          "  Pieces:           %d x %d KiB\n"
          "  Comment:          %s\n"
          "  Private:          %s\n"
          "  Creation date:    %s\n"
          "  Primary tracker:  %s\n"
          "  Backup trackers:\n"
          "%s"
      % (metainfo['info']['name'],
         size,
         piece_count,
         piece_length / KIB,
         metainfo['comment'] if 'comment'       in metainfo else "(none)",
         "yes" if options.private else "no",
         creation_date,
         metainfo['announce'],
         backup_trackers))

    return 0
def main(argv):
    # Validate the configuration.
    for abbr, replacement in TRACKER_ABBR.items():
        if not isinstance(abbr, str):
            print("Configuration error: invalid tracker abbrevation: '%s' "
                  "(must be a string instead)" % abbr,
                  file=sys.stderr)
            return 1
        if not isinstance(replacement, (str, list)):
            print("Configuration error: invalid tracker abbreviation: '%s' "
                  "(must be a string or list of strings instead)" %
                  str(replacement),
                  file=sys.stderr)
            return 1

    # Create OptionParser.
    kwargs = {
        'usage':
        "%prog [options] <file-or-directory> <main-tracker-url> "
        "[<backup-tracker-url> ...]",
        'version':
        "%%prog v%s" % VERSION,
        'description':
        "py3createtorrent is a comprehensive command line utility for "
        "creating torrents."
    }

    parser = optparse.OptionParser(**kwargs)

    # Add options to the OptionParser.
    # Note: Commonly used options are added first.
    parser.add_option(
        "-p",
        "--piece-length",
        type="int",
        action="store",
        dest="piece_length",
        default=0,
        help="piece size in KiB. 0 = automatic selection (default).")

    parser.add_option("-P",
                      "--private",
                      action="store_true",
                      dest="private",
                      default=False,
                      help="create private torrent")

    parser.add_option("-c",
                      "--comment",
                      type="string",
                      action="store",
                      dest="comment",
                      default=False,
                      help="include comment")

    parser.add_option(
        "-s",
        "--source",
        type="string",
        action="store",
        dest="source",
        default=False,
        help=
        "source string, used to create a different infohash for cross-seeding")

    parser.add_option("-f",
                      "--force",
                      action="store_true",
                      dest="force",
                      default=False,
                      help="dont ask anything, just do it")

    parser.add_option("-v",
                      "--verbose",
                      action="store_true",
                      dest="verbose",
                      default=False,
                      help="verbose mode")

    parser.add_option("-q",
                      "--quiet",
                      action="store_true",
                      dest="quiet",
                      default=False,
                      help="be quiet, e.g. don't print summary")

    parser.add_option("-o",
                      "--output",
                      type="string",
                      action="store",
                      dest="output",
                      default=None,
                      metavar="PATH",
                      help="custom output location (directory or complete "
                      "path). default = current directory.")

    parser.add_option("-e",
                      "--exclude",
                      type="string",
                      action="append",
                      dest="exclude",
                      default=[],
                      metavar="PATH",
                      help="exclude path (can be repeated)")

    parser.add_option("--exclude-pattern",
                      type="string",
                      action="append",
                      dest="exclude_pattern",
                      default=[],
                      metavar="REGEXP",
                      help="exclude paths matching the regular expression "
                      "(can be repeated)")

    parser.add_option(
        "--exclude-pattern-ci",
        type="string",
        action="append",
        dest="exclude_pattern_ci",
        default=[],
        metavar="REGEXP",
        help="exclude paths matching the case-insensitive regular "
        "expression (can be repeated)")

    parser.add_option("-d",
                      "--date",
                      type="int",
                      action="store",
                      dest="date",
                      default=-1,
                      metavar="TIMESTAMP",
                      help="set creation date (unix timestamp). -1 = now "
                      "(default). -2 = disable.")

    parser.add_option("-n",
                      "--name",
                      type="string",
                      action="store",
                      dest="name",
                      default=None,
                      help="use this file (or directory) name instead of the "
                      "real one")

    parser.add_option("--md5",
                      action="store_true",
                      dest="include_md5",
                      default=False,
                      help="include MD5 hashes in torrent file")

    (options, args) = parser.parse_args(args=argv[1:])

    # Positional arguments must have been provided:
    # -> file / directory plus at least one tracker.
    if len(args) < 2:
        parser.error("You must specify a valid path and at least one tracker.")

    # Ask the user if he really wants to use uncommon piece lengths.
    # (Unless the force option has been set.)
    if not options.force and 0 < options.piece_length < 16:
        if "yes" != input("It is strongly recommended to use a piece length "
                          "greater or equal than 16 KiB! Do you really want "
                          "to continue? yes/no: "):
            parser.error("Aborted.")

    if not options.force and options.piece_length > 1024:
        if "yes" != input("It is strongly recommended to use a maximum piece "
                          "length of 1024 KiB! Do you really want to "
                          "continue? yes/no: "):
            parser.error("Aborted.")

    # Verbose and quiet options may not be used together.
    if options.verbose and options.quiet:
        parser.error("Being verbose and quiet exclude each other.")

    global VERBOSE
    VERBOSE = options.verbose

    # ##########################################
    # CALCULATE/SET THE FOLLOWING METAINFO DATA:
    # - info
    #   - pieces (concatenated 20 byte sha1 hashes of all the data)
    #   - files (if multiple files)
    #   - length and md5sum (if single file)
    #   - name (may be overwritten in the next section by the --name option)

    node = os.path.abspath(args[0])
    trackers = args[1:]

    # Validate the given path.
    if not os.path.isfile(node) and not os.path.isdir(node):
        parser.error("'%s' neither is a file nor a directory." % node)

    # Evaluate / apply the tracker abbreviations.
    trackers = replace_in_list(trackers, TRACKER_ABBR)

    # Remove duplicate trackers.
    trackers = remove_duplicates(trackers)

    # Validate tracker URLs.
    invalid_trackers = False
    regexp = re.compile(r"^(http|https|udp)://", re.I)
    for t in trackers:
        if not regexp.search(t):
            print("Warning: Not a valid tracker URL: %s" % t, file=sys.stderr)
            invalid_trackers = True

    if invalid_trackers and not options.force:
        if "yes" != input("Some tracker URLs are invalid. Continue? yes/no: "):
            parser.error("Aborted.")

    # Parse and validate excluded paths.
    excluded_paths = frozenset([os.path.normcase(os.path.abspath(path)) \
                                for path in options.exclude])

    # Parse exclude patterns.
    excluded_regexps = set(
        re.compile(regexp) for regexp in options.exclude_pattern)
    excluded_regexps |= set(
        re.compile(regexp, re.IGNORECASE)
        for regexp in options.exclude_pattern_ci)

    # Warn the user if he attempts to exclude any paths when creating
    # a torrent for a single file (makes no sense).
    if os.path.isfile(node) and (len(excluded_paths) > 0 or \
       len(excluded_regexps) > 0):
        print(
            "Warning: Excluding paths is not possible when creating a "
            "torrent for a single file.",
            file=sys.stderr)

    # Warn the user if he attempts to exclude a specific path, that does not
    # even exist.
    for path in excluded_paths:
        if not os.path.exists(path):
            print(
                "Warning: You're excluding a path that does not exist: '%s'" %
                path,
                file=sys.stderr)

    # Get the torrent's files and / or calculate its size.
    if os.path.isfile(node):
        torrent_size = os.path.getsize(node)
    else:
        torrent_files = get_files_in_directory(
            node,
            excluded_paths=excluded_paths,
            excluded_regexps=excluded_regexps)
        torrent_size = sum([
            os.path.getsize(os.path.join(node, file)) for file in torrent_files
        ])

    # Torrents for 0 byte data can't be created.
    if torrent_size == 0:
        print("Error: Can't create torrent for 0 byte data.", file=sys.stderr)
        print("Check your files and exclusions!", file=sys.stderr)
        return 1

    # Calculate or parse the piece size.
    if options.piece_length == 0:
        piece_length = calculate_piece_length(torrent_size)
    elif options.piece_length > 0:
        piece_length = options.piece_length * KIB
    else:
        parser.error("Invalid piece size: '%d'" % options.piece_length)

    # Do the main work now.
    # -> prepare the metainfo dictionary.
    if os.path.isfile(node):
        info = create_single_file_info(node, piece_length, options.include_md5)
    else:
        info = create_multi_file_info(node, torrent_files, piece_length,
                                      options.include_md5)

    assert len(info['pieces']) % 20 == 0, "len(pieces) not a multiple of 20"

    # ###########################
    # FINISH METAINFO DICTIONARY:
    # - info
    #   - piece length
    #   - name (eventually overwrite)
    #   - private
    # - announce
    # - announce-list (if multiple trackers)
    # - creation date (may be disabled as well)
    # - created by
    # - comment (may be disabled as well (if ADVERTISE = False))

    # Finish sub-dict "info".
    info['piece length'] = piece_length

    if options.private:
        info['private'] = 1

    # Construct outer metainfo dict, which contains the torrent's whole
    # information.
    metainfo = {
        'info': info,
        'announce': trackers[0],
    }

    # Make "announce-list" field, if there are multiple trackers.
    if len(trackers) > 1:
        metainfo['announce-list'] = [[tracker] for tracker in trackers]

    # Set "creation date".
    # The user may specify a custom creation date. He may also decide not
    # to include the creation date field at all.
    if options.date == -1:
        # use current time
        metainfo['creation date'] = int(time.time())
    elif options.date >= 0:
        # use specified timestamp directly
        metainfo['creation date'] = options.date

    # Add the "created by" field.
    metainfo['created by'] = 'py3createtorrent'

    # Add user's comment or advertise py3createtorrent (unless this behaviour
    # has been disabled by the user).
    # The user may also decide not to include the comment field at all
    # by specifying an empty comment.
    if isinstance(options.comment, str):
        if len(options.comment) > 0:
            metainfo['comment'] = options.comment
    elif ADVERTISE:
        metainfo['comment'] = ""

    # Add a source string, which is used to create a different infohash for cross-seeding
    if isinstance(options.source, str):
        if len(options.source) > 0:
            metainfo['info']['source'] = options.source
    #else:
    #    metainfo['info']['source'] = ""

    # Add the name field.
    # By default this is the name of directory or file the torrent
    # is being created for.
    if options.name:
        options.name = options.name.strip()

        regexp = re.compile("^[A-Z0-9_\-\., ]+$", re.I)

        if not regexp.match(options.name):
            parser.error("Invalid name: '%s'. Allowed chars: A_Z, a-z, 0-9, "
                         "any of {.,_-} plus spaces." % options.name)

        metainfo['info']['name'] = options.name

    # ###################################################
    # BENCODE METAINFO DICTIONARY AND WRITE TORRENT FILE:
    # - take into consideration the --output option
    # - properly handle KeyboardInterrups while writing the file

    # Respect the custom output location.
    if not options.output:
        # Use current directory.
        output_path = metainfo['info']['name'] + ".torrent"

    else:
        # Use the directory or filename specified by the user.
        options.output = os.path.abspath(options.output)

        # The user specified an output directory:
        if os.path.isdir(options.output):
            output_path = os.path.join(options.output,
                                       metainfo['info']['name'] + ".torrent")
            if os.path.isfile(output_path):
                if not options.force and os.path.exists(output_path):
                    if "yes" != input("'%s' does already exist. Overwrite? "
                                      "yes/no: " % output_path):
                        parser.error("Aborted.")

        # The user specified a filename:
        else:
            # Is there already a file with this path? -> overwrite?!
            if os.path.isfile(options.output):
                if not options.force and os.path.exists(options.output):
                    if "yes" != input("'%s' does already exist. Overwrite? "
                                      "yes/no: " % options.output):
                        parser.error("Aborted.")

            output_path = options.output

    # Actually write the torrent file now.
    try:
        with open(output_path, "wb") as fh:
            fh.write(bencode(metainfo))
    except IOError as exc:
        print("IOError: " + str(exc), file=sys.stderr)
        print(
            "Could not write the torrent file. Check torrent name and your "
            "privileges.",
            file=sys.stderr)
        print("Absolute output path: '%s'" % os.path.abspath(output_path),
              file=sys.stderr)
        return 1
    except KeyboardInterrupt:
        # Properly handle KeyboardInterrupts.
        # todo: open()'s context manager may already do this on his own?
        if os.path.exists(output_path):
            os.remove(output_path)

    # #########################
    # PREPARE AND PRINT SUMMARY
    # - but check quiet option

    # If the quiet option has been set, we're already finished here,
    # because we don't print a summary in this case.
    if options.quiet:
        return 0

    # Print summary!
    print("Successfully created torrent:")

    # Create the list of backup trackers.
    backup_trackers = ""
    if 'announce-list' in metainfo:
        _backup_trackers = metainfo['announce-list'][1:]
        _backup_trackers.sort(key=lambda x: x[0].lower())

        for tracker in _backup_trackers:
            backup_trackers += "    " + tracker[0] + "\n"
        backup_trackers = backup_trackers.rstrip()
    else:
        backup_trackers = "    (none)"

    # Calculate piece count.
    piece_count = math.ceil(torrent_size / metainfo['info']['piece length'])

    # Make torrent size human readable.
    if torrent_size > 10 * MIB:
        size = "%.2f MiB" % (torrent_size / MIB)
    else:
        size = "%d KiB" % (torrent_size / KIB)

    # Make creation date human readable (ISO format).
    if 'creation date' in metainfo:
        creation_date = datetime.datetime.fromtimestamp(metainfo['creation \
date']).isoformat(' ')
    else:
        creation_date = "(none)"

    # Now actually print the summary table.
    print("  Name:             %s\n"
          "  Size:             %s\n"
          "  Pieces:           %d x %d KiB\n"
          "  Comment:          %s\n"
          "  Source String:    %s\n"
          "  Private:          %s\n"
          "  Creation date:    %s\n"
          "  Primary tracker:  %s\n"
          "  Backup trackers:\n"
          "%s" %
          (metainfo['info']['name'], size, piece_count, piece_length / KIB,
           metainfo['comment'] if 'comment' in metainfo else "(none)",
           metainfo['info']['source'] if 'source' in metainfo['info'] else
           "(none)", "yes" if options.private else "no", creation_date,
           metainfo['announce'], backup_trackers))

    return 0
def main(argv):
    # Validate the configuration.

    # Create OptionParser.
    kwargs = {
        'usage':
        "%prog [options] <file-or-directory> "
        "[<backup-tracker-url> ...]",
        'version':
        "%%prog v%s" % VERSION,
        'description':
        "py3createtorrent is a comprehensive command line utility for "
        "creating torrents."
    }

    parser = optparse.OptionParser(**kwargs)

    # Add options to the OptionParser.
    # Note: Commonly used options are added first.
    parser.add_option(
        "-p",
        "--piece-length",
        type="int",
        action="store",
        dest="piece_length",
        default=0,
        help="piece size in KiB. 0 = automatic selection (default).")

    parser.add_option("-P",
                      "--private",
                      action="store_true",
                      dest="private",
                      default=False,
                      help="create private torrent")

    parser.add_option("-c",
                      "--comment",
                      type="string",
                      action="store",
                      dest="comment",
                      default=False,
                      help="include comment")

    parser.add_option("-f",
                      "--force",
                      action="store_true",
                      dest="force",
                      default=False,
                      help="dont ask anything, just do it")

    parser.add_option("-o",
                      "--output",
                      type="string",
                      action="store",
                      dest="output",
                      default=None,
                      metavar="PATH",
                      help="custom output location (directory or complete "
                      "path). default = current directory.")

    parser.add_option("-e",
                      "--exclude",
                      type="string",
                      action="append",
                      dest="exclude",
                      default=[],
                      metavar="PATH",
                      help="exclude path (can be repeated)")

    parser.add_option("--exclude-pattern",
                      type="string",
                      action="append",
                      dest="exclude_pattern",
                      default=[],
                      metavar="REGEXP",
                      help="exclude paths matching the regular expression "
                      "(can be repeated)")

    parser.add_option(
        "--exclude-pattern-ci",
        type="string",
        action="append",
        dest="exclude_pattern_ci",
        default=[],
        metavar="REGEXP",
        help="exclude paths matching the case-insensitive regular "
        "expression (can be repeated)")

    parser.add_option("-d",
                      "--date",
                      type="int",
                      action="store",
                      dest="date",
                      default=-1,
                      metavar="TIMESTAMP",
                      help="set creation date (unix timestamp). -1 = now "
                      "(default). -2 = disable.")

    parser.add_option("-n",
                      "--name",
                      type="string",
                      action="store",
                      dest="name",
                      default=None,
                      help="use this file (or directory) name instead of the "
                      "real one")

    parser.add_option("--md5",
                      action="store_true",
                      dest="include_md5",
                      default=False,
                      help="include MD5 hashes in torrent file")

    (options, args) = parser.parse_args(args=argv[1:])

    # Positional arguments must have been provided:

    # Ask the user if he really wants to use uncommon piece lengths.
    # (Unless the force option has been set.)
    if not options.force and 0 < options.piece_length < 16:
        if "yes" != input("It is strongly recommended to use a piece length "
                          "greater or equal than 16 KiB! Do you really want "
                          "to continue? yes/no: "):
            parser.error("Aborted.")

    if not options.force and options.piece_length > 1024:
        if "yes" != input("It is strongly recommended to use a maximum piece "
                          "length of 1024 KiB! Do you really want to "
                          "continue? yes/no: "):
            parser.error("Aborted.")

    # ##########################################
    # CALCULATE/SET THE FOLLOWING METAINFO DATA:
    # - info
    #   - pieces (concatenated 20 byte sha1 hashes of all the data)
    #   - files (if multiple files)
    #   - length and md5sum (if single file)
    #   - name (may be overwritten in the next section by the --name option)

    node = os.path.abspath(args[0])

    # Validate the given path.
    if not os.path.isfile(node) and not os.path.isdir(node):
        parser.error("'%s' neither is a file nor a directory." % node)

    # Parse and validate excluded paths.
    excluded_paths = frozenset([os.path.normcase(os.path.abspath(path)) \
                                for path in options.exclude])

    # Parse exclude patterns.
    excluded_regexps = set(
        re.compile(regexp) for regexp in options.exclude_pattern)
    excluded_regexps |= set(
        re.compile(regexp, re.IGNORECASE)
        for regexp in options.exclude_pattern_ci)

    # Warn the user if he attempts to exclude any paths when creating
    # a torrent for a single file (makes no sense).
    if os.path.isfile(node) and (len(excluded_paths) > 0 or \
       len(excluded_regexps) > 0):
        print(
            "Warning: Excluding paths is not possible when creating a "
            "torrent for a single file.",
            file=sys.stderr)

    # Warn the user if he attempts to exclude a specific path, that does not
    # even exist.
    for path in excluded_paths:
        if not os.path.exists(path):
            print(
                "Warning: You're excluding a path that does not exist: '%s'" %
                path,
                file=sys.stderr)

    # Get the torrent's files and / or calculate its size.
    if os.path.isfile(node):
        torrent_size = os.path.getsize(node)
    else:
        torrent_files = get_files_in_directory(
            node,
            excluded_paths=excluded_paths,
            excluded_regexps=excluded_regexps)
        torrent_size = sum([
            os.path.getsize(os.path.join(node, file)) for file in torrent_files
        ])

    # Torrents for 0 byte data can't be created.
    if torrent_size == 0:
        raise Exception("No data for torrent.")

    # Calculate or parse the piece size.
    if options.piece_length == 0:
        piece_length = calculate_piece_length(torrent_size)
    elif options.piece_length > 0:
        piece_length = options.piece_length * KIB
    else:
        parser.error("Invalid piece size: '%d'" % options.piece_length)

    # Do the main work now.
    # -> prepare the metainfo dictionary.
    if os.path.isfile(node):
        info = create_single_file_info(node, piece_length, options.include_md5)
    else:
        info = create_multi_file_info(node, torrent_files, piece_length,
                                      options.include_md5)

    assert len(info['pieces']) % 20 == 0, "len(pieces) not a multiple of 20"

    # ###########################
    # FINISH METAINFO DICTIONARY:
    # - info
    #   - piece length
    #   - name (eventually overwrite)
    #   - private
    # - announce
    # - announce-list (if multiple trackers)
    # - creation date (may be disabled as well)
    # - created by

    # Finish sub-dict "info".
    info['piece length'] = piece_length

    if options.private:
        info['private'] = 1

    # Construct outer metainfo dict, which contains the torrent's whole
    # information.
    metainfo = {
        'info': info,
        'announce': 'http://academictorrents.com/announce.php',
    }

    # Set "creation date".
    # The user may specify a custom creation date. He may also decide not
    # to include the creation date field at all.
    if options.date == -1:
        # use current time
        metainfo['creation date'] = int(time.time())
    elif options.date >= 0:
        # use specified timestamp directly
        metainfo['creation date'] = options.date

    # Add the "created by" field.
    metainfo['created by'] = 'py3createtorrent v%s' % VERSION

    # Add user's comment or advertise py3createtorrent (unless this behaviour
    # has been disabled by the user).
    # The user may also decide not to include the comment field at all
    # by specifying an empty comment.
    if isinstance(options.comment, str):
        if len(options.comment) > 0:
            metainfo['comment'] = options.comment

    # Add the name field.
    # By default this is the name of directory or file the torrent
    # is being created for.
    if options.name:
        options.name = options.name.strip()

        regexp = re.compile("^[A-Z0-9_\-\., ]+$", re.I)

        if not regexp.match(options.name):
            parser.error("Invalid name: '%s'. Allowed chars: A_Z, a-z, 0-9, "
                         "any of {.,_-} plus spaces." % options.name)

        metainfo['info']['name'] = options.name

    # ###################################################
    # BENCODE METAINFO DICTIONARY AND WRITE TORRENT FILE:
    # - take into consideration the --output option
    # - properly handle KeyboardInterrups while writing the file

    # Respect the custom output location.
    if not options.output:
        # Use current directory.
        output_path = metainfo['info']['name'] + ".torrent"

    else:
        # Use the directory or filename specified by the user.
        options.output = os.path.abspath(options.output)

        # The user specified an output directory:
        if os.path.isdir(options.output):
            output_path = os.path.join(options.output,
                                       metainfo['info']['name'] + ".torrent")
            if os.path.isfile(output_path):
                if not options.force and os.path.exists(output_path):
                    if "yes" != input("'%s' does already exist. Overwrite? "
                                      "yes/no: " % output_path):
                        parser.error("Aborted.")

        # The user specified a filename:
        else:
            # Is there already a file with this path? -> overwrite?!
            if os.path.isfile(options.output):
                if not options.force and os.path.exists(options.output):
                    if "yes" != input("'%s' does already exist. Overwrite? "
                                      "yes/no: " % options.output):
                        parser.error("Aborted.")

            output_path = options.output

    # Actually write the torrent file now.
    try:
        with open(output_path, "wb") as fh:
            fh.write(bencode(metainfo))
    except IOError as exc:
        print("IOError: " + str(exc), file=sys.stderr)
        print(
            "Could not write the torrent file. Check torrent name and your "
            "privileges.",
            file=sys.stderr)
        print("Absolute output path: '%s'" % os.path.abspath(output_path),
              file=sys.stderr)
        return 1
    except KeyboardInterrupt:
        # Properly handle KeyboardInterrupts.
        # todo: open()'s context manager may already do this on his own?
        if os.path.exists(output_path):
            os.remove(output_path)
    return 0
Beispiel #4
0
def make_torrent(node):
    # CALCULATE/SET THE FOLLOWING METAINFO DATA:
    # - info
    #   - pieces (concatenated 20 byte sha1 hashes of all the data)
    #   - files (if multiple files)
    #   - length and md5sum (if single file)
    #   - name (may be overwritten in the next section by the --name option)

    node = os.path.abspath(node)
    # Validate the given path.
    if not os.path.isfile(node) and not os.path.isdir(node):
        raise Exception("'%s' neither is a file nor a directory." % node)

    # Get the torrent's files and / or calculate its size.
    if os.path.isfile(node):
        torrent_size = os.path.getsize(node)
    else:
        torrent_files = get_files_in_directory(node)
        torrent_size = sum([os.path.getsize(os.path.join(node, file))
                            for file in torrent_files])

    # Torrents for 0 byte data can't be created.
    if torrent_size == 0:
        raise Exception("No data for torrent.")

    piece_length = calculate_piece_length(torrent_size)

    # Do the main work now.
    # -> prepare the metainfo dictionary.
    if os.path.isfile(node):
        info = create_single_file_info(node, piece_length)
    else:
        info = create_multi_file_info(node, torrent_files, piece_length)
    info['piece length'] = piece_length

    # Finish sub-dict "info".

    # Construct outer metainfo dict, which contains the torrent's whole
    # information.
    metainfo =  {
        'info': info,
        'announce': 'http://academictorrents.com/announce.php',
        'creation date': int(time.time()),
        'created by': '',
    }

    # ###################################################
    # BENCODE METAINFO DICTIONARY AND WRITE TORRENT FILE:
    # - properly handle KeyboardInterrups while writing the file

    # Use current directory.
    output_path = metainfo['info']['name'] + ".torrent"

    # Actually write the torrent file now.
    try:
        with open(output_path, "wb") as fh:
            fh.write(bencode(metainfo))
    except IOError as exc:
        print("IOError: " + str(exc), file=sys.stderr)
        print("Could not write the torrent file. Check torrent name and your "
              "privileges.", file=sys.stderr)
        return 1
    except KeyboardInterrupt:
        # Properly handle KeyboardInterrupts.
        # todo: open()'s context manager may already do this on his own?
        if os.path.exists(output_path):
            os.remove(output_path)
    return output_path
Beispiel #5
0
def main(folder, tracker):

    (options, args) = option_parse().parse_args(args=[folder, tracker])

    # Positional arguments must have been provided:
    # -> file / directory plus at least one tracker.
    if len(args) < 2:
        parser.error("You must specify a valid path and at least one tracker.")

    # Ask the user if he really wants to use uncommon piece lengths.
    # (Unless the force option has been set.)
    if not options.force and 0 < options.piece_length < 16:
        if "yes" != input("It is strongly recommended to use a piece length \
greater or equal than 16 KiB! Do you really want to continue? yes/no: "):
            parser.error("Aborted.")

    if not options.force and options.piece_length > 1024:
        if "yes" != input("It is strongly recommended to use a maximum piece \
length of 1024 KiB! Do you really want to continue? yes/no: "):
            parser.error("Aborted.")

    # Verbose and quiet options may not be used together.
    if options.verbose and options.quiet:
        parser.error("Being verbose and quiet exclude each other.")

    ## manually setting private flag to True
    options.private = True

    global VERBOSE
    VERBOSE = False

    # ##########################################
    # CALCULATE/SET THE FOLLOWING METAINFO DATA:
    # - info
    #   - pieces (concatenated 20 byte sha1 hashes of all the data)
    #   - files (if multiple files)
    #   - length and md5sum (if single file)
    #   - name (may be overwritten in the next section by the --name option)

    node = folder
    trackers = [tracker]

    # Validate the given path.
    if not os.path.isfile(node) and not os.path.isdir(node):
        parser.error("'%s' neither is a file nor a directory." % node)

    # Parse and validate excluded paths.
    excluded_paths = frozenset([os.path.normcase(os.path.abspath(path)) \
                                for path in options.exclude])

    # Parse exclude patterns.
    excluded_regexps = frozenset(options.exclude_pattern)

    # Warn the user if he attempts to exclude any paths when creating
    # a torrent for a single file (makes no sense).
    if os.path.isfile(node) and (len(excluded_paths) > 0 or \
       len(excluded_regexps) > 0):
        print("Warning: Excluding paths is not possible when creating a \
torrent for a single file.")

    # Warn the user if he attempts to exclude a specific path, that does not
    # even exist.
    for path in excluded_paths:
        if not os.path.exists(path):
            print(
                "Warning: You're excluding a path that does not exist: '%s'" %
                path)

    # Get the torrent's files and / or calculate its size.
    if os.path.isfile(node):
        torrent_size = os.path.getsize(node)
    else:
        torrent_files = get_files_in_directory(
            node,
            excluded_paths=excluded_paths,
            excluded_regexps=excluded_regexps)
        torrent_size = int(sum([os.path.getsize(f) for f in torrent_files]))

    # Torrents for 0 byte data can't be created.
    if torrent_size == 0:
        print("Error: Can't create torrent for 0 byte data.")
        print("Check your files and exclusions!")
        return 1

    # Calculate or parse the piece size.
    if options.piece_length == 0:
        piece_length = calculate_piece_length(torrent_size)
    elif options.piece_length > 0:
        piece_length = options.piece_length * KIB
    else:
        parser.error("Invalid piece size: '%d'" % options.piece_length)

    # Do the main work now.
    # -> prepare the metainfo dictionary.
    if os.path.isfile(node):
        info = create_single_file_info(node, piece_length)
    else:
        info = create_multi_file_info(node, torrent_files, piece_length)

    assert len(info['pieces']) % 20 == 0, "len(pieces) not a multiple of 20"

    # ###########################
    # FINISH METAINFO DICTIONARY:
    # - info
    #   - piece length
    #   - name (eventually overwrite)
    #   - private
    # - announce
    # - announce-list (if multiple trackers)
    # - creation date (may be disabled as well)
    # - created by
    # - comment (may be disabled as well (if ADVERTISE = False))

    # Finish sub-dict "info".
    info['piece length'] = piece_length

    # flaming - private flag should always be on (see above)
    if options.private:
        info['private'] = 1

    # Construct outer metainfo dict, which contains the torrent's whole
    # information.
    metainfo = {
        'info': info,
        'announce': trackers[0],
    }

    # Make "announce-list" field, if there are multiple trackers.
    if len(trackers) > 1:
        metainfo['announce-list'] = [[tracker] for tracker in trackers]

    # Set "creation date".
    # The user may specify a custom creation date. He may also decide not
    # to include the creation date field at all.
    if options.date == -1:
        # use current time
        metainfo['creation date'] = int(time.time())
    elif options.date >= 0:
        # use specified timestamp directly
        metainfo['creation date'] = options.date

    # Add the "created by" field.
    metainfo['created by'] = 'py3ct v%s' % VERSION

    # Comment field
    # flaming - disabled for now
    ##    if isinstance(options.comment, str):
    ##        if len(options.comment) > 0:
    ##            metainfo['comment'] = options.comment

    # Add the name field.
    # By default this is the name of directory or file the torrent
    # is being created for.
    if options.name:
        options.name = options.name.strip()

        regexp = re.compile("^[A-Z0-9_\-\., ]+$", re.I)

        if not regexp.match(options.name):
            parser.error("Invalid name: '%s'. Allowed chars: A_Z, a-z, \
0-9, any of {.,_-} plus spaces." % options.name)

        metainfo['info']['name'] = options.name

    # ###################################################
    # BENCODE METAINFO DICTIONARY AND WRITE TORRENT FILE:
    # - take into consideration the --output option
    # - properly handle KeyboardInterrups while writing the file

    # Respect the custom output location.
    if not options.output:
        # Use current directory.
        output_path = metainfo['info']['name'] + ".torrent"

    else:
        # Use the directory or filename specified by the user.
        options.output = os.path.abspath(options.output)

        # The user specified an output directory:
        if os.path.isdir(options.output):
            output_path = os.path.join(options.output,
                                       metainfo['info']['name'] + ".torrent")
            if os.path.isfile(output_path):
                if not options.force and os.path.exists(output_path):
                    if "yes" != input("'%s' does already exist. Overwrite? \
yes/no: " % output_path):
                        parser.error("Aborted.")

        # The user specified a filename:
        else:
            # Is there already a file with this path? -> overwrite?!
            if os.path.isfile(options.output):
                if not options.force and os.path.exists(options.output):
                    if "yes" != input("'%s' does already exist. Overwrite? \
yes/no: " % options.output):
                        parser.error("Aborted.")

            output_path = options.output

    # Actually write the torrent file now.
    try:
        fh = open(output_path, 'wb')
        fh.write(bencode(metainfo))
        fh.close()
    except IOError, exc:
        print("IOError: " + str(exc))
        print("Could not write the torrent file. Check torrent name and your \
privileges.")
        print("Absolute output path: '%s'" % os.path.abspath(output_path))
        return 1
Beispiel #6
0
        localFile, headers = urlretrieve(latestURI, file_location / filename)
        command = [
            '/usr/bin/python3',
            '{}/py3createtorrent.py'.format(script_location),
            '-o',
            tempDir,
            localFile,
        ] + trackers

        run(command)

        # Calculating the magnet link and storing it in a file
        torrent = Path(tempDir + '/' + filename + '.torrent').open('rb').read()
        metadata = bdecode(torrent)

        hashcontents = bencode(metadata['info'])
        digest = hashlib.sha1(hashcontents).hexdigest()

        params = {
            'dn': metadata['info']['name'],
            'tr': metadata['announce'],
            'xl': metadata['info']['length']
        }
        paramstr = urlencode(params)
        magneturi = 'magnet:?xt=urn:btih:{}&{}'.format(digest, paramstr)
        with (webroot / 'magnetLinks' / (filename + '.txt')).open('w') as file:
            file.write(magneturi + '\n')

        # Magnet has been calculated, put a copy of the torrent in the web root
        copy(tempDir + '/' + filename + '.torrent', webroot / 'torrents/')
def main(folder,tracker):

    (options, args) = option_parse().parse_args(args = [folder,tracker])

    # Positional arguments must have been provided:
    # -> file / directory plus at least one tracker.
    if len(args) < 2:
        parser.error("You must specify a valid path and at least one tracker.")

    # Ask the user if he really wants to use uncommon piece lengths.
    # (Unless the force option has been set.)
    if not options.force and 0 < options.piece_length < 16:
        if "yes" != input("It is strongly recommended to use a piece length \
greater or equal than 16 KiB! Do you really want to continue? yes/no: "):
            parser.error("Aborted.")

    if not options.force and options.piece_length > 1024:
        if "yes" != input("It is strongly recommended to use a maximum piece \
length of 1024 KiB! Do you really want to continue? yes/no: "):
            parser.error("Aborted.")

    # Verbose and quiet options may not be used together.
    if options.verbose and options.quiet:
        parser.error("Being verbose and quiet exclude each other.")

    ## manually setting private flag to True
    options.private = True

    global VERBOSE
    VERBOSE = False

    # ##########################################
    # CALCULATE/SET THE FOLLOWING METAINFO DATA:
    # - info
    #   - pieces (concatenated 20 byte sha1 hashes of all the data)
    #   - files (if multiple files)
    #   - length and md5sum (if single file)
    #   - name (may be overwritten in the next section by the --name option)

    node = folder
    trackers = [tracker]

    # Validate the given path.
    if not os.path.isfile(node) and not os.path.isdir(node):
        parser.error("'%s' neither is a file nor a directory." % node)

    # Parse and validate excluded paths.
    excluded_paths = frozenset([os.path.normcase(os.path.abspath(path)) \
                                for path in options.exclude])

    # Parse exclude patterns.
    excluded_regexps = frozenset(options.exclude_pattern)

    # Warn the user if he attempts to exclude any paths when creating
    # a torrent for a single file (makes no sense).
    if os.path.isfile(node) and (len(excluded_paths) > 0 or \
       len(excluded_regexps) > 0):
        print("Warning: Excluding paths is not possible when creating a \
torrent for a single file.")

    # Warn the user if he attempts to exclude a specific path, that does not
    # even exist.
    for path in excluded_paths:
        if not os.path.exists(path):
            print("Warning: You're excluding a path that does not exist: '%s'"
                  % path)

    # Get the torrent's files and / or calculate its size.
    if os.path.isfile(node):
        torrent_size = os.path.getsize(node)
    else:
        torrent_files = get_files_in_directory(node,
                                         excluded_paths=excluded_paths,
                                         excluded_regexps=excluded_regexps)
        torrent_size = int(sum([os.path.getsize(f) for f in torrent_files]))

    # Torrents for 0 byte data can't be created.
    if torrent_size == 0:
        print("Error: Can't create torrent for 0 byte data.")
        print("Check your files and exclusions!")
        return 1

    # Calculate or parse the piece size.
    if options.piece_length == 0:
        piece_length = calculate_piece_length(torrent_size)
    elif options.piece_length > 0:
        piece_length = options.piece_length * KIB
    else:
        parser.error("Invalid piece size: '%d'" % options.piece_length)

    # Do the main work now.
    # -> prepare the metainfo dictionary.
    if os.path.isfile(node):
        info = create_single_file_info(node, piece_length)
    else:
        info = create_multi_file_info(node, torrent_files, piece_length)

    assert len(info['pieces']) % 20 == 0, "len(pieces) not a multiple of 20"

    # ###########################
    # FINISH METAINFO DICTIONARY:
    # - info
    #   - piece length
    #   - name (eventually overwrite)
    #   - private
    # - announce
    # - announce-list (if multiple trackers)
    # - creation date (may be disabled as well)
    # - created by
    # - comment (may be disabled as well (if ADVERTISE = False))

    # Finish sub-dict "info".
    info['piece length'] = piece_length

    # flaming - private flag should always be on (see above)
    if options.private:
        info['private'] = 1

    # Construct outer metainfo dict, which contains the torrent's whole
    # information.
    metainfo =  {
                'info':           info,
                'announce':       trackers[0],
                }

    # Make "announce-list" field, if there are multiple trackers.
    if len(trackers) > 1:
        metainfo['announce-list'] = [[tracker] for tracker in trackers]

    # Set "creation date".
    # The user may specify a custom creation date. He may also decide not
    # to include the creation date field at all.
    if   options.date == -1:
        # use current time
        metainfo['creation date'] = int(time.time())
    elif options.date >= 0:
        # use specified timestamp directly
        metainfo['creation date'] = options.date

    # Add the "created by" field.
    metainfo['created by'] = 'py3ct v%s' % VERSION

    # Comment field
    # flaming - disabled for now
##    if isinstance(options.comment, str):
##        if len(options.comment) > 0:
##            metainfo['comment'] = options.comment

    # Add the name field.
    # By default this is the name of directory or file the torrent
    # is being created for.
    if options.name:
        options.name = options.name.strip()

        regexp = re.compile("^[A-Z0-9_\-\., ]+$", re.I)

        if not regexp.match(options.name):
            parser.error("Invalid name: '%s'. Allowed chars: A_Z, a-z, \
0-9, any of {.,_-} plus spaces." % options.name)

        metainfo['info']['name'] = options.name

    # ###################################################
    # BENCODE METAINFO DICTIONARY AND WRITE TORRENT FILE:
    # - take into consideration the --output option
    # - properly handle KeyboardInterrups while writing the file

    # Respect the custom output location.
    if not options.output:
        # Use current directory.
        output_path = metainfo['info']['name'] + ".torrent"

    else:
        # Use the directory or filename specified by the user.
        options.output = os.path.abspath(options.output)

        # The user specified an output directory:
        if os.path.isdir(options.output):
            output_path = os.path.join(options.output,
                                       metainfo['info']['name']+".torrent")
            if os.path.isfile(output_path):
                if not options.force and os.path.exists(output_path):
                    if "yes" != input("'%s' does already exist. Overwrite? \
yes/no: " % output_path):
                        parser.error("Aborted.")

        # The user specified a filename:
        else:
            # Is there already a file with this path? -> overwrite?!
            if os.path.isfile(options.output):
                if not options.force and os.path.exists(options.output):
                    if "yes" != input("'%s' does already exist. Overwrite? \
yes/no: " % options.output):
                        parser.error("Aborted.")

            output_path = options.output


    # Actually write the torrent file now.
    try:
        fh = open(output_path,'wb')
        fh.write(bencode(metainfo))
        fh.close()
    except IOError, exc:
        print("IOError: " + str(exc))
        print("Could not write the torrent file. Check torrent name and your \
privileges.")
        print("Absolute output path: '%s'" % os.path.abspath(output_path))
        return 1
def main(argv):
    # Validate the configuration.

    # Create OptionParser.
    kwargs = {
        "usage": "%prog [options] <file-or-directory> " "[<backup-tracker-url> ...]",
        "version": "%%prog v%s" % VERSION,
        "description": "py3createtorrent is a comprehensive command line utility for " "creating torrents.",
    }

    parser = optparse.OptionParser(**kwargs)

    # Add options to the OptionParser.
    # Note: Commonly used options are added first.
    parser.add_option(
        "-p",
        "--piece-length",
        type="int",
        action="store",
        dest="piece_length",
        default=0,
        help="piece size in KiB. 0 = automatic selection (default).",
    )

    parser.add_option(
        "-P", "--private", action="store_true", dest="private", default=False, help="create private torrent"
    )

    parser.add_option(
        "-c", "--comment", type="string", action="store", dest="comment", default=False, help="include comment"
    )

    parser.add_option(
        "-f", "--force", action="store_true", dest="force", default=False, help="dont ask anything, just do it"
    )

    parser.add_option(
        "-o",
        "--output",
        type="string",
        action="store",
        dest="output",
        default=None,
        metavar="PATH",
        help="custom output location (directory or complete " "path). default = current directory.",
    )

    parser.add_option(
        "-e",
        "--exclude",
        type="string",
        action="append",
        dest="exclude",
        default=[],
        metavar="PATH",
        help="exclude path (can be repeated)",
    )

    parser.add_option(
        "--exclude-pattern",
        type="string",
        action="append",
        dest="exclude_pattern",
        default=[],
        metavar="REGEXP",
        help="exclude paths matching the regular expression " "(can be repeated)",
    )

    parser.add_option(
        "--exclude-pattern-ci",
        type="string",
        action="append",
        dest="exclude_pattern_ci",
        default=[],
        metavar="REGEXP",
        help="exclude paths matching the case-insensitive regular " "expression (can be repeated)",
    )

    parser.add_option(
        "-d",
        "--date",
        type="int",
        action="store",
        dest="date",
        default=-1,
        metavar="TIMESTAMP",
        help="set creation date (unix timestamp). -1 = now " "(default). -2 = disable.",
    )

    parser.add_option(
        "-n",
        "--name",
        type="string",
        action="store",
        dest="name",
        default=None,
        help="use this file (or directory) name instead of the " "real one",
    )

    parser.add_option(
        "--md5", action="store_true", dest="include_md5", default=False, help="include MD5 hashes in torrent file"
    )

    (options, args) = parser.parse_args(args=argv[1:])

    # Positional arguments must have been provided:

    # Ask the user if he really wants to use uncommon piece lengths.
    # (Unless the force option has been set.)
    if not options.force and 0 < options.piece_length < 16:
        if "yes" != input(
            "It is strongly recommended to use a piece length "
            "greater or equal than 16 KiB! Do you really want "
            "to continue? yes/no: "
        ):
            parser.error("Aborted.")

    if not options.force and options.piece_length > 1024:
        if "yes" != input(
            "It is strongly recommended to use a maximum piece "
            "length of 1024 KiB! Do you really want to "
            "continue? yes/no: "
        ):
            parser.error("Aborted.")

    # ##########################################
    # CALCULATE/SET THE FOLLOWING METAINFO DATA:
    # - info
    #   - pieces (concatenated 20 byte sha1 hashes of all the data)
    #   - files (if multiple files)
    #   - length and md5sum (if single file)
    #   - name (may be overwritten in the next section by the --name option)

    node = os.path.abspath(args[0])

    # Validate the given path.
    if not os.path.isfile(node) and not os.path.isdir(node):
        parser.error("'%s' neither is a file nor a directory." % node)

    # Parse and validate excluded paths.
    excluded_paths = frozenset([os.path.normcase(os.path.abspath(path)) for path in options.exclude])

    # Parse exclude patterns.
    excluded_regexps = set(re.compile(regexp) for regexp in options.exclude_pattern)
    excluded_regexps |= set(re.compile(regexp, re.IGNORECASE) for regexp in options.exclude_pattern_ci)

    # Warn the user if he attempts to exclude any paths when creating
    # a torrent for a single file (makes no sense).
    if os.path.isfile(node) and (len(excluded_paths) > 0 or len(excluded_regexps) > 0):
        print("Warning: Excluding paths is not possible when creating a " "torrent for a single file.", file=sys.stderr)

    # Warn the user if he attempts to exclude a specific path, that does not
    # even exist.
    for path in excluded_paths:
        if not os.path.exists(path):
            print("Warning: You're excluding a path that does not exist: '%s'" % path, file=sys.stderr)

    # Get the torrent's files and / or calculate its size.
    if os.path.isfile(node):
        torrent_size = os.path.getsize(node)
    else:
        torrent_files = get_files_in_directory(node, excluded_paths=excluded_paths, excluded_regexps=excluded_regexps)
        torrent_size = sum([os.path.getsize(os.path.join(node, file)) for file in torrent_files])

    # Torrents for 0 byte data can't be created.
    if torrent_size == 0:
        raise Exception("No data for torrent.")

    # Calculate or parse the piece size.
    if options.piece_length == 0:
        piece_length = calculate_piece_length(torrent_size)
    elif options.piece_length > 0:
        piece_length = options.piece_length * KIB
    else:
        parser.error("Invalid piece size: '%d'" % options.piece_length)

    # Do the main work now.
    # -> prepare the metainfo dictionary.
    if os.path.isfile(node):
        info = create_single_file_info(node, piece_length, options.include_md5)
    else:
        info = create_multi_file_info(node, torrent_files, piece_length, options.include_md5)

    assert len(info["pieces"]) % 20 == 0, "len(pieces) not a multiple of 20"

    # ###########################
    # FINISH METAINFO DICTIONARY:
    # - info
    #   - piece length
    #   - name (eventually overwrite)
    #   - private
    # - announce
    # - announce-list (if multiple trackers)
    # - creation date (may be disabled as well)
    # - created by

    # Finish sub-dict "info".
    info["piece length"] = piece_length

    if options.private:
        info["private"] = 1

    # Construct outer metainfo dict, which contains the torrent's whole
    # information.
    metainfo = {"info": info, "announce": "http://academictorrents.com/announce.php"}

    # Set "creation date".
    # The user may specify a custom creation date. He may also decide not
    # to include the creation date field at all.
    if options.date == -1:
        # use current time
        metainfo["creation date"] = int(time.time())
    elif options.date >= 0:
        # use specified timestamp directly
        metainfo["creation date"] = options.date

    # Add the "created by" field.
    metainfo["created by"] = "py3createtorrent v%s" % VERSION

    # Add user's comment or advertise py3createtorrent (unless this behaviour
    # has been disabled by the user).
    # The user may also decide not to include the comment field at all
    # by specifying an empty comment.
    if isinstance(options.comment, str):
        if len(options.comment) > 0:
            metainfo["comment"] = options.comment

    # Add the name field.
    # By default this is the name of directory or file the torrent
    # is being created for.
    if options.name:
        options.name = options.name.strip()

        regexp = re.compile("^[A-Z0-9_\-\., ]+$", re.I)

        if not regexp.match(options.name):
            parser.error(
                "Invalid name: '%s'. Allowed chars: A_Z, a-z, 0-9, " "any of {.,_-} plus spaces." % options.name
            )

        metainfo["info"]["name"] = options.name

    # ###################################################
    # BENCODE METAINFO DICTIONARY AND WRITE TORRENT FILE:
    # - take into consideration the --output option
    # - properly handle KeyboardInterrups while writing the file

    # Respect the custom output location.
    if not options.output:
        # Use current directory.
        output_path = metainfo["info"]["name"] + ".torrent"

    else:
        # Use the directory or filename specified by the user.
        options.output = os.path.abspath(options.output)

        # The user specified an output directory:
        if os.path.isdir(options.output):
            output_path = os.path.join(options.output, metainfo["info"]["name"] + ".torrent")
            if os.path.isfile(output_path):
                if not options.force and os.path.exists(output_path):
                    if "yes" != input("'%s' does already exist. Overwrite? " "yes/no: " % output_path):
                        parser.error("Aborted.")

        # The user specified a filename:
        else:
            # Is there already a file with this path? -> overwrite?!
            if os.path.isfile(options.output):
                if not options.force and os.path.exists(options.output):
                    if "yes" != input("'%s' does already exist. Overwrite? " "yes/no: " % options.output):
                        parser.error("Aborted.")

            output_path = options.output

    # Actually write the torrent file now.
    try:
        with open(output_path, "wb") as fh:
            fh.write(bencode(metainfo))
    except IOError as exc:
        print("IOError: " + str(exc), file=sys.stderr)
        print("Could not write the torrent file. Check torrent name and your " "privileges.", file=sys.stderr)
        print("Absolute output path: '%s'" % os.path.abspath(output_path), file=sys.stderr)
        return 1
    except KeyboardInterrupt:
        # Properly handle KeyboardInterrupts.
        # todo: open()'s context manager may already do this on his own?
        if os.path.exists(output_path):
            os.remove(output_path)
    return 0