Packages

Create torrent files from single or multiple files.

Current section

Files

Jump to
torrex lib torrex.ex
Raw

lib/torrex.ex

defmodule Torrex do
@moduledoc """
Generate .torrent files for single files and directories.
"""
alias Torrex.FileUtils
@default_piece_length 524_288 # 512 KiB
@typedoc """
Encodable types.
"""
@type encodable :: file | directory
@typedoc """
Path to a file.
"""
@type file :: String.t
@typedoc """
Path to a directory of files.
"""
@type directory :: String.t
@type options :: [option]
@type option ::
piece_length |
output |
announce_list |
creation_date |
comment |
created_by |
private |
md5sum
@typedoc """
Length of each file piece in the torrent. Defaults to 512 KiB.
"""
@type piece_length :: {:piece_length, integer}
@typedoc """
Name of the output .torrent file. Defaults to provided file/directory name.
"""
@type output :: {:output, String.t}
@typedoc """
List of announce URLs (optional).
"""
@type announce_list :: {:announce_list, [String.t]}
@typedoc """
Include the creation time of the torrent, in standard UNIX epoch format (optional).
Defaults to true.
"""
@type creation_date :: {:creation_date, boolean}
@typedoc """
Include a free-form textual comment (optional).
"""
@type comment :: {:comment, String.t}
@typedoc """
Name of version of the program used to create the .torrent (optional).
"""
@type created_by :: {:created_by, String.t}
@typedoc """
Is the torrent private (optional)? Defaults to false.
"""
@type private :: {:private, boolean}
@typedoc """
Include an md5 of all files in the torrent (optional). Defaults to false.
"""
@type md5sum :: {:md5sum, boolean}
@doc """
Encodes a file or directory to a .torrent file.
"""
@spec encode(encodable, String.t, options) :: String.t
def encode(path, announce, opts \\ []) do
type = case File.dir?(path) do
true -> :dir
_ -> :file
end
piece_length = opts[:piece_length] || @default_piece_length
%{
"info" => encode_info(type, path, piece_length, opts),
"announce" => announce,
"encoding" => "UTF-8"
}
|> set_options(opts)
|> write_to_file(path, opts)
end
defp encode_info(:file, path, piece_length, opts) do
%{
"piece length" => piece_length,
"pieces" => FileUtils.hash_pieces(path, piece_length),
"name" => Path.basename(path),
"length" => File.stat!(path).size
}
|> set_private(opts)
|> maybe_set_md5(path, opts)
end
defp encode_info(:dir, path, piece_length, opts) do
files = FileUtils.traverse_dir(path)
%{
"piece length" => piece_length,
"pieces" => FileUtils.hash_pieces(files, piece_length),
"name" => Path.basename(path),
"files" => encode_files(files, path, opts)
}
|> set_private(opts)
end
defp encode_files(files, base_dir, opts) do
Enum.map(files, fn path ->
%{
"length" => File.stat!(path).size,
"path" => encode_path(path, base_dir)
}
|> maybe_set_md5(path, opts)
end)
end
defp encode_path(path, base_dir) do
path
|> Path.relative_to(base_dir)
|> Path.split()
end
defp set_private(info, opts) do
case opts[:private] || false do
true -> Map.put(info, "private", 1)
_ -> Map.put(info, "private", 0)
end
end
defp maybe_set_md5(info, path, opts) do
case opts[:md5sum] || false do
true -> Map.put(info, "md5sum", FileUtils.md5_stream(path))
_ -> info
end
end
defp set_options(dict, opts) do
dict
|> maybe_set_announce_list(opts)
|> maybe_set_creation_date(opts)
|> maybe_set_comment(opts)
|> maybe_set_created_by(opts)
end
defp maybe_set_announce_list(dict, opts) do
case opts[:announce_list] do
list when is_list(list) -> Map.put(dict, "announce-list", list)
_ -> dict
end
end
defp maybe_set_creation_date(dict, opts) do
case opts[:creation_date] || true do
true -> Map.put(dict, "creation date", :os.system_time(:seconds))
_ -> dict
end
end
defp maybe_set_comment(dict, opts) do
case opts[:comment] do
comment when is_bitstring(comment) -> Map.put(dict, "comment", comment)
_ -> dict
end
end
defp maybe_set_created_by(dict, opts) do
case opts[:created_by] do
author when is_bitstring(author) -> Map.put(dict, "created by", author)
_ -> dict
end
end
defp write_to_file(dict, path, opts) do
iodata = Benx.encode(dict)
name = determine_torrent_name(path, opts)
File.write!(name, iodata)
name
end
defp determine_torrent_name(path, opts) do
case opts[:output] do
name when is_bitstring(name) -> name
_ -> name_from_path(path)
end
|> Kernel.<>(".torrent")
end
defp name_from_path(path) do
case File.dir?(path) do
true -> path
_ -> Path.rootname(path)
end
|> Path.split()
|> List.last()
end
@doc """
Decodes a .torrent file.
"""
@spec decode(file) :: map
def decode(path) do
path
|> File.read!()
|> :erlang.binary_to_list()
|> Benx.decode!()
end
end