mirror of
https://github.com/Ernous/TorrServerJellyfin.git
synced 2025-12-18 13:06:09 +05:00
initial
This commit is contained in:
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, build with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
TorrServer.iml
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
src/github.com/
|
||||||
|
src/golang.org/
|
||||||
|
src/bazil.org/
|
||||||
|
src/gopkg.in/
|
||||||
|
pkg/
|
||||||
|
bin/
|
||||||
|
dist/
|
||||||
674
LICENSE
Normal file
674
LICENSE
Normal file
@@ -0,0 +1,674 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
|
GNU General Public License for most of our software; it applies also to
|
||||||
|
any other work released this way by its authors. You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to prevent others from denying you
|
||||||
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
|
or can get the source code. And you must show them these terms so they
|
||||||
|
know their rights.
|
||||||
|
|
||||||
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
|
|
||||||
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
|
that there is no warranty for this free software. For both users' and
|
||||||
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
|
changed, so that their problems will not be attributed erroneously to
|
||||||
|
authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run
|
||||||
|
modified versions of the software inside them, although the manufacturer
|
||||||
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
|
protecting users' freedom to change the software. The systematic
|
||||||
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
|
products. If such problems arise substantially in other domains, we
|
||||||
|
stand ready to extend this provision to those domains in future versions
|
||||||
|
of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents.
|
||||||
|
States should not allow patents to restrict development and use of
|
||||||
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
|
avoid the special danger that patents applied to a free program could
|
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the special requirements of the GNU Affero General Public License,
|
||||||
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU General Public License from time to time. Such new versions will
|
||||||
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program does terminal interaction, make it output a short
|
||||||
|
notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
|
<program> Copyright (C) <year> <name of author>
|
||||||
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, your program's commands
|
||||||
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
|
<http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
The GNU General Public License does not permit incorporating your program
|
||||||
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
|
may consider it more useful to permit linking proprietary applications with
|
||||||
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License. But first, please read
|
||||||
|
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||||
19
README.md
Normal file
19
README.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
## TorrServer
|
||||||
|
TorrServer, torrent to http
|
||||||
|
|
||||||
|
### Donate:
|
||||||
|
[PayPal](https://www.paypal.me/yourok)
|
||||||
|
|
||||||
|
[Yandex.Деньги](https://money.yandex.ru/to/410013733697114/100)
|
||||||
|
|
||||||
|
### Thanks to everyone who tested and helped
|
||||||
|
|
||||||
|
###### **kuzzman** [tv-box.pp.ua](http://tv-box.pp.ua/)
|
||||||
|
|
||||||
|
###### **MadAndron**
|
||||||
|
|
||||||
|
###### **SpAwN_LMG**
|
||||||
|
|
||||||
|
###### **Zivio**
|
||||||
|
|
||||||
|
###### **Tw1cker Руслан Пахнев** [github.com/Nemiroff](https://github.com/Nemiroff)
|
||||||
158
build-all.sh
Executable file
158
build-all.sh
Executable file
@@ -0,0 +1,158 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# GoLang cross-compile snippet for Go 1.6+ based loosely on Dave Chaney's cross-compile script:
|
||||||
|
# http://dave.cheney.net/2012/09/08/an-introduction-to-cross-compilation-with-go
|
||||||
|
#
|
||||||
|
# To use:
|
||||||
|
#
|
||||||
|
# $ cd ~/path-to/my-awesome-project
|
||||||
|
# $ go-build-all
|
||||||
|
#
|
||||||
|
# Features:
|
||||||
|
#
|
||||||
|
# * Cross-compiles to multiple machine types and architectures.
|
||||||
|
# * Uses the current directory name as the output name...
|
||||||
|
# * ...unless you supply an source file: $ go-build-all main.go
|
||||||
|
# * Windows binaries are named .exe.
|
||||||
|
# * ARM v5, v6, v7 and v8 (arm64) support
|
||||||
|
#
|
||||||
|
# ARM Support:
|
||||||
|
#
|
||||||
|
# You must read https://github.com/golang/go/wiki/GoArm for the specifics of running
|
||||||
|
# Linux/BSD-style kernels and what kernel modules are needed for the target platform.
|
||||||
|
# While not needed for cross-compilation of this script, you're users will need to ensure
|
||||||
|
# the correct modules are included.
|
||||||
|
#
|
||||||
|
# Requirements:
|
||||||
|
#
|
||||||
|
# * GoLang 1.6+ (for mips and ppc), 1.5 for non-mips/ppc.
|
||||||
|
# * CD to directory of the binary you are compiling. $PWD is used here.
|
||||||
|
#
|
||||||
|
# For 1.4 and earlier, see http://dave.cheney.net/2012/09/08/an-introduction-to-cross-compilation-with-go
|
||||||
|
#
|
||||||
|
|
||||||
|
# This PLATFORMS list is refreshed after every major Go release.
|
||||||
|
# Though more platforms may be supported (freebsd/386), they have been removed
|
||||||
|
# from the standard ports/downloads and therefore removed from this list.
|
||||||
|
#
|
||||||
|
PLATFORMS=""
|
||||||
|
PLATFORMS="$PLATFORMS darwin/amd64" # amd64 only as of go1.5
|
||||||
|
PLATFORMS="$PLATFORMS windows/amd64 windows/386" # arm compilation not available for Windows
|
||||||
|
PLATFORMS="$PLATFORMS linux/amd64 linux/386"
|
||||||
|
# PLATFORMS="$PLATFORMS linux/ppc64 linux/ppc64le"
|
||||||
|
PLATFORMS="$PLATFORMS linux/mips linux/mipsle linux/mips64 linux/mips64le" # experimental in go1.6
|
||||||
|
#PLATFORMS="$PLATFORMS linux/arm linux/arm64"
|
||||||
|
# PLATFORMS="$PLATFORMS freebsd/amd64"
|
||||||
|
# PLATFORMS="$PLATFORMS netbsd/amd64" # amd64 only as of go1.6
|
||||||
|
# PLATFORMS="$PLATFORMS openbsd/amd64" # amd64 only as of go1.6
|
||||||
|
# PLATFORMS="$PLATFORMS dragonfly/amd64" # amd64 only as of go1.5
|
||||||
|
# PLATFORMS="$PLATFORMS plan9/amd64 plan9/386" # as of go1.4
|
||||||
|
# PLATFORMS="$PLATFORMS solaris/amd64" # as of go1.3
|
||||||
|
|
||||||
|
# ARMBUILDS lists the platforms that are currently supported. From this list
|
||||||
|
# we generate the following architectures:
|
||||||
|
#
|
||||||
|
# ARM64 (aka ARMv8) <- only supported on linux and darwin builds (go1.6)
|
||||||
|
# ARMv7
|
||||||
|
# ARMv6
|
||||||
|
# ARMv5
|
||||||
|
#
|
||||||
|
# Some words of caution from the master:
|
||||||
|
#
|
||||||
|
# @dfc: you'll have to use gomobile to build for darwin/arm64 [and others]
|
||||||
|
# @dfc: that target expects that you're bulding for a mobile phone
|
||||||
|
# @dfc: iphone 5 and below, ARMv7, iphone 3 and below ARMv6, iphone 5s and above arm64
|
||||||
|
#
|
||||||
|
# PLATFORMS_ARM="linux freebsd netbsd"
|
||||||
|
PLATFORMS_ARM="linux"
|
||||||
|
|
||||||
|
##############################################################
|
||||||
|
# Shouldn't really need to modify anything below this line. #
|
||||||
|
##############################################################
|
||||||
|
|
||||||
|
type setopt >/dev/null 2>&1
|
||||||
|
|
||||||
|
export GOPATH="${PWD}"
|
||||||
|
|
||||||
|
SCRIPT_NAME=`basename "$0"`
|
||||||
|
FAILURES=""
|
||||||
|
SOURCE_FILE="dist/TorrServer"
|
||||||
|
CURRENT_DIRECTORY=${PWD##*/}
|
||||||
|
OUTPUT=${SOURCE_FILE:-$CURRENT_DIRECTORY} # if no src file given, use current dir name
|
||||||
|
|
||||||
|
for PLATFORM in $PLATFORMS; do
|
||||||
|
GOOS=${PLATFORM%/*}
|
||||||
|
GOARCH=${PLATFORM#*/}
|
||||||
|
BIN_FILENAME="${OUTPUT}-${GOOS}-${GOARCH}"
|
||||||
|
if [[ "${GOOS}" == "windows" ]]; then BIN_FILENAME="${BIN_FILENAME}.exe"; fi
|
||||||
|
CMD="GOOS=${GOOS} GOARCH=${GOARCH} go build -o ${BIN_FILENAME} main"
|
||||||
|
echo "${CMD}"
|
||||||
|
eval $CMD || FAILURES="${FAILURES} ${PLATFORM}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# ARM builds
|
||||||
|
if [[ $PLATFORMS_ARM == *"linux"* ]]; then
|
||||||
|
CMD="GOOS=linux GOARCH=arm64 go build -o ${OUTPUT}-linux-arm64 main"
|
||||||
|
echo "${CMD}"
|
||||||
|
eval $CMD || FAILURES="${FAILURES} ${PLATFORM}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
for GOOS in $PLATFORMS_ARM; do
|
||||||
|
GOARCH="arm"
|
||||||
|
# build for each ARM version
|
||||||
|
for GOARM in 7 6 5; do
|
||||||
|
BIN_FILENAME="${OUTPUT}-${GOOS}-${GOARCH}${GOARM}"
|
||||||
|
CMD="GOARM=${GOARM} GOOS=${GOOS} GOARCH=${GOARCH} go build -o ${BIN_FILENAME} main"
|
||||||
|
echo "${CMD}"
|
||||||
|
eval "${CMD}" || FAILURES="${FAILURES} ${GOOS}/${GOARCH}${GOARM}"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
# eval errors
|
||||||
|
if [[ "${FAILURES}" != "" ]]; then
|
||||||
|
echo ""
|
||||||
|
echo "${SCRIPT_NAME} failed on: ${FAILURES}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export CGO_ENABLED=1
|
||||||
|
export GOOS=android
|
||||||
|
export LDFLAGS="-s -w"
|
||||||
|
|
||||||
|
export NDK_TOOLCHAIN=/home/yourok/Space/Projects/GO/TorrServer/pkg/gomobile/ndk-toolchains/arm
|
||||||
|
export CC=$NDK_TOOLCHAIN/bin/arm-linux-androideabi-clang
|
||||||
|
export CXX=$NDK_TOOLCHAIN/bin/arm-linux-androideabi-clang++
|
||||||
|
export GOARCH=arm
|
||||||
|
export GOARM=7
|
||||||
|
BIN_FILENAME="dist/TorrServer-${GOOS}-${GOARCH}${GOARM}"
|
||||||
|
echo "Android ${BIN_FILENAME}"
|
||||||
|
go build -ldflags="${LDFLAGS}" -o ${BIN_FILENAME} main
|
||||||
|
|
||||||
|
export NDK_TOOLCHAIN=/home/yourok/Space/Projects/GO/TorrServer/pkg/gomobile/ndk-toolchains/arm64
|
||||||
|
export CC=$NDK_TOOLCHAIN/bin/aarch64-linux-android-clang
|
||||||
|
export CXX=$NDK_TOOLCHAIN/bin/aarch64-linux-android-clang++
|
||||||
|
export GOARCH=arm64
|
||||||
|
export GOARM=""
|
||||||
|
BIN_FILENAME="dist/TorrServer-${GOOS}-${GOARCH}${GOARM}"
|
||||||
|
echo "Android ${BIN_FILENAME}"
|
||||||
|
go build -ldflags="${LDFLAGS}" -o ${BIN_FILENAME} main
|
||||||
|
|
||||||
|
export NDK_TOOLCHAIN=/home/yourok/Space/Projects/GO/TorrServer/pkg/gomobile/ndk-toolchains/x86
|
||||||
|
export CC=$NDK_TOOLCHAIN/bin/i686-linux-android-clang
|
||||||
|
export CXX=$NDK_TOOLCHAIN/bin/i686-linux-android-clang++
|
||||||
|
export GOARCH=386
|
||||||
|
export GOARM=""
|
||||||
|
BIN_FILENAME="dist/TorrServer-${GOOS}-${GOARCH}${GOARM}"
|
||||||
|
echo "Android ${BIN_FILENAME}"
|
||||||
|
go build -ldflags="${LDFLAGS}" -o ${BIN_FILENAME} main
|
||||||
|
|
||||||
|
export NDK_TOOLCHAIN=/home/yourok/Space/Projects/GO/TorrServer/pkg/gomobile/ndk-toolchains/x86_64
|
||||||
|
export CC=$NDK_TOOLCHAIN/bin/x86_64-linux-android-clang
|
||||||
|
export CXX=$NDK_TOOLCHAIN/bin/x86_64-linux-android-clang++
|
||||||
|
export GOARCH=amd64
|
||||||
|
export GOARM=""
|
||||||
|
BIN_FILENAME="dist/TorrServer-${GOOS}-${GOARCH}${GOARM}"
|
||||||
|
echo "Android ${BIN_FILENAME}"
|
||||||
|
go build -ldflags="${LDFLAGS}" -o ${BIN_FILENAME} main
|
||||||
|
|
||||||
|
# ./compile.sh
|
||||||
7
compile_init.sh
Executable file
7
compile_init.sh
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -x
|
||||||
|
export PATH=$PATH:/usr/local/go/bin/
|
||||||
|
export GOPATH=`pwd`
|
||||||
|
export ANDROID_HOME=$HOME'/Android/Sdk'
|
||||||
|
go get golang.org/x/mobile/cmd/gomobile
|
||||||
|
./bin/gomobile init -v -ndk /home/yourok/Android/Ndk/android-ndk/
|
||||||
84
make.go
Normal file
84
make.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
fmt.Println("Need version")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
release_version := os.Args[1]
|
||||||
|
isTest := false
|
||||||
|
if len(os.Args) > 2 {
|
||||||
|
isTest = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\nMake:", release_version, "\n")
|
||||||
|
cmd := exec.Command("./build-all.sh")
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
fmt.Println("Error compile", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
files, err := ioutil.ReadDir("dist")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error read dist")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\nMake json")
|
||||||
|
js := Release{}
|
||||||
|
js.Name = "TorrServer"
|
||||||
|
js.Version = release_version
|
||||||
|
js.BuildDate = time.Now().Format("02.01.2006")
|
||||||
|
js.Links = make(map[string]string)
|
||||||
|
for _, f := range files {
|
||||||
|
arch := strings.TrimPrefix(f.Name(), "TorrServer-")
|
||||||
|
js.Links[arch] = "https://github.com/YouROK/TorrServer/releases/download/" + release_version + "/" + f.Name()
|
||||||
|
}
|
||||||
|
buf, err := json.MarshalIndent(&js, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error make json")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if isTest {
|
||||||
|
err = ioutil.WriteFile("test.json", buf, 0666)
|
||||||
|
} else {
|
||||||
|
err = ioutil.WriteFile("release.json", buf, 0666)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error write to json file:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println("\n\nEnter tag manually:\n")
|
||||||
|
fmt.Println("git push origin", release_version)
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
type Release struct {
|
||||||
|
Name string
|
||||||
|
Version string
|
||||||
|
BuildDate string
|
||||||
|
Links map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
//"update": {
|
||||||
|
//"name": "TorrServer",
|
||||||
|
//"version": "1.0.61",
|
||||||
|
//"build_date": "20.07.2018",
|
||||||
|
//"links":[
|
||||||
|
//{"android-386":"https://github.com/YouROK/TorrServe/releases/download/1.0.61/TorrServer-android-386"},
|
||||||
|
//{"android-amd64":"https://github.com/YouROK/TorrServe/releases/download/1.0.61/TorrServer-android-amd64"},
|
||||||
|
//{"android-arm7":"https://github.com/YouROK/TorrServe/releases/download/1.0.61/TorrServer-android-arm7"},
|
||||||
|
//{"android-arm64":"https://github.com/YouROK/TorrServe/releases/download/1.0.61/TorrServer-android-arm64"},
|
||||||
|
//]
|
||||||
|
//}
|
||||||
3
make_release.sh
Executable file
3
make_release.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
go run make.go $@
|
||||||
24
release.json
Normal file
24
release.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"Name": "TorrServer",
|
||||||
|
"Version": "1.1.65",
|
||||||
|
"BuildDate": "29.08.2018",
|
||||||
|
"Links": {
|
||||||
|
"android-386": "https://github.com/YouROK/TorrServer/releases/download/1.1.65/TorrServer-android-386",
|
||||||
|
"android-amd64": "https://github.com/YouROK/TorrServer/releases/download/1.1.65/TorrServer-android-amd64",
|
||||||
|
"android-arm64": "https://github.com/YouROK/TorrServer/releases/download/1.1.65/TorrServer-android-arm64",
|
||||||
|
"android-arm7": "https://github.com/YouROK/TorrServer/releases/download/1.1.65/TorrServer-android-arm7",
|
||||||
|
"darwin-amd64": "https://github.com/YouROK/TorrServer/releases/download/1.1.65/TorrServer-darwin-amd64",
|
||||||
|
"linux-386": "https://github.com/YouROK/TorrServer/releases/download/1.1.65/TorrServer-linux-386",
|
||||||
|
"linux-amd64": "https://github.com/YouROK/TorrServer/releases/download/1.1.65/TorrServer-linux-amd64",
|
||||||
|
"linux-arm5": "https://github.com/YouROK/TorrServer/releases/download/1.1.65/TorrServer-linux-arm5",
|
||||||
|
"linux-arm6": "https://github.com/YouROK/TorrServer/releases/download/1.1.65/TorrServer-linux-arm6",
|
||||||
|
"linux-arm64": "https://github.com/YouROK/TorrServer/releases/download/1.1.65/TorrServer-linux-arm64",
|
||||||
|
"linux-arm7": "https://github.com/YouROK/TorrServer/releases/download/1.1.65/TorrServer-linux-arm7",
|
||||||
|
"linux-mips": "https://github.com/YouROK/TorrServer/releases/download/1.1.65/TorrServer-linux-mips",
|
||||||
|
"linux-mips64": "https://github.com/YouROK/TorrServer/releases/download/1.1.65/TorrServer-linux-mips64",
|
||||||
|
"linux-mips64le": "https://github.com/YouROK/TorrServer/releases/download/1.1.65/TorrServer-linux-mips64le",
|
||||||
|
"linux-mipsle": "https://github.com/YouROK/TorrServer/releases/download/1.1.65/TorrServer-linux-mipsle",
|
||||||
|
"windows-386.exe": "https://github.com/YouROK/TorrServer/releases/download/1.1.65/TorrServer-windows-386.exe",
|
||||||
|
"windows-amd64.exe": "https://github.com/YouROK/TorrServer/releases/download/1.1.65/TorrServer-windows-amd64.exe"
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/main/main.go
Normal file
90
src/main/main.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alexflint/go-arg"
|
||||||
|
"server"
|
||||||
|
"server/settings"
|
||||||
|
"server/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
Port string `arg:"-p" help:"web server port"`
|
||||||
|
Path string `arg:"-d" help:"database path"`
|
||||||
|
Add string `arg:"-a" help:"add torrent link and exit"`
|
||||||
|
Kill bool `arg:"-k" help:"dont kill program on signal"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (args) Version() string {
|
||||||
|
return "TorrServer " + version.Version
|
||||||
|
}
|
||||||
|
|
||||||
|
var params args
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
//test()
|
||||||
|
//return
|
||||||
|
//for _, g := range tmdb.GetMovieGenres("ru") {
|
||||||
|
// fmt.Println(g.Name, g.ID)
|
||||||
|
//}
|
||||||
|
//return
|
||||||
|
|
||||||
|
//movs, _ := tmdb.DiscoverShows(map[string]string{}, 1)
|
||||||
|
//js, _ := json.MarshalIndent(movs, "", " ")
|
||||||
|
//fmt.Println(string(js))
|
||||||
|
//return
|
||||||
|
|
||||||
|
arg.MustParse(¶ms)
|
||||||
|
|
||||||
|
if params.Path == "" {
|
||||||
|
params.Path, _ = os.Getwd()
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.Port == "" {
|
||||||
|
params.Port = "8090"
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.Add != "" {
|
||||||
|
add()
|
||||||
|
}
|
||||||
|
|
||||||
|
Preconfig(params.Kill)
|
||||||
|
|
||||||
|
server.Start(params.Path, params.Port)
|
||||||
|
settings.SaveSettings()
|
||||||
|
fmt.Println(server.WaitServer())
|
||||||
|
time.Sleep(time.Second * 3)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func add() {
|
||||||
|
err := addRemote()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error add torrent:", err)
|
||||||
|
os.Exit(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Added ok")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addRemote() error {
|
||||||
|
url := "http://localhost:" + params.Port + "/torrent/add"
|
||||||
|
fmt.Println("Add torrent link:", params.Add, "\n", url)
|
||||||
|
|
||||||
|
json := `{"Link":"` + params.Add + `"}`
|
||||||
|
resp, err := http.Post(url, "text/html; charset=utf-8", bytes.NewBufferString(json))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return errors.New(resp.Status)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
49
src/main/preconfig_pos.go
Normal file
49
src/main/preconfig_pos.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Preconfig(kill bool) {
|
||||||
|
if kill {
|
||||||
|
sigc := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigc,
|
||||||
|
syscall.SIGHUP,
|
||||||
|
syscall.SIGINT,
|
||||||
|
syscall.SIGSTOP,
|
||||||
|
syscall.SIGPIPE,
|
||||||
|
syscall.SIGTERM,
|
||||||
|
syscall.SIGQUIT)
|
||||||
|
go func() {
|
||||||
|
for s := range sigc {
|
||||||
|
fmt.Println("Signal catched:", s)
|
||||||
|
fmt.Println("For stop server, close in web")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
//dns resover
|
||||||
|
addrs, err := net.LookupHost("www.themoviedb.org")
|
||||||
|
if len(addrs) == 0 {
|
||||||
|
fmt.Println("Check dns", addrs, err)
|
||||||
|
|
||||||
|
fn := func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||||
|
d := net.Dialer{}
|
||||||
|
return d.DialContext(ctx, "udp", "1.1.1.1:53")
|
||||||
|
}
|
||||||
|
|
||||||
|
net.DefaultResolver = &net.Resolver{
|
||||||
|
Dial: fn,
|
||||||
|
}
|
||||||
|
|
||||||
|
addrs, err = net.LookupHost("www.themoviedb.org")
|
||||||
|
fmt.Println("Check new dns", addrs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/main/preconfig_win.go
Normal file
7
src/main/preconfig_win.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// +build windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
func Preconfig(kill bool) {
|
||||||
|
|
||||||
|
}
|
||||||
74
src/main/test.go
Normal file
74
src/main/test.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"server/utils"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent"
|
||||||
|
)
|
||||||
|
|
||||||
|
func test() {
|
||||||
|
config := torrent.NewDefaultClientConfig()
|
||||||
|
|
||||||
|
config.EstablishedConnsPerTorrent = 100
|
||||||
|
config.HalfOpenConnsPerTorrent = 65
|
||||||
|
config.DisableIPv6 = true
|
||||||
|
config.NoDHT = true
|
||||||
|
|
||||||
|
client, err := torrent.NewClient(config)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Ubuntu
|
||||||
|
t, err := client.AddMagnet("magnet:?xt=urn:btih:e4be9e4db876e3e3179778b03e906297be5c8dbe&dn=ubuntu-18.04-desktop-amd64.iso&tr=http%3a%2f%2ftorrent.ubuntu.com%3a6969%2fannounce&tr=http%3a%2f%2fipv6.torrent.ubuntu.com%3a6969%2fannounce")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
<-t.GotInfo()
|
||||||
|
file := t.Files()[0]
|
||||||
|
|
||||||
|
reader := file.NewReader()
|
||||||
|
var wa sync.WaitGroup
|
||||||
|
wa.Add(1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
buf := make([]byte, t.Info().PieceLength)
|
||||||
|
for {
|
||||||
|
_, err := reader.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wa.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
cl := t.Closed()
|
||||||
|
lastTimeSpeed := time.Now()
|
||||||
|
DownloadSpeed := 0.0
|
||||||
|
BytesReadUsefulData := int64(0)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-cl:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
client.WriteStatus(os.Stdout)
|
||||||
|
st := t.Stats()
|
||||||
|
deltaDlBytes := st.BytesReadUsefulData.Int64() - BytesReadUsefulData
|
||||||
|
deltaTime := time.Since(lastTimeSpeed).Seconds()
|
||||||
|
DownloadSpeed = float64(deltaDlBytes) / deltaTime
|
||||||
|
BytesReadUsefulData = st.BytesReadUsefulData.Int64()
|
||||||
|
lastTimeSpeed = time.Now()
|
||||||
|
fmt.Println("DL speed:", utils.Format(DownloadSpeed))
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
wa.Wait()
|
||||||
|
}
|
||||||
33
src/server/Server.go
Normal file
33
src/server/Server.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"server/settings"
|
||||||
|
"server/web"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Start(settingsPath, port string) {
|
||||||
|
settings.Path = settingsPath
|
||||||
|
err := settings.ReadSettings()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error read settings:", err)
|
||||||
|
}
|
||||||
|
if port == "" {
|
||||||
|
port = "8090"
|
||||||
|
}
|
||||||
|
server.Start(port)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WaitServer() string {
|
||||||
|
err := server.Wait()
|
||||||
|
if err != nil {
|
||||||
|
return err.Error()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func Stop() {
|
||||||
|
go server.Stop()
|
||||||
|
settings.CloseDB()
|
||||||
|
}
|
||||||
52
src/server/settings/DB.go
Normal file
52
src/server/settings/DB.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/boltdb/bolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
db *bolt.DB
|
||||||
|
dbInfosName = []byte("Infos")
|
||||||
|
dbTorrentsName = []byte("Torrents")
|
||||||
|
dbSettingsName = []byte("Settings")
|
||||||
|
Path string
|
||||||
|
)
|
||||||
|
|
||||||
|
func openDB() error {
|
||||||
|
if db != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
db, err = bolt.Open(filepath.Join(Path, "torrserver.db"), 0666, nil)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Print(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Update(func(tx *bolt.Tx) error {
|
||||||
|
_, err = tx.CreateBucketIfNotExists(dbSettingsName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not create Settings bucket: %v", err)
|
||||||
|
}
|
||||||
|
_, err = tx.CreateBucketIfNotExists(dbTorrentsName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not create Torrents bucket: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
CloseDB()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func CloseDB() {
|
||||||
|
if db != nil {
|
||||||
|
db.Close()
|
||||||
|
db = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
61
src/server/settings/Info.go
Normal file
61
src/server/settings/Info.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/boltdb/bolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AddInfo(hash, info string) error {
|
||||||
|
err := openDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hash = strings.ToUpper(hash)
|
||||||
|
return db.Update(func(tx *bolt.Tx) error {
|
||||||
|
dbt, err := tx.CreateBucketIfNotExists([]byte(dbInfosName))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dbi, err := dbt.CreateBucketIfNotExists([]byte(hash))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = dbi.Put([]byte("Info"), []byte(info))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error save torrent info %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetInfo(hash string) string {
|
||||||
|
err := openDB()
|
||||||
|
if err != nil {
|
||||||
|
return "{}"
|
||||||
|
}
|
||||||
|
|
||||||
|
hash = strings.ToUpper(hash)
|
||||||
|
ret := "{}"
|
||||||
|
err = db.View(func(tx *bolt.Tx) error {
|
||||||
|
hdb := tx.Bucket(dbInfosName)
|
||||||
|
if hdb == nil {
|
||||||
|
return fmt.Errorf("could not find torrent info")
|
||||||
|
}
|
||||||
|
hdb = hdb.Bucket([]byte(hash))
|
||||||
|
if hdb != nil {
|
||||||
|
info := hdb.Get([]byte("Info"))
|
||||||
|
if info == nil {
|
||||||
|
return fmt.Errorf("error get torrent info")
|
||||||
|
}
|
||||||
|
ret = string(info)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return ret
|
||||||
|
}
|
||||||
102
src/server/settings/Settings.go
Normal file
102
src/server/settings/Settings.go
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/boltdb/bolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
sets *Settings
|
||||||
|
StartTime time.Time
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
sets = new(Settings)
|
||||||
|
sets.CacheSize = 200 * 1024 * 1024
|
||||||
|
sets.PreloadBufferSize = 20 * 1024 * 1024
|
||||||
|
sets.ConnectionsLimit = 100
|
||||||
|
sets.RetrackersMode = 1
|
||||||
|
sets.DisableDHT = true
|
||||||
|
StartTime = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
type Settings struct {
|
||||||
|
CacheSize int64 // in byte, def 200 mb
|
||||||
|
PreloadBufferSize int64 // in byte, buffer for preload
|
||||||
|
|
||||||
|
RetrackersMode int //0 - don`t add, 1 - add retrackers, 2 - remove retrackers
|
||||||
|
|
||||||
|
//BT Config
|
||||||
|
DisableTCP bool
|
||||||
|
DisableUTP bool
|
||||||
|
DisableUPNP bool
|
||||||
|
DisableDHT bool
|
||||||
|
DisableUpload bool
|
||||||
|
Encryption int // 0 - Enable, 1 - disable, 2 - force
|
||||||
|
DownloadRateLimit int // in kb, 0 - inf
|
||||||
|
UploadRateLimit int // in kb, 0 - inf
|
||||||
|
ConnectionsLimit int
|
||||||
|
}
|
||||||
|
|
||||||
|
func Get() *Settings {
|
||||||
|
return sets
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Settings) String() string {
|
||||||
|
buf, _ := json.MarshalIndent(sets, "", " ")
|
||||||
|
return string(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadSettings() error {
|
||||||
|
err := openDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
buf := make([]byte, 0)
|
||||||
|
err = db.View(func(tx *bolt.Tx) error {
|
||||||
|
sdb := tx.Bucket(dbSettingsName)
|
||||||
|
if sdb == nil {
|
||||||
|
return fmt.Errorf("error load settings")
|
||||||
|
}
|
||||||
|
|
||||||
|
buf = sdb.Get([]byte("json"))
|
||||||
|
if buf == nil {
|
||||||
|
return fmt.Errorf("error load settings")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
err = json.Unmarshal(buf, sets)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if sets.ConnectionsLimit <= 0 {
|
||||||
|
sets.ConnectionsLimit = 50
|
||||||
|
}
|
||||||
|
if sets.CacheSize <= 0 {
|
||||||
|
sets.CacheSize = 200 * 1024 * 1024
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveSettings() error {
|
||||||
|
err := openDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := json.Marshal(sets)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.Update(func(tx *bolt.Tx) error {
|
||||||
|
setsDB, err := tx.CreateBucketIfNotExists(dbSettingsName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return setsDB.Put([]byte("json"), []byte(buf))
|
||||||
|
})
|
||||||
|
}
|
||||||
292
src/server/settings/Torrent.go
Normal file
292
src/server/settings/Torrent.go
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/boltdb/bolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Torrent struct {
|
||||||
|
Name string
|
||||||
|
Magnet string
|
||||||
|
Hash string
|
||||||
|
Size int64
|
||||||
|
Timestamp int64
|
||||||
|
|
||||||
|
Files []File
|
||||||
|
}
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
Name string
|
||||||
|
Size int64
|
||||||
|
Viewed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetViewed(hash, filename string) error {
|
||||||
|
err := openDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.Update(func(tx *bolt.Tx) error {
|
||||||
|
dbt := tx.Bucket(dbTorrentsName)
|
||||||
|
if dbt == nil {
|
||||||
|
return fmt.Errorf("could not find torrent")
|
||||||
|
}
|
||||||
|
hdb := dbt.Bucket([]byte(hash))
|
||||||
|
if hdb == nil {
|
||||||
|
return fmt.Errorf("could not find torrent")
|
||||||
|
}
|
||||||
|
|
||||||
|
fdb := hdb.Bucket([]byte("Files"))
|
||||||
|
if fdb == nil {
|
||||||
|
return fmt.Errorf("could not find torrent")
|
||||||
|
}
|
||||||
|
|
||||||
|
fdb = fdb.Bucket([]byte(filename))
|
||||||
|
if fdb == nil {
|
||||||
|
return fmt.Errorf("could not find torrent")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = fdb.Put([]byte("Viewed"), []byte{1})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error save torrent %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveTorrentDB(torrent *Torrent) error {
|
||||||
|
err := openDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.Update(func(tx *bolt.Tx) error {
|
||||||
|
dbt, err := tx.CreateBucketIfNotExists(dbTorrentsName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not create Torrents bucket: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println("Save torrent:", torrent.Name)
|
||||||
|
hdb, err := dbt.CreateBucketIfNotExists([]byte(torrent.Hash))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not create Torrent bucket: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = hdb.Put([]byte("Name"), []byte(torrent.Name))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error save torrent: %v", err)
|
||||||
|
}
|
||||||
|
err = hdb.Put([]byte("Link"), []byte(torrent.Magnet))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error save torrent: %v", err)
|
||||||
|
}
|
||||||
|
err = hdb.Put([]byte("Size"), i2b(torrent.Size))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error save torrent: %v", err)
|
||||||
|
}
|
||||||
|
err = hdb.Put([]byte("Timestamp"), i2b(torrent.Timestamp))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error save torrent: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fdb, err := hdb.CreateBucketIfNotExists([]byte("Files"))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error save torrent files: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range torrent.Files {
|
||||||
|
ffdb, err := fdb.CreateBucketIfNotExists([]byte(f.Name))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error save torrent files: %v", err)
|
||||||
|
}
|
||||||
|
err = ffdb.Put([]byte("Size"), i2b(f.Size))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error save torrent files: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b := 0
|
||||||
|
if f.Viewed {
|
||||||
|
b = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ffdb.Put([]byte("Viewed"), []byte{byte(b)})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error save torrent files: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func RemoveTorrentDB(hash string) error {
|
||||||
|
err := openDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.Update(func(tx *bolt.Tx) error {
|
||||||
|
dbt := tx.Bucket(dbTorrentsName)
|
||||||
|
if dbt == nil {
|
||||||
|
return fmt.Errorf("could not find torrent")
|
||||||
|
}
|
||||||
|
|
||||||
|
return dbt.DeleteBucket([]byte(hash))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadTorrentDB(hash string) (*Torrent, error) {
|
||||||
|
err := openDB()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var torr *Torrent
|
||||||
|
err = db.View(func(tx *bolt.Tx) error {
|
||||||
|
hdb := tx.Bucket(dbTorrentsName)
|
||||||
|
if hdb == nil {
|
||||||
|
return fmt.Errorf("could not find torrent")
|
||||||
|
}
|
||||||
|
hdb = hdb.Bucket([]byte(hash))
|
||||||
|
if hdb != nil {
|
||||||
|
torr = new(Torrent)
|
||||||
|
torr.Hash = string(hash)
|
||||||
|
tmp := hdb.Get([]byte("Name"))
|
||||||
|
if tmp == nil {
|
||||||
|
return fmt.Errorf("error load torrent")
|
||||||
|
}
|
||||||
|
torr.Name = string(tmp)
|
||||||
|
|
||||||
|
tmp = hdb.Get([]byte("Link"))
|
||||||
|
if tmp == nil {
|
||||||
|
return fmt.Errorf("error load torrent")
|
||||||
|
}
|
||||||
|
torr.Magnet = string(tmp)
|
||||||
|
|
||||||
|
tmp = hdb.Get([]byte("Size"))
|
||||||
|
if tmp == nil {
|
||||||
|
return fmt.Errorf("error load torrent")
|
||||||
|
}
|
||||||
|
torr.Size = b2i(tmp)
|
||||||
|
|
||||||
|
tmp = hdb.Get([]byte("Timestamp"))
|
||||||
|
if tmp == nil {
|
||||||
|
return fmt.Errorf("error load torrent")
|
||||||
|
}
|
||||||
|
torr.Timestamp = b2i(tmp)
|
||||||
|
|
||||||
|
fdb := hdb.Bucket([]byte("Files"))
|
||||||
|
if fdb == nil {
|
||||||
|
return fmt.Errorf("error load torrent files")
|
||||||
|
}
|
||||||
|
cf := fdb.Cursor()
|
||||||
|
for fn, _ := cf.First(); fn != nil; fn, _ = cf.Next() {
|
||||||
|
file := File{Name: string(fn)}
|
||||||
|
ffdb := fdb.Bucket(fn)
|
||||||
|
if ffdb == nil {
|
||||||
|
return fmt.Errorf("error load torrent files")
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp := ffdb.Get([]byte("Size"))
|
||||||
|
if tmp == nil {
|
||||||
|
return fmt.Errorf("error load torrent file")
|
||||||
|
}
|
||||||
|
file.Size = b2i(tmp)
|
||||||
|
|
||||||
|
tmp = ffdb.Get([]byte("Viewed"))
|
||||||
|
if tmp == nil {
|
||||||
|
return fmt.Errorf("error load torrent file")
|
||||||
|
}
|
||||||
|
file.Viewed = len(tmp) > 0 && tmp[0] == 1
|
||||||
|
torr.Files = append(torr.Files, file)
|
||||||
|
}
|
||||||
|
SortFiles(torr.Files)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return torr, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadTorrentsDB() ([]*Torrent, error) {
|
||||||
|
err := openDB()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
torrs := make([]*Torrent, 0)
|
||||||
|
err = db.View(func(tx *bolt.Tx) error {
|
||||||
|
tdb := tx.Bucket(dbTorrentsName)
|
||||||
|
c := tdb.Cursor()
|
||||||
|
for h, _ := c.First(); h != nil; h, _ = c.Next() {
|
||||||
|
hdb := tdb.Bucket(h)
|
||||||
|
if hdb != nil {
|
||||||
|
torr := new(Torrent)
|
||||||
|
torr.Hash = string(h)
|
||||||
|
tmp := hdb.Get([]byte("Name"))
|
||||||
|
if tmp == nil {
|
||||||
|
return fmt.Errorf("error load torrent")
|
||||||
|
}
|
||||||
|
torr.Name = string(tmp)
|
||||||
|
|
||||||
|
tmp = hdb.Get([]byte("Link"))
|
||||||
|
if tmp == nil {
|
||||||
|
return fmt.Errorf("error load torrent")
|
||||||
|
}
|
||||||
|
torr.Magnet = string(tmp)
|
||||||
|
|
||||||
|
tmp = hdb.Get([]byte("Size"))
|
||||||
|
if tmp == nil {
|
||||||
|
return fmt.Errorf("error load torrent")
|
||||||
|
}
|
||||||
|
torr.Size = b2i(tmp)
|
||||||
|
|
||||||
|
tmp = hdb.Get([]byte("Timestamp"))
|
||||||
|
if tmp == nil {
|
||||||
|
return fmt.Errorf("error load torrent")
|
||||||
|
}
|
||||||
|
torr.Timestamp = b2i(tmp)
|
||||||
|
|
||||||
|
fdb := hdb.Bucket([]byte("Files"))
|
||||||
|
if fdb == nil {
|
||||||
|
return fmt.Errorf("error load torrent files")
|
||||||
|
}
|
||||||
|
cf := fdb.Cursor()
|
||||||
|
for fn, _ := cf.First(); fn != nil; fn, _ = cf.Next() {
|
||||||
|
file := File{Name: string(fn)}
|
||||||
|
ffdb := fdb.Bucket(fn)
|
||||||
|
if ffdb == nil {
|
||||||
|
return fmt.Errorf("error load torrent files")
|
||||||
|
}
|
||||||
|
tmp := ffdb.Get([]byte("Size"))
|
||||||
|
if tmp == nil {
|
||||||
|
return fmt.Errorf("error load torrent file")
|
||||||
|
}
|
||||||
|
file.Size = b2i(tmp)
|
||||||
|
|
||||||
|
tmp = ffdb.Get([]byte("Viewed"))
|
||||||
|
if tmp == nil {
|
||||||
|
return fmt.Errorf("error load torrent file")
|
||||||
|
}
|
||||||
|
file.Viewed = len(tmp) > 0 && tmp[0] == 1
|
||||||
|
torr.Files = append(torr.Files, file)
|
||||||
|
}
|
||||||
|
SortFiles(torr.Files)
|
||||||
|
torrs = append(torrs, torr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return torrs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func i2b(v int64) []byte {
|
||||||
|
b := make([]byte, 8)
|
||||||
|
binary.BigEndian.PutUint64(b, uint64(v))
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func b2i(v []byte) int64 {
|
||||||
|
return int64(binary.BigEndian.Uint64(v))
|
||||||
|
}
|
||||||
88
src/server/settings/UsableFiles.go
Normal file
88
src/server/settings/UsableFiles.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
uFiles = map[string]interface{}{
|
||||||
|
".3g2": nil,
|
||||||
|
".3gp": nil,
|
||||||
|
".aaf": nil,
|
||||||
|
".asf": nil,
|
||||||
|
".avchd": nil,
|
||||||
|
".avi": nil,
|
||||||
|
".drc": nil,
|
||||||
|
".flv": nil,
|
||||||
|
".m2v": nil,
|
||||||
|
".m4p": nil,
|
||||||
|
".m4v": nil,
|
||||||
|
".mkv": nil,
|
||||||
|
".mng": nil,
|
||||||
|
".mov": nil,
|
||||||
|
".mp2": nil,
|
||||||
|
".mp4": nil,
|
||||||
|
".mpe": nil,
|
||||||
|
".mpeg": nil,
|
||||||
|
".mpg": nil,
|
||||||
|
".mpv": nil,
|
||||||
|
".mxf": nil,
|
||||||
|
".nsv": nil,
|
||||||
|
".ogg": nil,
|
||||||
|
".ogv": nil,
|
||||||
|
".ts": nil,
|
||||||
|
".m2ts": nil,
|
||||||
|
".mts": nil,
|
||||||
|
".qt": nil,
|
||||||
|
".rm": nil,
|
||||||
|
".rmvb": nil,
|
||||||
|
".roq": nil,
|
||||||
|
".svi": nil,
|
||||||
|
".vob": nil,
|
||||||
|
".webm": nil,
|
||||||
|
".wmv": nil,
|
||||||
|
".yuv": nil,
|
||||||
|
|
||||||
|
".aac": nil,
|
||||||
|
".aiff": nil,
|
||||||
|
".ape": nil,
|
||||||
|
".au": nil,
|
||||||
|
".flac": nil,
|
||||||
|
".gsm": nil,
|
||||||
|
".it": nil,
|
||||||
|
".m3u": nil,
|
||||||
|
".m4a": nil,
|
||||||
|
".mid": nil,
|
||||||
|
".mod": nil,
|
||||||
|
".mp3": nil,
|
||||||
|
".mpa": nil,
|
||||||
|
".pls": nil,
|
||||||
|
".ra": nil,
|
||||||
|
".s3m": nil,
|
||||||
|
".sid": nil,
|
||||||
|
".wav": nil,
|
||||||
|
".wma": nil,
|
||||||
|
".xm": nil,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func SortFiles(files []File) {
|
||||||
|
sort.Slice(files, func(i, j int) bool {
|
||||||
|
if haveUsable(files[i].Name) && !haveUsable(files[j].Name) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if !haveUsable(files[i].Name) && haveUsable(files[j].Name) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return files[i].Name < files[j].Name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func haveUsable(name string) bool {
|
||||||
|
ext := strings.ToLower(filepath.Ext(name))
|
||||||
|
_, ok := uFiles[ext]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
182
src/server/torr/BTServer.go
Normal file
182
src/server/torr/BTServer.go
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
package torr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"server/settings"
|
||||||
|
"server/torr/storage"
|
||||||
|
"server/torr/storage/memcache"
|
||||||
|
"server/torr/storage/state"
|
||||||
|
"server/utils"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent"
|
||||||
|
"github.com/anacrolix/torrent/iplist"
|
||||||
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BTServer struct {
|
||||||
|
config *torrent.ClientConfig
|
||||||
|
client *torrent.Client
|
||||||
|
|
||||||
|
storage storage.Storage
|
||||||
|
|
||||||
|
torrents map[metainfo.Hash]*Torrent
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
wmu sync.Mutex
|
||||||
|
|
||||||
|
watching bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBTS() *BTServer {
|
||||||
|
bts := new(BTServer)
|
||||||
|
bts.torrents = make(map[metainfo.Hash]*Torrent)
|
||||||
|
return bts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bt *BTServer) Connect() error {
|
||||||
|
bt.mu.Lock()
|
||||||
|
defer bt.mu.Unlock()
|
||||||
|
var err error
|
||||||
|
bt.configure()
|
||||||
|
bt.client, err = torrent.NewClient(bt.config)
|
||||||
|
bt.torrents = make(map[metainfo.Hash]*Torrent)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bt *BTServer) Disconnect() {
|
||||||
|
bt.mu.Lock()
|
||||||
|
defer bt.mu.Unlock()
|
||||||
|
if bt.client != nil {
|
||||||
|
bt.client.Close()
|
||||||
|
bt.client = nil
|
||||||
|
utils.FreeOSMemGC()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bt *BTServer) Reconnect() error {
|
||||||
|
bt.Disconnect()
|
||||||
|
return bt.Connect()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bt *BTServer) configure() {
|
||||||
|
bt.storage = memcache.NewStorage(settings.Get().CacheSize)
|
||||||
|
|
||||||
|
blocklist, _ := iplist.MMapPackedFile(filepath.Join(settings.Path, "blocklist"))
|
||||||
|
|
||||||
|
userAgent := "uTorrent/3.4.9"
|
||||||
|
peerID := "-UT3490-"
|
||||||
|
|
||||||
|
bt.config = torrent.NewDefaultClientConfig()
|
||||||
|
|
||||||
|
bt.config.DisableIPv6 = true
|
||||||
|
bt.config.DisableTCP = settings.Get().DisableTCP
|
||||||
|
bt.config.DisableUTP = settings.Get().DisableUTP
|
||||||
|
bt.config.NoDefaultPortForwarding = settings.Get().DisableUPNP
|
||||||
|
bt.config.NoDHT = settings.Get().DisableDHT
|
||||||
|
bt.config.NoUpload = settings.Get().DisableUpload
|
||||||
|
bt.config.EncryptionPolicy = torrent.EncryptionPolicy{
|
||||||
|
DisableEncryption: settings.Get().Encryption == 1,
|
||||||
|
ForceEncryption: settings.Get().Encryption == 2,
|
||||||
|
}
|
||||||
|
bt.config.IPBlocklist = blocklist
|
||||||
|
bt.config.DefaultStorage = bt.storage
|
||||||
|
bt.config.Bep20 = peerID
|
||||||
|
bt.config.PeerID = utils.PeerIDRandom(peerID)
|
||||||
|
bt.config.HTTPUserAgent = userAgent
|
||||||
|
bt.config.EstablishedConnsPerTorrent = settings.Get().ConnectionsLimit
|
||||||
|
|
||||||
|
bt.config.TorrentPeersHighWater = 3000
|
||||||
|
bt.config.HalfOpenConnsPerTorrent = 50
|
||||||
|
|
||||||
|
if settings.Get().DownloadRateLimit > 0 {
|
||||||
|
bt.config.DownloadRateLimiter = utils.Limit(settings.Get().DownloadRateLimit * 1024)
|
||||||
|
}
|
||||||
|
if settings.Get().UploadRateLimit > 0 {
|
||||||
|
bt.config.UploadRateLimiter = utils.Limit(settings.Get().UploadRateLimit * 1024)
|
||||||
|
}
|
||||||
|
|
||||||
|
//bt.config.Debug = true
|
||||||
|
|
||||||
|
fmt.Println("Configure client:", settings.Get())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bt *BTServer) AddTorrent(magnet metainfo.Magnet, onAdd func(*Torrent)) (*Torrent, error) {
|
||||||
|
torr, err := NewTorrent(magnet, bt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if onAdd != nil {
|
||||||
|
go func() {
|
||||||
|
if torr.GotInfo() {
|
||||||
|
onAdd(torr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
} else {
|
||||||
|
go torr.GotInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
return torr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bt *BTServer) List() []*Torrent {
|
||||||
|
bt.mu.Lock()
|
||||||
|
defer bt.mu.Unlock()
|
||||||
|
list := make([]*Torrent, 0)
|
||||||
|
for _, t := range bt.torrents {
|
||||||
|
list = append(list, t)
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bt *BTServer) GetTorrent(hash metainfo.Hash) *Torrent {
|
||||||
|
bt.mu.Lock()
|
||||||
|
defer bt.mu.Unlock()
|
||||||
|
|
||||||
|
if t, ok := bt.torrents[hash]; ok {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bt *BTServer) RemoveTorrent(hash torrent.InfoHash) {
|
||||||
|
if torr, ok := bt.torrents[hash]; ok {
|
||||||
|
torr.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bt *BTServer) BTState() *BTState {
|
||||||
|
bt.mu.Lock()
|
||||||
|
defer bt.mu.Unlock()
|
||||||
|
|
||||||
|
btState := new(BTState)
|
||||||
|
btState.LocalPort = bt.client.LocalPort()
|
||||||
|
btState.PeerID = fmt.Sprintf("%x", bt.client.PeerID())
|
||||||
|
btState.BannedIPs = len(bt.client.BadPeerIPs())
|
||||||
|
for _, dht := range bt.client.DhtServers() {
|
||||||
|
btState.DHTs = append(btState.DHTs, dht)
|
||||||
|
}
|
||||||
|
for _, t := range bt.torrents {
|
||||||
|
btState.Torrents = append(btState.Torrents, t)
|
||||||
|
}
|
||||||
|
return btState
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bt *BTServer) CacheState(hash metainfo.Hash) *state.CacheState {
|
||||||
|
st := bt.GetTorrent(hash)
|
||||||
|
if st == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheState := bt.storage.GetStats(hash)
|
||||||
|
return cacheState
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bt *BTServer) WriteState(w io.Writer) {
|
||||||
|
bt.client.WriteStatus(w)
|
||||||
|
}
|
||||||
51
src/server/torr/Play.go
Normal file
51
src/server/torr/Play.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package torr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"server/settings"
|
||||||
|
"server/utils"
|
||||||
|
|
||||||
|
"github.com/anacrolix/missinggo/httptoo"
|
||||||
|
"github.com/anacrolix/torrent"
|
||||||
|
"github.com/labstack/echo"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (bt *BTServer) View(torr *Torrent, file *torrent.File, c echo.Context) error {
|
||||||
|
go settings.SetViewed(torr.Hash().HexString(), file.Path())
|
||||||
|
reader := torr.NewReader(file, 0)
|
||||||
|
|
||||||
|
fmt.Println("Connect reader:", len(torr.readers))
|
||||||
|
c.Response().Header().Set("Connection", "close")
|
||||||
|
c.Response().Header().Set("ETag", httptoo.EncodeQuotedString(fmt.Sprintf("%s/%s", torr.Hash().HexString(), file.Path())))
|
||||||
|
|
||||||
|
http.ServeContent(c.Response(), c.Request(), file.Path(), time.Time{}, reader)
|
||||||
|
|
||||||
|
fmt.Println("Disconnect reader:", len(torr.readers))
|
||||||
|
torr.CloseReader(reader)
|
||||||
|
return c.NoContent(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bt *BTServer) Play(torr *Torrent, file *torrent.File, preload int64, c echo.Context) error {
|
||||||
|
if torr.status == TorrentAdded {
|
||||||
|
if !torr.GotInfo() {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "torrent closed befor get info")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if torr.status == TorrentGettingInfo {
|
||||||
|
if !torr.WaitInfo() {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "torrent closed befor get info")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if torr.PreloadedBytes == 0 {
|
||||||
|
torr.Preload(file, preload)
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectUrl := c.Scheme() + "://" + c.Request().Host + "/torrent/view/" + torr.Hash().HexString() + "/" + utils.CleanFName(file.Path())
|
||||||
|
return c.Redirect(http.StatusFound, redirectUrl)
|
||||||
|
|
||||||
|
//return bt.View(torr, file, c)
|
||||||
|
}
|
||||||
57
src/server/torr/State.go
Normal file
57
src/server/torr/State.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package torr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/anacrolix/dht"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BTState struct {
|
||||||
|
LocalPort int
|
||||||
|
PeerID string
|
||||||
|
BannedIPs int
|
||||||
|
DHTs []*dht.Server
|
||||||
|
|
||||||
|
Torrents []*Torrent
|
||||||
|
}
|
||||||
|
|
||||||
|
type TorrentStats struct {
|
||||||
|
Name string
|
||||||
|
Hash string
|
||||||
|
|
||||||
|
TorrentStatus TorrentStatus
|
||||||
|
TorrentStatusString string
|
||||||
|
|
||||||
|
LoadedSize int64
|
||||||
|
TorrentSize int64
|
||||||
|
|
||||||
|
PreloadedBytes int64
|
||||||
|
PreloadSize int64
|
||||||
|
|
||||||
|
DownloadSpeed float64
|
||||||
|
UploadSpeed float64
|
||||||
|
|
||||||
|
TotalPeers int
|
||||||
|
PendingPeers int
|
||||||
|
ActivePeers int
|
||||||
|
ConnectedSeeders int
|
||||||
|
HalfOpenPeers int
|
||||||
|
|
||||||
|
BytesWritten int64
|
||||||
|
BytesWrittenData int64
|
||||||
|
BytesRead int64
|
||||||
|
BytesReadData int64
|
||||||
|
BytesReadUsefulData int64
|
||||||
|
ChunksWritten int64
|
||||||
|
ChunksRead int64
|
||||||
|
ChunksReadUseful int64
|
||||||
|
ChunksReadWasted int64
|
||||||
|
PiecesDirtiedGood int64
|
||||||
|
PiecesDirtiedBad int64
|
||||||
|
|
||||||
|
FileStats []TorrentFileStat
|
||||||
|
}
|
||||||
|
|
||||||
|
type TorrentFileStat struct {
|
||||||
|
Id int
|
||||||
|
Path string
|
||||||
|
Length int64
|
||||||
|
}
|
||||||
400
src/server/torr/Torrent.go
Normal file
400
src/server/torr/Torrent.go
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
package torr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"server/settings"
|
||||||
|
"server/utils"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent"
|
||||||
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
|
"github.com/labstack/gommon/bytes"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TorrentStatus int
|
||||||
|
|
||||||
|
func (t TorrentStatus) String() string {
|
||||||
|
switch t {
|
||||||
|
case TorrentAdded:
|
||||||
|
return "Torrent added"
|
||||||
|
case TorrentGettingInfo:
|
||||||
|
return "Torrent getting info"
|
||||||
|
case TorrentPreload:
|
||||||
|
return "Torrent preload"
|
||||||
|
case TorrentWorking:
|
||||||
|
return "Torrent working"
|
||||||
|
case TorrentClosed:
|
||||||
|
return "Torrent closed"
|
||||||
|
default:
|
||||||
|
return "Torrent unknown status"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
TorrentAdded = TorrentStatus(iota)
|
||||||
|
TorrentGettingInfo
|
||||||
|
TorrentPreload
|
||||||
|
TorrentWorking
|
||||||
|
TorrentClosed
|
||||||
|
)
|
||||||
|
|
||||||
|
type Torrent struct {
|
||||||
|
*torrent.Torrent
|
||||||
|
|
||||||
|
status TorrentStatus
|
||||||
|
|
||||||
|
readers map[torrent.Reader]struct{}
|
||||||
|
|
||||||
|
muTorrent sync.Mutex
|
||||||
|
muReader sync.Mutex
|
||||||
|
|
||||||
|
bt *BTServer
|
||||||
|
|
||||||
|
lastTimeSpeed time.Time
|
||||||
|
DownloadSpeed float64
|
||||||
|
UploadSpeed float64
|
||||||
|
BytesReadUsefulData int64
|
||||||
|
BytesWrittenData int64
|
||||||
|
|
||||||
|
PreloadSize int64
|
||||||
|
PreloadedBytes int64
|
||||||
|
|
||||||
|
hash metainfo.Hash
|
||||||
|
|
||||||
|
expiredTime time.Time
|
||||||
|
|
||||||
|
closed <-chan struct{}
|
||||||
|
|
||||||
|
progressTicker *time.Ticker
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTorrent(magnet metainfo.Magnet, bt *BTServer) (*Torrent, error) {
|
||||||
|
switch settings.Get().RetrackersMode {
|
||||||
|
case 1:
|
||||||
|
magnet.Trackers = append(magnet.Trackers, utils.GetDefTrackers()...)
|
||||||
|
case 2:
|
||||||
|
magnet.Trackers = nil
|
||||||
|
case 3:
|
||||||
|
magnet.Trackers = utils.GetDefTrackers()
|
||||||
|
}
|
||||||
|
goTorrent, _, err := bt.client.AddTorrentSpec(&torrent.TorrentSpec{
|
||||||
|
Trackers: [][]string{magnet.Trackers},
|
||||||
|
DisplayName: magnet.DisplayName,
|
||||||
|
InfoHash: magnet.InfoHash,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bt.mu.Lock()
|
||||||
|
defer bt.mu.Unlock()
|
||||||
|
if tor, ok := bt.torrents[magnet.InfoHash]; ok {
|
||||||
|
return tor, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
torr := new(Torrent)
|
||||||
|
torr.Torrent = goTorrent
|
||||||
|
torr.status = TorrentAdded
|
||||||
|
torr.lastTimeSpeed = time.Now()
|
||||||
|
torr.bt = bt
|
||||||
|
torr.readers = make(map[torrent.Reader]struct{})
|
||||||
|
torr.hash = magnet.InfoHash
|
||||||
|
torr.closed = goTorrent.Closed()
|
||||||
|
|
||||||
|
go torr.watch()
|
||||||
|
|
||||||
|
bt.torrents[magnet.InfoHash] = torr
|
||||||
|
return torr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Torrent) WaitInfo() bool {
|
||||||
|
if t.Torrent == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-t.Torrent.GotInfo():
|
||||||
|
return true
|
||||||
|
case <-t.closed:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Torrent) GotInfo() bool {
|
||||||
|
if t.status == TorrentClosed {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
t.status = TorrentGettingInfo
|
||||||
|
if t.WaitInfo() {
|
||||||
|
t.status = TorrentWorking
|
||||||
|
t.expiredTime = time.Now().Add(time.Minute * 5)
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
t.Close()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Torrent) watch() {
|
||||||
|
t.progressTicker = time.NewTicker(time.Second)
|
||||||
|
defer t.progressTicker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-t.progressTicker.C:
|
||||||
|
go t.progressEvent()
|
||||||
|
case <-t.closed:
|
||||||
|
t.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Torrent) progressEvent() {
|
||||||
|
if t.expired() {
|
||||||
|
t.drop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.muTorrent.Lock()
|
||||||
|
if t.Torrent != nil && t.Torrent.Info() != nil {
|
||||||
|
st := t.Torrent.Stats()
|
||||||
|
deltaDlBytes := st.BytesReadUsefulData.Int64() - t.BytesReadUsefulData
|
||||||
|
deltaUpBytes := st.BytesWrittenData.Int64() - t.BytesWrittenData
|
||||||
|
deltaTime := time.Since(t.lastTimeSpeed).Seconds()
|
||||||
|
|
||||||
|
t.DownloadSpeed = float64(deltaDlBytes) / deltaTime
|
||||||
|
t.UploadSpeed = float64(deltaUpBytes) / deltaTime
|
||||||
|
|
||||||
|
t.BytesWrittenData = st.BytesWrittenData.Int64()
|
||||||
|
t.BytesReadUsefulData = st.BytesReadUsefulData.Int64()
|
||||||
|
} else {
|
||||||
|
t.DownloadSpeed = 0
|
||||||
|
t.UploadSpeed = 0
|
||||||
|
}
|
||||||
|
t.muTorrent.Unlock()
|
||||||
|
t.lastTimeSpeed = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Torrent) expired() bool {
|
||||||
|
return len(t.readers) == 0 && t.expiredTime.Before(time.Now()) && (t.status == TorrentWorking || t.status == TorrentClosed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Torrent) Files() []*torrent.File {
|
||||||
|
if t.Torrent != nil && t.Torrent.Info() != nil {
|
||||||
|
files := t.Torrent.Files()
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Torrent) Hash() metainfo.Hash {
|
||||||
|
return t.hash
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Torrent) Status() TorrentStatus {
|
||||||
|
return t.status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Torrent) Length() int64 {
|
||||||
|
if t.Info() == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return t.Torrent.Length()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Torrent) NewReader(file *torrent.File, readahead int64) torrent.Reader {
|
||||||
|
t.muReader.Lock()
|
||||||
|
|
||||||
|
if t.status == TorrentClosed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
defer t.muReader.Unlock()
|
||||||
|
reader := file.NewReader()
|
||||||
|
if readahead <= 0 {
|
||||||
|
readahead = utils.GetReadahead()
|
||||||
|
}
|
||||||
|
reader.SetReadahead(readahead)
|
||||||
|
t.readers[reader] = struct{}{}
|
||||||
|
return reader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Torrent) CloseReader(reader torrent.Reader) {
|
||||||
|
t.muReader.Lock()
|
||||||
|
reader.Close()
|
||||||
|
delete(t.readers, reader)
|
||||||
|
t.expiredTime = time.Now().Add(time.Second * 5)
|
||||||
|
t.muReader.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Torrent) Preload(file *torrent.File, size int64) {
|
||||||
|
if size < 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.status == TorrentGettingInfo {
|
||||||
|
t.WaitInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
t.muTorrent.Lock()
|
||||||
|
if t.status != TorrentWorking {
|
||||||
|
t.muTorrent.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if size == 0 {
|
||||||
|
size = settings.Get().PreloadBufferSize
|
||||||
|
}
|
||||||
|
if size == 0 {
|
||||||
|
t.muTorrent.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.status = TorrentPreload
|
||||||
|
t.muTorrent.Unlock()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if t.status == TorrentPreload {
|
||||||
|
t.status = TorrentWorking
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
buff5mb := int64(5 * 1024 * 1024)
|
||||||
|
startPreloadLength := size
|
||||||
|
endPreloadOffset := int64(0)
|
||||||
|
if startPreloadLength > buff5mb {
|
||||||
|
endPreloadOffset = file.Offset() + file.Length() - buff5mb
|
||||||
|
}
|
||||||
|
|
||||||
|
readerPre := t.NewReader(file, startPreloadLength)
|
||||||
|
if readerPre == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
t.CloseReader(readerPre)
|
||||||
|
t.expiredTime = time.Now().Add(time.Minute * 1)
|
||||||
|
}()
|
||||||
|
|
||||||
|
if endPreloadOffset > 0 {
|
||||||
|
readerPost := t.NewReader(file, 1)
|
||||||
|
if readerPre == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
readerPost.Seek(endPreloadOffset, io.SeekStart)
|
||||||
|
readerPost.SetReadahead(buff5mb)
|
||||||
|
defer func() {
|
||||||
|
t.CloseReader(readerPost)
|
||||||
|
t.expiredTime = time.Now().Add(time.Minute * 1)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
if size > file.Length() {
|
||||||
|
size = file.Length()
|
||||||
|
}
|
||||||
|
|
||||||
|
t.PreloadSize = size
|
||||||
|
var lastSize int64 = 0
|
||||||
|
errCount := 0
|
||||||
|
for t.status == TorrentPreload {
|
||||||
|
t.expiredTime = time.Now().Add(time.Minute * 1)
|
||||||
|
t.PreloadedBytes = t.Torrent.BytesCompleted()
|
||||||
|
fmt.Println("Preload:", file.Torrent().InfoHash().HexString(), bytes.Format(t.PreloadedBytes), "/", bytes.Format(t.PreloadSize), "Speed:", utils.Format(t.DownloadSpeed), "Peers:[", t.Torrent.Stats().ConnectedSeeders, "]", t.Torrent.Stats().ActivePeers, "/", t.Torrent.Stats().TotalPeers)
|
||||||
|
if t.PreloadedBytes >= t.PreloadSize {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if lastSize == t.PreloadedBytes {
|
||||||
|
errCount++
|
||||||
|
} else {
|
||||||
|
lastSize = t.PreloadedBytes
|
||||||
|
errCount = 0
|
||||||
|
}
|
||||||
|
if errCount > 120 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Torrent) drop() {
|
||||||
|
t.muTorrent.Lock()
|
||||||
|
if t.Torrent != nil {
|
||||||
|
t.Torrent.Drop()
|
||||||
|
t.Torrent = nil
|
||||||
|
}
|
||||||
|
t.muTorrent.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Torrent) Close() {
|
||||||
|
t.status = TorrentClosed
|
||||||
|
t.bt.mu.Lock()
|
||||||
|
defer t.bt.mu.Unlock()
|
||||||
|
|
||||||
|
t.muReader.Lock()
|
||||||
|
defer t.muReader.Unlock()
|
||||||
|
|
||||||
|
for r := range t.readers {
|
||||||
|
r.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := t.bt.torrents[t.hash]; ok {
|
||||||
|
delete(t.bt.torrents, t.hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.drop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Torrent) Stats() TorrentStats {
|
||||||
|
t.muTorrent.Lock()
|
||||||
|
defer t.muTorrent.Unlock()
|
||||||
|
|
||||||
|
st := TorrentStats{}
|
||||||
|
|
||||||
|
st.Name = t.Name()
|
||||||
|
st.Hash = t.hash.HexString()
|
||||||
|
st.TorrentStatus = t.status
|
||||||
|
st.TorrentStatusString = t.status.String()
|
||||||
|
|
||||||
|
if t.Torrent != nil {
|
||||||
|
st.LoadedSize = t.Torrent.BytesCompleted()
|
||||||
|
st.TorrentSize = t.Length()
|
||||||
|
st.PreloadedBytes = t.PreloadedBytes
|
||||||
|
st.PreloadSize = t.PreloadSize
|
||||||
|
st.DownloadSpeed = t.DownloadSpeed
|
||||||
|
st.UploadSpeed = t.UploadSpeed
|
||||||
|
|
||||||
|
tst := t.Torrent.Stats()
|
||||||
|
st.BytesWritten = tst.BytesWritten.Int64()
|
||||||
|
st.BytesWrittenData = tst.BytesWrittenData.Int64()
|
||||||
|
st.BytesRead = tst.BytesRead.Int64()
|
||||||
|
st.BytesReadData = tst.BytesReadData.Int64()
|
||||||
|
st.BytesReadUsefulData = tst.BytesReadUsefulData.Int64()
|
||||||
|
st.ChunksWritten = tst.ChunksWritten.Int64()
|
||||||
|
st.ChunksRead = tst.ChunksRead.Int64()
|
||||||
|
st.ChunksReadUseful = tst.ChunksReadUseful.Int64()
|
||||||
|
st.ChunksReadWasted = tst.ChunksReadWasted.Int64()
|
||||||
|
st.PiecesDirtiedGood = tst.PiecesDirtiedGood.Int64()
|
||||||
|
st.PiecesDirtiedBad = tst.PiecesDirtiedBad.Int64()
|
||||||
|
st.TotalPeers = tst.TotalPeers
|
||||||
|
st.PendingPeers = tst.PendingPeers
|
||||||
|
st.ActivePeers = tst.ActivePeers
|
||||||
|
st.ConnectedSeeders = tst.ConnectedSeeders
|
||||||
|
st.HalfOpenPeers = tst.HalfOpenPeers
|
||||||
|
|
||||||
|
for i, f := range t.Files() {
|
||||||
|
st.FileStats = append(st.FileStats, TorrentFileStat{
|
||||||
|
Id: i,
|
||||||
|
Path: f.Path(),
|
||||||
|
Length: f.Length(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sort.Slice(st.FileStats, func(i, j int) bool {
|
||||||
|
return st.FileStats[i].Path < st.FileStats[j].Path
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return st
|
||||||
|
}
|
||||||
15
src/server/torr/storage/Storage.go
Normal file
15
src/server/torr/storage/Storage.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"server/torr/storage/state"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
|
"github.com/anacrolix/torrent/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Storage interface {
|
||||||
|
storage.ClientImpl
|
||||||
|
|
||||||
|
GetStats(hash metainfo.Hash) *state.CacheState
|
||||||
|
CloseHash(hash metainfo.Hash)
|
||||||
|
}
|
||||||
1
src/server/torr/storage/filecache/Cache.go
Normal file
1
src/server/torr/storage/filecache/Cache.go
Normal file
@@ -0,0 +1 @@
|
|||||||
|
package filecache
|
||||||
69
src/server/torr/storage/filecache/Storage.go
Normal file
69
src/server/torr/storage/filecache/Storage.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package filecache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"server/settings"
|
||||||
|
"server/torr/storage"
|
||||||
|
"server/torr/storage/state"
|
||||||
|
|
||||||
|
"github.com/anacrolix/missinggo/filecache"
|
||||||
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
|
storage2 "github.com/anacrolix/torrent/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Storage struct {
|
||||||
|
storage.Storage
|
||||||
|
|
||||||
|
caches map[metainfo.Hash]*filecache.Cache
|
||||||
|
capacity int64
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStorage(capacity int64) storage.Storage {
|
||||||
|
stor := new(Storage)
|
||||||
|
stor.capacity = capacity
|
||||||
|
stor.caches = make(map[metainfo.Hash]*filecache.Cache)
|
||||||
|
return stor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (storage2.TorrentImpl, error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
path := filepath.Join(settings.Path, "cache", infoHash.String())
|
||||||
|
cache, err := filecache.NewCache(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cache.SetCapacity(s.capacity)
|
||||||
|
s.caches[infoHash] = cache
|
||||||
|
return storage2.NewResourcePieces(cache.AsResourceProvider()).OpenTorrent(info, infoHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) GetStats(hash metainfo.Hash) *state.CacheState {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) Clean() {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) CloseHash(hash metainfo.Hash) {
|
||||||
|
if s.caches == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) Close() error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
86
src/server/torr/storage/memcache/Buffer.go
Normal file
86
src/server/torr/storage/memcache/Buffer.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package memcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"server/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type buffer struct {
|
||||||
|
pieceId int
|
||||||
|
buf []byte
|
||||||
|
used bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type BufferPool struct {
|
||||||
|
buffs map[int]*buffer
|
||||||
|
frees int
|
||||||
|
size int64
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBufferPool(bufferLength int64, capacity int64) *BufferPool {
|
||||||
|
bp := new(BufferPool)
|
||||||
|
buffsSize := int(capacity/bufferLength) + 3
|
||||||
|
bp.frees = buffsSize
|
||||||
|
bp.size = bufferLength
|
||||||
|
return bp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BufferPool) mkBuffs() {
|
||||||
|
if b.buffs != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b.buffs = make(map[int]*buffer, b.frees)
|
||||||
|
fmt.Println("Create", b.frees, "buffers")
|
||||||
|
for i := 0; i < b.frees; i++ {
|
||||||
|
buf := buffer{
|
||||||
|
-1,
|
||||||
|
make([]byte, b.size),
|
||||||
|
false,
|
||||||
|
}
|
||||||
|
b.buffs[i] = &buf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BufferPool) GetBuffer(p *Piece) (buff []byte, index int) {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
b.mkBuffs()
|
||||||
|
for id, buf := range b.buffs {
|
||||||
|
if !buf.used {
|
||||||
|
buf.used = true
|
||||||
|
buf.pieceId = p.Id
|
||||||
|
buff = buf.buf
|
||||||
|
index = id
|
||||||
|
b.frees--
|
||||||
|
//fmt.Printf("Get buffer: %v %v %v %p\n", id, p.Id, b.frees, buff)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println("Create slow buffer")
|
||||||
|
return make([]byte, b.size), -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BufferPool) ReleaseBuffer(index int) {
|
||||||
|
if index == -1 {
|
||||||
|
utils.FreeOSMem()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
b.mkBuffs()
|
||||||
|
if buff, ok := b.buffs[index]; ok {
|
||||||
|
buff.used = false
|
||||||
|
buff.pieceId = -1
|
||||||
|
b.frees++
|
||||||
|
//fmt.Println("Release buffer:", index, b.frees)
|
||||||
|
} else {
|
||||||
|
utils.FreeOSMem()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BufferPool) Len() int {
|
||||||
|
return b.frees
|
||||||
|
}
|
||||||
196
src/server/torr/storage/memcache/Cache.go
Normal file
196
src/server/torr/storage/memcache/Cache.go
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
package memcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"server/torr/storage/state"
|
||||||
|
"server/utils"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
|
"github.com/anacrolix/torrent/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cache struct {
|
||||||
|
storage.TorrentImpl
|
||||||
|
|
||||||
|
s *Storage
|
||||||
|
|
||||||
|
capacity int64
|
||||||
|
filled int64
|
||||||
|
hash metainfo.Hash
|
||||||
|
|
||||||
|
pieceLength int64
|
||||||
|
pieceCount int
|
||||||
|
piecesBuff int
|
||||||
|
|
||||||
|
muPiece sync.Mutex
|
||||||
|
muRemove sync.Mutex
|
||||||
|
isRemove bool
|
||||||
|
|
||||||
|
pieces map[int]*Piece
|
||||||
|
bufferPull *BufferPool
|
||||||
|
|
||||||
|
prcLoaded int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCache(capacity int64, storage *Storage) *Cache {
|
||||||
|
ret := &Cache{
|
||||||
|
capacity: capacity,
|
||||||
|
filled: 0,
|
||||||
|
pieces: make(map[int]*Piece),
|
||||||
|
s: storage,
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) Init(info *metainfo.Info, hash metainfo.Hash) {
|
||||||
|
fmt.Println("Create cache for:", info.Name)
|
||||||
|
//Min capacity of 2 pieces length
|
||||||
|
cap := info.PieceLength * 2
|
||||||
|
if c.capacity < cap {
|
||||||
|
c.capacity = cap
|
||||||
|
}
|
||||||
|
c.pieceLength = info.PieceLength
|
||||||
|
c.pieceCount = info.NumPieces()
|
||||||
|
c.piecesBuff = int(c.capacity / c.pieceLength)
|
||||||
|
c.hash = hash
|
||||||
|
c.bufferPull = NewBufferPool(c.pieceLength, c.capacity)
|
||||||
|
|
||||||
|
for i := 0; i < c.pieceCount; i++ {
|
||||||
|
c.pieces[i] = &Piece{
|
||||||
|
Id: i,
|
||||||
|
Length: info.Piece(i).Length(),
|
||||||
|
Hash: info.Piece(i).Hash().HexString(),
|
||||||
|
cache: c,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) Piece(m metainfo.Piece) storage.PieceImpl {
|
||||||
|
c.muPiece.Lock()
|
||||||
|
defer c.muPiece.Unlock()
|
||||||
|
if val, ok := c.pieces[m.Index()]; ok {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) Close() error {
|
||||||
|
c.isRemove = false
|
||||||
|
fmt.Println("Close cache for:", c.hash)
|
||||||
|
if _, ok := c.s.caches[c.hash]; ok {
|
||||||
|
delete(c.s.caches, c.hash)
|
||||||
|
}
|
||||||
|
c.pieces = nil
|
||||||
|
c.bufferPull = nil
|
||||||
|
utils.FreeOSMemGC()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) GetState() state.CacheState {
|
||||||
|
cState := state.CacheState{}
|
||||||
|
cState.Capacity = c.capacity
|
||||||
|
cState.PiecesLength = c.pieceLength
|
||||||
|
cState.PiecesCount = c.pieceCount
|
||||||
|
cState.Hash = c.hash.HexString()
|
||||||
|
|
||||||
|
stats := make(map[int]state.ItemState, 0)
|
||||||
|
c.muPiece.Lock()
|
||||||
|
var fill int64 = 0
|
||||||
|
for _, value := range c.pieces {
|
||||||
|
stat := value.Stat()
|
||||||
|
if stat.BufferSize > 0 {
|
||||||
|
fill += stat.BufferSize
|
||||||
|
stats[stat.Id] = stat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.filled = fill
|
||||||
|
c.muPiece.Unlock()
|
||||||
|
cState.Filled = c.filled
|
||||||
|
cState.Pieces = stats
|
||||||
|
return cState
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) cleanPieces() {
|
||||||
|
if c.isRemove {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.muRemove.Lock()
|
||||||
|
if c.isRemove {
|
||||||
|
c.muRemove.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.isRemove = true
|
||||||
|
defer func() { c.isRemove = false }()
|
||||||
|
c.muRemove.Unlock()
|
||||||
|
|
||||||
|
remPieces := c.getRemPieces()
|
||||||
|
if len(remPieces) > 0 && (c.capacity < c.filled || c.bufferPull.Len() <= 1) {
|
||||||
|
remCount := int((c.filled - c.capacity) / c.pieceLength)
|
||||||
|
if remCount < 1 {
|
||||||
|
remCount = 1
|
||||||
|
}
|
||||||
|
if remCount > len(remPieces) {
|
||||||
|
remCount = len(remPieces)
|
||||||
|
}
|
||||||
|
|
||||||
|
remPieces = remPieces[:remCount]
|
||||||
|
|
||||||
|
for _, p := range remPieces {
|
||||||
|
c.removePiece(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) getRemPieces() []*Piece {
|
||||||
|
pieces := make([]*Piece, 0)
|
||||||
|
fill := int64(0)
|
||||||
|
loading := 0
|
||||||
|
used := make(map[int]struct{})
|
||||||
|
for _, b := range c.bufferPull.buffs {
|
||||||
|
if b.used {
|
||||||
|
used[b.pieceId] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for u := range used {
|
||||||
|
v := c.pieces[u]
|
||||||
|
if v.Size > 0 {
|
||||||
|
if v.Id > 0 {
|
||||||
|
pieces = append(pieces, v)
|
||||||
|
}
|
||||||
|
fill += v.Size
|
||||||
|
if !v.complete {
|
||||||
|
loading++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.filled = fill
|
||||||
|
sort.Slice(pieces, func(i, j int) bool {
|
||||||
|
return pieces[i].accessed.Before(pieces[j].accessed)
|
||||||
|
})
|
||||||
|
|
||||||
|
c.prcLoaded = prc(c.piecesBuff-loading, c.piecesBuff)
|
||||||
|
return pieces
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) removePiece(piece *Piece) {
|
||||||
|
c.muPiece.Lock()
|
||||||
|
defer c.muPiece.Unlock()
|
||||||
|
piece.Release()
|
||||||
|
|
||||||
|
st := fmt.Sprintf("%v%% %v\t%s\t%s", c.prcLoaded, piece.Id, piece.accessed.Format("15:04:05.000"), piece.Hash)
|
||||||
|
if c.prcLoaded >= 95 {
|
||||||
|
fmt.Println("Clean memory GC:", st)
|
||||||
|
utils.FreeOSMemGC()
|
||||||
|
} else {
|
||||||
|
fmt.Println("Clean memory:", st)
|
||||||
|
utils.FreeOSMem()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func prc(val, of int) int {
|
||||||
|
return int(float64(val) * 100.0 / float64(of))
|
||||||
|
}
|
||||||
115
src/server/torr/storage/memcache/Piece.go
Normal file
115
src/server/torr/storage/memcache/Piece.go
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package memcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"server/torr/storage/state"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Piece struct {
|
||||||
|
storage.PieceImpl
|
||||||
|
|
||||||
|
Id int
|
||||||
|
Hash string
|
||||||
|
Length int64
|
||||||
|
Size int64
|
||||||
|
|
||||||
|
complete bool
|
||||||
|
readed bool
|
||||||
|
accessed time.Time
|
||||||
|
buffer []byte
|
||||||
|
bufIndex int
|
||||||
|
|
||||||
|
mu sync.RWMutex
|
||||||
|
cache *Cache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Piece) WriteAt(b []byte, off int64) (n int, err error) {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
|
if p.buffer == nil {
|
||||||
|
go p.cache.cleanPieces()
|
||||||
|
p.buffer, p.bufIndex = p.cache.bufferPull.GetBuffer(p)
|
||||||
|
if p.buffer == nil {
|
||||||
|
return 0, errors.New("Can't get buffer write")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n = copy(p.buffer[off:], b[:])
|
||||||
|
p.Size += int64(n)
|
||||||
|
p.accessed = time.Now()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Piece) ReadAt(b []byte, off int64) (n int, err error) {
|
||||||
|
p.mu.RLock()
|
||||||
|
defer p.mu.RUnlock()
|
||||||
|
|
||||||
|
size := len(b)
|
||||||
|
if size+int(off) > len(p.buffer) {
|
||||||
|
size = len(p.buffer) - int(off)
|
||||||
|
if size < 0 {
|
||||||
|
size = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(p.buffer) < int(off) || len(p.buffer) < int(off)+size {
|
||||||
|
return 0, io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
n = copy(b, p.buffer[int(off) : int(off)+size][:])
|
||||||
|
p.accessed = time.Now()
|
||||||
|
if int(off)+size >= len(p.buffer) {
|
||||||
|
p.readed = true
|
||||||
|
}
|
||||||
|
if int64(len(b))+off >= p.Size {
|
||||||
|
go p.cache.cleanPieces()
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Piece) MarkComplete() error {
|
||||||
|
if len(p.buffer) == 0 {
|
||||||
|
return errors.New("piece is not complete")
|
||||||
|
}
|
||||||
|
p.complete = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Piece) MarkNotComplete() error {
|
||||||
|
p.complete = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Piece) Completion() storage.Completion {
|
||||||
|
return storage.Completion{
|
||||||
|
Complete: p.complete && len(p.buffer) > 0,
|
||||||
|
Ok: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Piece) Release() {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
if p.buffer != nil {
|
||||||
|
p.buffer = nil
|
||||||
|
p.cache.bufferPull.ReleaseBuffer(p.bufIndex)
|
||||||
|
p.bufIndex = -1
|
||||||
|
}
|
||||||
|
p.Size = 0
|
||||||
|
p.complete = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Piece) Stat() state.ItemState {
|
||||||
|
itm := state.ItemState{
|
||||||
|
Id: p.Id,
|
||||||
|
Hash: p.Hash,
|
||||||
|
Accessed: p.accessed,
|
||||||
|
Completed: p.complete,
|
||||||
|
BufferSize: p.Size,
|
||||||
|
}
|
||||||
|
return itm
|
||||||
|
}
|
||||||
66
src/server/torr/storage/memcache/Storage.go
Normal file
66
src/server/torr/storage/memcache/Storage.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package memcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"server/torr/storage"
|
||||||
|
"server/torr/storage/state"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
|
storage2 "github.com/anacrolix/torrent/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Storage struct {
|
||||||
|
storage.Storage
|
||||||
|
|
||||||
|
caches map[metainfo.Hash]*Cache
|
||||||
|
capacity int64
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStorage(capacity int64) storage.Storage {
|
||||||
|
stor := new(Storage)
|
||||||
|
stor.capacity = capacity
|
||||||
|
stor.caches = make(map[metainfo.Hash]*Cache)
|
||||||
|
return stor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (storage2.TorrentImpl, error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
ch := NewCache(s.capacity, s)
|
||||||
|
ch.Init(info, infoHash)
|
||||||
|
s.caches[infoHash] = ch
|
||||||
|
return ch, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) GetStats(hash metainfo.Hash) *state.CacheState {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
if c, ok := s.caches[hash]; ok {
|
||||||
|
st := c.GetState()
|
||||||
|
return &st
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) CloseHash(hash metainfo.Hash) {
|
||||||
|
if s.caches == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
if ch, ok := s.caches[hash]; ok {
|
||||||
|
ch.Close()
|
||||||
|
delete(s.caches, hash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Storage) Close() error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
for _, ch := range s.caches {
|
||||||
|
ch.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
22
src/server/torr/storage/state/state.go
Normal file
22
src/server/torr/storage/state/state.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package state
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CacheState struct {
|
||||||
|
Hash string
|
||||||
|
Capacity int64
|
||||||
|
Filled int64
|
||||||
|
PiecesLength int64
|
||||||
|
PiecesCount int
|
||||||
|
Pieces map[int]ItemState
|
||||||
|
}
|
||||||
|
|
||||||
|
type ItemState struct {
|
||||||
|
Id int
|
||||||
|
Accessed time.Time
|
||||||
|
BufferSize int64
|
||||||
|
Completed bool
|
||||||
|
Hash string
|
||||||
|
}
|
||||||
17
src/server/utils/Prallel.go
Normal file
17
src/server/utils/Prallel.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParallelFor(begin, end int, fn func(i int)) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(end - begin)
|
||||||
|
for i := begin; i < end; i++ {
|
||||||
|
go func(i int) {
|
||||||
|
fn(i)
|
||||||
|
wg.Done()
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
74
src/server/utils/Torrent.go
Normal file
74
src/server/utils/Torrent.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base32"
|
||||||
|
"errors"
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"server/settings"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent"
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
)
|
||||||
|
|
||||||
|
var trackers = []string{
|
||||||
|
"http://bt4.t-ru.org/ann?magnet",
|
||||||
|
"http://retracker.mgts.by:80/announce",
|
||||||
|
"http://tracker.city9x.com:2710/announce",
|
||||||
|
"http://tracker.electro-torrent.pl:80/announce",
|
||||||
|
"http://tracker.internetwarriors.net:1337/announce",
|
||||||
|
"http://tracker2.itzmx.com:6961/announce",
|
||||||
|
"udp://46.148.18.250:2710",
|
||||||
|
"udp://opentor.org:2710",
|
||||||
|
"udp://public.popcorn-tracker.org:6969/announce",
|
||||||
|
"udp://tracker.opentrackr.org:1337/announce",
|
||||||
|
|
||||||
|
"http://bt.svao-ix.ru/announce",
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDefTrackers() []string {
|
||||||
|
return trackers
|
||||||
|
}
|
||||||
|
|
||||||
|
func PeerIDRandom(peer string) string {
|
||||||
|
randomBytes := make([]byte, 32)
|
||||||
|
_, err := rand.Read(randomBytes)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return peer + base32.StdEncoding.EncodeToString(randomBytes)[:20-len(peer)]
|
||||||
|
}
|
||||||
|
|
||||||
|
func GotInfo(t *torrent.Torrent, timeout int) error {
|
||||||
|
gi := t.GotInfo()
|
||||||
|
select {
|
||||||
|
case <-gi:
|
||||||
|
return nil
|
||||||
|
case <-time.Tick(time.Second * time.Duration(timeout)):
|
||||||
|
return errors.New("timeout load torrent info")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetReadahead() int64 {
|
||||||
|
readahead := int64(float64(settings.Get().CacheSize) * 0.33)
|
||||||
|
if readahead < 66*1024*1024 {
|
||||||
|
readahead = int64(settings.Get().CacheSize)
|
||||||
|
if readahead > 66*1024*1024 {
|
||||||
|
readahead = 66 * 1024 * 1024
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return readahead
|
||||||
|
}
|
||||||
|
|
||||||
|
func Limit(i int) *rate.Limiter {
|
||||||
|
l := rate.NewLimiter(rate.Inf, 0)
|
||||||
|
if i > 0 {
|
||||||
|
b := i
|
||||||
|
if b < 16*1024 {
|
||||||
|
b = 16 * 1024
|
||||||
|
}
|
||||||
|
l = rate.NewLimiter(rate.Limit(i), b)
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
33
src/server/utils/TorrentInfo.go
Normal file
33
src/server/utils/TorrentInfo.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"server/settings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AddInfo(hash, js string) {
|
||||||
|
info := settings.GetInfo(hash)
|
||||||
|
if info != "{}" {
|
||||||
|
var jsset map[string]interface{}
|
||||||
|
var err error
|
||||||
|
if err = json.Unmarshal([]byte(js), &jsset); err == nil {
|
||||||
|
var jsdb map[string]interface{}
|
||||||
|
if err = json.Unmarshal([]byte(info), &jsdb); err == nil {
|
||||||
|
for k, v := range jsset {
|
||||||
|
jsdb[k] = v
|
||||||
|
}
|
||||||
|
jsstr, err := json.Marshal(jsdb)
|
||||||
|
if err == nil {
|
||||||
|
settings.AddInfo(hash, string(jsstr))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
settings.AddInfo(hash, js)
|
||||||
|
}
|
||||||
78
src/server/utils/Utils.go
Normal file
78
src/server/utils/Utils.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"runtime"
|
||||||
|
"runtime/debug"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CleanFName(file string) string {
|
||||||
|
re := regexp.MustCompile(`[ !*'();:@&=+$,/?#\[\]~"]`)
|
||||||
|
ret := re.ReplaceAllString(file, `_`)
|
||||||
|
ret = strings.Replace(ret, "__", "_", -1)
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func FreeOSMem() {
|
||||||
|
debug.FreeOSMemory()
|
||||||
|
}
|
||||||
|
|
||||||
|
func FreeOSMemGC() {
|
||||||
|
runtime.GC()
|
||||||
|
debug.FreeOSMemory()
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
_ = 1.0 << (10 * iota) // ignore first value by assigning to blank identifier
|
||||||
|
KB
|
||||||
|
MB
|
||||||
|
GB
|
||||||
|
TB
|
||||||
|
PB
|
||||||
|
EB
|
||||||
|
)
|
||||||
|
|
||||||
|
func Format(b float64) string {
|
||||||
|
multiple := ""
|
||||||
|
value := b
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case b >= EB:
|
||||||
|
value /= EB
|
||||||
|
multiple = "EB"
|
||||||
|
case b >= PB:
|
||||||
|
value /= PB
|
||||||
|
multiple = "PB"
|
||||||
|
case b >= TB:
|
||||||
|
value /= TB
|
||||||
|
multiple = "TB"
|
||||||
|
case b >= GB:
|
||||||
|
value /= GB
|
||||||
|
multiple = "GB"
|
||||||
|
case b >= MB:
|
||||||
|
value /= MB
|
||||||
|
multiple = "MB"
|
||||||
|
case b >= KB:
|
||||||
|
value /= KB
|
||||||
|
multiple = "KB"
|
||||||
|
case b == 0:
|
||||||
|
return "0"
|
||||||
|
default:
|
||||||
|
return strconv.FormatInt(int64(b), 10) + "B"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%.2f%s", value, multiple)
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//func IsCyrillic(str string) bool {
|
||||||
|
// for _, r := range str {
|
||||||
|
// if unicode.Is(unicode.Cyrillic, r) {
|
||||||
|
// return true
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return false
|
||||||
|
//}
|
||||||
3
src/server/version/Version.go
Normal file
3
src/server/version/Version.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package version
|
||||||
|
|
||||||
|
const Version = "1.1.65"
|
||||||
15
src/server/web/About.go
Normal file
15
src/server/web/About.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/labstack/echo"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initAbout(e *echo.Echo) {
|
||||||
|
e.GET("/about", aboutPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
func aboutPage(c echo.Context) error {
|
||||||
|
return c.Render(http.StatusOK, "aboutPage", nil)
|
||||||
|
}
|
||||||
115
src/server/web/Info.go
Normal file
115
src/server/web/Info.go
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"server/utils"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
|
"github.com/labstack/echo"
|
||||||
|
"github.com/labstack/gommon/bytes"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initInfo(e *echo.Echo) {
|
||||||
|
server.GET("/cache", cachePage)
|
||||||
|
server.GET("/stat", statePage)
|
||||||
|
server.GET("/btstat", btStatePage)
|
||||||
|
}
|
||||||
|
|
||||||
|
func btStatePage(c echo.Context) error {
|
||||||
|
bts.WriteState(c.Response())
|
||||||
|
return c.NoContent(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cachePage(c echo.Context) error {
|
||||||
|
return c.Render(http.StatusOK, "cachePage", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func statePage(c echo.Context) error {
|
||||||
|
state := bts.BTState()
|
||||||
|
|
||||||
|
msg := ""
|
||||||
|
|
||||||
|
msg += fmt.Sprintf("Listen port: %d<br>\n", state.LocalPort)
|
||||||
|
msg += fmt.Sprintf("Peer ID: %+q<br>\n", state.PeerID)
|
||||||
|
msg += fmt.Sprintf("Banned IPs: %d<br>\n", state.BannedIPs)
|
||||||
|
|
||||||
|
for _, dht := range state.DHTs {
|
||||||
|
msg += fmt.Sprintf("%s DHT server at %s:<br>\n", dht.Addr().Network(), dht.Addr().String())
|
||||||
|
dhtStats := dht.Stats()
|
||||||
|
msg += fmt.Sprintf("\t # Nodes: %d (%d good, %d banned)<br>\n", dhtStats.Nodes, dhtStats.GoodNodes, dhtStats.BadNodes)
|
||||||
|
msg += fmt.Sprintf("\t Server ID: %x<br>\n", dht.ID())
|
||||||
|
msg += fmt.Sprintf("\t Announces: %d<br>\n", dhtStats.SuccessfulOutboundAnnouncePeerQueries)
|
||||||
|
msg += fmt.Sprintf("\t Outstanding transactions: %d<br>\n", dhtStats.OutstandingTransactions)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(state.Torrents, func(i, j int) bool {
|
||||||
|
return state.Torrents[i].Hash().HexString() < state.Torrents[j].Hash().HexString()
|
||||||
|
})
|
||||||
|
msg += "Torrents:<br>\n"
|
||||||
|
for _, t := range state.Torrents {
|
||||||
|
st := t.Stats()
|
||||||
|
msg += fmt.Sprintf("Name: %v<br>\n", st.Name)
|
||||||
|
msg += fmt.Sprintf("Hash: %v<br>\n", st.Hash)
|
||||||
|
msg += fmt.Sprintf("Status: %v<br>\n", st.TorrentStatus)
|
||||||
|
msg += fmt.Sprintf("Loaded Size: %v<br>\n", bytes.Format(st.LoadedSize))
|
||||||
|
msg += fmt.Sprintf("Torrent Size: %v<br>\n<br>\n", bytes.Format(st.TorrentSize))
|
||||||
|
|
||||||
|
msg += fmt.Sprintf("Preloaded Bytes: %v<br>\n", bytes.Format(st.PreloadedBytes))
|
||||||
|
msg += fmt.Sprintf("Preload Size: %v<br>\n<br>\n", bytes.Format(st.PreloadSize))
|
||||||
|
|
||||||
|
msg += fmt.Sprintf("Download Speed: %v/Sec<br>\n", utils.Format(st.DownloadSpeed))
|
||||||
|
msg += fmt.Sprintf("Upload Speed: %v/Sec<br>\n<br>\n", utils.Format(st.UploadSpeed))
|
||||||
|
|
||||||
|
msg += fmt.Sprintf("\t TotalPeers: %v<br>\n", st.TotalPeers)
|
||||||
|
msg += fmt.Sprintf("\t PendingPeers: %v<br>\n", st.PendingPeers)
|
||||||
|
msg += fmt.Sprintf("\t ActivePeers: %v<br>\n", st.ActivePeers)
|
||||||
|
msg += fmt.Sprintf("\t ConnectedSeeders: %v<br>\n", st.ConnectedSeeders)
|
||||||
|
msg += fmt.Sprintf("\t HalfOpenPeers: %v<br>\n", st.HalfOpenPeers)
|
||||||
|
|
||||||
|
msg += fmt.Sprintf("\t BytesWritten: %v (%v)<br>\n", st.BytesWritten, bytes.Format(st.BytesWritten))
|
||||||
|
msg += fmt.Sprintf("\t BytesWrittenData: %v (%v)<br>\n", st.BytesWrittenData, bytes.Format(st.BytesWrittenData))
|
||||||
|
msg += fmt.Sprintf("\t BytesRead: %v (%v)<br>\n", st.BytesRead, bytes.Format(st.BytesRead))
|
||||||
|
msg += fmt.Sprintf("\t BytesReadData: %v (%v)<br>\n", st.BytesReadData, bytes.Format(st.BytesReadData))
|
||||||
|
msg += fmt.Sprintf("\t BytesReadUsefulData: %v (%v)<br>\n", st.BytesReadUsefulData, bytes.Format(st.BytesReadUsefulData))
|
||||||
|
msg += fmt.Sprintf("\t ChunksWritten: %v<br>\n", st.ChunksWritten)
|
||||||
|
msg += fmt.Sprintf("\t ChunksRead: %v<br>\n", st.ChunksRead)
|
||||||
|
msg += fmt.Sprintf("\t ChunksReadUseful: %v<br>\n", st.ChunksReadUseful)
|
||||||
|
msg += fmt.Sprintf("\t ChunksReadWasted: %v<br>\n", st.ChunksReadWasted)
|
||||||
|
msg += fmt.Sprintf("\t PiecesDirtiedGood: %v<br>\n", st.PiecesDirtiedGood)
|
||||||
|
msg += fmt.Sprintf("\t PiecesDirtiedBad: %v<br>\n<br>\n", st.PiecesDirtiedBad)
|
||||||
|
if len(st.FileStats) > 0 {
|
||||||
|
msg += fmt.Sprintf("\t Files:<br>\n")
|
||||||
|
for _, f := range st.FileStats {
|
||||||
|
msg += fmt.Sprintf("\t \t %v Size:%v<br>\n", f.Path, bytes.Format(f.Length))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := metainfo.NewHashFromHex(st.Hash)
|
||||||
|
cState := bts.CacheState(hash)
|
||||||
|
if cState != nil {
|
||||||
|
msg += fmt.Sprintf("CacheType:<br>\n")
|
||||||
|
msg += fmt.Sprintf("Capacity: %v<br>\n", bytes.Format(cState.Capacity))
|
||||||
|
msg += fmt.Sprintf("Filled: %v<br>\n", bytes.Format(cState.Filled))
|
||||||
|
msg += fmt.Sprintf("PiecesLength: %v<br>\n", bytes.Format(cState.PiecesLength))
|
||||||
|
msg += fmt.Sprintf("PiecesCount: %v<br>\n", cState.PiecesCount)
|
||||||
|
for _, p := range cState.Pieces {
|
||||||
|
msg += fmt.Sprintf("\t Piece: %v\t  Access: %s\t  Buffer size: %d(%s)\t  Complete: %v\t  Hash: %s\n<br>", p.Id, p.Accessed.Format("15:04:05.000"), p.BufferSize, bytes.Format(int64(p.BufferSize)), p.Completed, p.Hash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
msg += "<hr><br><br>\n\n"
|
||||||
|
}
|
||||||
|
//msg += `
|
||||||
|
//<script>
|
||||||
|
//document.addEventListener("DOMContentLoaded", function(event) {
|
||||||
|
// setTimeout(function(){
|
||||||
|
// location.reload();
|
||||||
|
// }, 1000);
|
||||||
|
//});
|
||||||
|
//</script>
|
||||||
|
//
|
||||||
|
//`
|
||||||
|
return c.HTML(http.StatusOK, msg)
|
||||||
|
}
|
||||||
151
src/server/web/Server.go
Normal file
151
src/server/web/Server.go
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"server/settings"
|
||||||
|
"server/torr"
|
||||||
|
"server/version"
|
||||||
|
"server/web/mods"
|
||||||
|
"server/web/templates"
|
||||||
|
|
||||||
|
"github.com/anacrolix/sync"
|
||||||
|
"github.com/labstack/echo"
|
||||||
|
"github.com/labstack/echo/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
server *echo.Echo
|
||||||
|
bts *torr.BTServer
|
||||||
|
|
||||||
|
mutex sync.Mutex
|
||||||
|
fnMutex sync.Mutex
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
func Start(port string) {
|
||||||
|
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||||
|
|
||||||
|
fmt.Println("Start web server, version:", version.Version)
|
||||||
|
|
||||||
|
bts = torr.NewBTS()
|
||||||
|
err := bts.Connect()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error start torrent client:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mutex.Lock()
|
||||||
|
server = echo.New()
|
||||||
|
server.HideBanner = true
|
||||||
|
server.HidePort = true
|
||||||
|
server.HTTPErrorHandler = HTTPErrorHandler
|
||||||
|
|
||||||
|
//server.Use(middleware.Logger())
|
||||||
|
server.Use(middleware.Recover())
|
||||||
|
|
||||||
|
templates.InitTemplate(server)
|
||||||
|
initTorrent(server)
|
||||||
|
initSettings(server)
|
||||||
|
initInfo(server)
|
||||||
|
initAbout(server)
|
||||||
|
mods.InitMods(server)
|
||||||
|
|
||||||
|
server.GET("/", mainPage)
|
||||||
|
server.GET("/echo", echoPage)
|
||||||
|
server.POST("/shutdown", shutdownPage)
|
||||||
|
server.GET("/js/api.js", templates.Api_JS)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer mutex.Unlock()
|
||||||
|
|
||||||
|
server.Listener, err = net.Listen("tcp", "0.0.0.0:"+port)
|
||||||
|
if err == nil {
|
||||||
|
err = server.Start("0.0.0.0:" + port)
|
||||||
|
}
|
||||||
|
server = nil
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error start web server:", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Stop() {
|
||||||
|
fnMutex.Lock()
|
||||||
|
defer fnMutex.Unlock()
|
||||||
|
if server != nil {
|
||||||
|
fmt.Println("Stop web server")
|
||||||
|
server.Close()
|
||||||
|
server = nil
|
||||||
|
if bts != nil {
|
||||||
|
bts.Disconnect()
|
||||||
|
bts = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Wait() error {
|
||||||
|
mutex.Lock()
|
||||||
|
mutex.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func mainPage(c echo.Context) error {
|
||||||
|
return c.Render(http.StatusOK, "mainPage", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func echoPage(c echo.Context) error {
|
||||||
|
return c.String(http.StatusOK, version.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
func shutdownPage(c echo.Context) error {
|
||||||
|
go func() {
|
||||||
|
Stop()
|
||||||
|
settings.CloseDB()
|
||||||
|
time.Sleep(time.Second * 2)
|
||||||
|
os.Exit(5)
|
||||||
|
}()
|
||||||
|
return c.NoContent(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func HTTPErrorHandler(err error, c echo.Context) {
|
||||||
|
var (
|
||||||
|
code = http.StatusInternalServerError
|
||||||
|
msg interface{}
|
||||||
|
)
|
||||||
|
|
||||||
|
if he, ok := err.(*echo.HTTPError); ok {
|
||||||
|
code = he.Code
|
||||||
|
msg = he.Message
|
||||||
|
if he.Internal != nil {
|
||||||
|
msg = fmt.Sprintf("%v, %v", err, he.Internal)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
msg = http.StatusText(code)
|
||||||
|
}
|
||||||
|
if _, ok := msg.(string); ok {
|
||||||
|
msg = echo.Map{"message": msg}
|
||||||
|
}
|
||||||
|
|
||||||
|
if code != 404 && c.Request().URL.Path != "/torrent/stat" {
|
||||||
|
log.Println("Web server error:", err, c.Request().URL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send response
|
||||||
|
if !c.Response().Committed {
|
||||||
|
if c.Request().Method == echo.HEAD { // Issue #608
|
||||||
|
err = c.NoContent(code)
|
||||||
|
} else {
|
||||||
|
err = c.JSON(code, msg)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/server/web/Settings.go
Normal file
52
src/server/web/Settings.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"server/settings"
|
||||||
|
|
||||||
|
"github.com/labstack/echo"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initSettings(e *echo.Echo) {
|
||||||
|
e.GET("/settings", settingsPage)
|
||||||
|
e.POST("/settings/read", settingsRead)
|
||||||
|
e.POST("/settings/write", settingsWrite)
|
||||||
|
}
|
||||||
|
|
||||||
|
func settingsPage(c echo.Context) error {
|
||||||
|
return c.Render(http.StatusOK, "settingsPage", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func settingsRead(c echo.Context) error {
|
||||||
|
return c.JSON(http.StatusOK, settings.Get())
|
||||||
|
}
|
||||||
|
|
||||||
|
func settingsWrite(c echo.Context) error {
|
||||||
|
err := getJsSettings(c)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
settings.SaveSettings()
|
||||||
|
return c.JSON(http.StatusOK, "Ok")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getJsSettings(c echo.Context) error {
|
||||||
|
buf, _ := ioutil.ReadAll(c.Request().Body)
|
||||||
|
decoder := json.NewDecoder(bytes.NewBuffer(buf))
|
||||||
|
err := decoder.Decode(settings.Get())
|
||||||
|
if err != nil {
|
||||||
|
if ute, ok := err.(*json.UnmarshalTypeError); ok {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unmarshal type error: expected=%v, got=%v, offset=%v", ute.Type, ute.Value, ute.Offset))
|
||||||
|
} else if se, ok := err.(*json.SyntaxError); ok {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Syntax error: offset=%v, error=%v", se.Offset, se.Error()))
|
||||||
|
} else {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
659
src/server/web/Torrent.go
Normal file
659
src/server/web/Torrent.go
Normal file
@@ -0,0 +1,659 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"server/settings"
|
||||||
|
"server/torr"
|
||||||
|
"server/utils"
|
||||||
|
"server/web/helpers"
|
||||||
|
|
||||||
|
"github.com/anacrolix/missinggo/httptoo"
|
||||||
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
|
"github.com/labstack/echo"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initTorrent(e *echo.Echo) {
|
||||||
|
e.POST("/torrent/add", torrentAdd)
|
||||||
|
e.POST("/torrent/upload", torrentUpload)
|
||||||
|
e.POST("/torrent/get", torrentGet)
|
||||||
|
e.POST("/torrent/rem", torrentRem)
|
||||||
|
e.POST("/torrent/list", torrentList)
|
||||||
|
e.POST("/torrent/stat", torrentStat)
|
||||||
|
e.POST("/torrent/cache", torrentCache)
|
||||||
|
e.POST("/torrent/drop", torrentDrop)
|
||||||
|
|
||||||
|
e.GET("/torrent/restart", torrentRestart)
|
||||||
|
|
||||||
|
e.GET("/torrent/playlist.m3u", torrentPlayListAll)
|
||||||
|
|
||||||
|
e.GET("/torrent/play", torrentPlay)
|
||||||
|
e.HEAD("/torrent/play", torrentPlay)
|
||||||
|
|
||||||
|
e.GET("/torrent/view/:hash/:file", torrentView)
|
||||||
|
e.HEAD("/torrent/view/:hash/:file", torrentView)
|
||||||
|
e.GET("/torrent/preload/:hash/:file", torrentPreload)
|
||||||
|
e.GET("/torrent/preload/:size/:hash/:file", torrentPreloadSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
type TorrentJsonRequest struct {
|
||||||
|
Link string `json:",omitempty"`
|
||||||
|
Hash string `json:",omitempty"`
|
||||||
|
Title string `json:",omitempty"`
|
||||||
|
Info string `json:",omitempty"`
|
||||||
|
DontSave bool `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TorrentJsonResponse struct {
|
||||||
|
Name string
|
||||||
|
Magnet string
|
||||||
|
Hash string
|
||||||
|
AddTime int64
|
||||||
|
Length int64
|
||||||
|
Status torr.TorrentStatus
|
||||||
|
Playlist string
|
||||||
|
Info string
|
||||||
|
Files []TorFile `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TorFile struct {
|
||||||
|
Name string
|
||||||
|
Link string
|
||||||
|
Preload string
|
||||||
|
Size int64
|
||||||
|
Viewed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func torrentAdd(c echo.Context) error {
|
||||||
|
jreq, err := getJsReqTorr(c)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if jreq.Link == "" {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Link must be non-empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
magnet, err := helpers.GetMagnet(jreq.Link)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error get magnet:", jreq.Hash, err)
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if jreq.Title != "" {
|
||||||
|
magnet.DisplayName = jreq.Title
|
||||||
|
}
|
||||||
|
|
||||||
|
err = helpers.Add(bts, *magnet, !jreq.DontSave)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error add torrent:", jreq.Hash, err)
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if jreq.Info != "" {
|
||||||
|
go func() {
|
||||||
|
utils.AddInfo(magnet.InfoHash.HexString(), jreq.Info)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.String(http.StatusOK, magnet.InfoHash.HexString())
|
||||||
|
}
|
||||||
|
|
||||||
|
func torrentUpload(c echo.Context) error {
|
||||||
|
form, err := c.MultipartForm()
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
defer form.RemoveAll()
|
||||||
|
|
||||||
|
_, dontSave := form.Value["DontSave"]
|
||||||
|
var magnets []metainfo.Magnet
|
||||||
|
|
||||||
|
for _, file := range form.File {
|
||||||
|
torrFile, err := file[0].Open()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer torrFile.Close()
|
||||||
|
|
||||||
|
mi, err := metainfo.Load(torrFile)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error upload torrent", err)
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := mi.UnmarshalInfo()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error upload torrent", err)
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
magnet := mi.Magnet(info.Name, mi.HashInfoBytes())
|
||||||
|
magnets = append(magnets, magnet)
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := make([]string, 0)
|
||||||
|
for _, magnet := range magnets {
|
||||||
|
er := helpers.Add(bts, magnet, !dontSave)
|
||||||
|
if er != nil {
|
||||||
|
err = er
|
||||||
|
fmt.Println("Error add torrent:", magnet.String(), er)
|
||||||
|
}
|
||||||
|
ret = append(ret, magnet.InfoHash.HexString())
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
func torrentGet(c echo.Context) error {
|
||||||
|
jreq, err := getJsReqTorr(c)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
if jreq.Hash == "" {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Hash must be non-empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
tor, err := settings.LoadTorrentDB(jreq.Hash)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error get torrent:", jreq.Hash, err)
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
torrStatus := torr.TorrentAdded
|
||||||
|
if tor == nil {
|
||||||
|
hash := metainfo.NewHashFromHex(jreq.Hash)
|
||||||
|
ts := bts.GetTorrent(hash)
|
||||||
|
if ts != nil {
|
||||||
|
torrStatus = ts.Status()
|
||||||
|
tor = toTorrentDB(ts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tor == nil {
|
||||||
|
fmt.Println("Error get: torrent not found", jreq.Hash)
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Error get: torrent not found "+jreq.Hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
js, err := getTorrentJS(tor)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error get torrent:", tor.Hash, err)
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
js.Status = torrStatus
|
||||||
|
return c.JSON(http.StatusOK, js)
|
||||||
|
}
|
||||||
|
|
||||||
|
func torrentRem(c echo.Context) error {
|
||||||
|
jreq, err := getJsReqTorr(c)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
if jreq.Hash == "" {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Hash must be non-empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.RemoveTorrentDB(jreq.Hash)
|
||||||
|
bts.RemoveTorrent(metainfo.NewHashFromHex(jreq.Hash))
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func torrentList(c echo.Context) error {
|
||||||
|
buf, _ := ioutil.ReadAll(c.Request().Body)
|
||||||
|
jsstr := string(buf)
|
||||||
|
decoder := json.NewDecoder(bytes.NewBufferString(jsstr))
|
||||||
|
jsreq := struct {
|
||||||
|
Request int
|
||||||
|
}{}
|
||||||
|
decoder.Decode(&jsreq)
|
||||||
|
|
||||||
|
reqType := jsreq.Request
|
||||||
|
|
||||||
|
js := make([]TorrentJsonResponse, 0)
|
||||||
|
list, _ := settings.LoadTorrentsDB()
|
||||||
|
|
||||||
|
for _, tor := range list {
|
||||||
|
jsTor, err := getTorrentJS(tor)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error get torrent:", err)
|
||||||
|
} else {
|
||||||
|
js = append(js, *jsTor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(js, func(i, j int) bool {
|
||||||
|
if js[i].AddTime == js[j].AddTime {
|
||||||
|
return js[i].Name < js[j].Name
|
||||||
|
}
|
||||||
|
return js[i].AddTime > js[j].AddTime
|
||||||
|
})
|
||||||
|
|
||||||
|
slist := bts.List()
|
||||||
|
|
||||||
|
find := func(tjs []TorrentJsonResponse, t *torr.Torrent) bool {
|
||||||
|
for _, j := range tjs {
|
||||||
|
if t.Hash().HexString() == j.Hash {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, st := range slist {
|
||||||
|
if !find(js, st) {
|
||||||
|
tdb := toTorrentDB(st)
|
||||||
|
jsTor, err := getTorrentJS(tdb)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error get torrent:", err)
|
||||||
|
} else {
|
||||||
|
jsTor.Status = st.Status()
|
||||||
|
js = append(js, *jsTor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if reqType == 1 {
|
||||||
|
ret := make([]TorrentJsonResponse, 0)
|
||||||
|
for _, r := range js {
|
||||||
|
if r.Status == torr.TorrentWorking || len(r.Files) > 0 {
|
||||||
|
ret = append(ret, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c.JSON(http.StatusOK, ret)
|
||||||
|
} else if reqType == 2 {
|
||||||
|
ret := make([]TorrentJsonResponse, 0)
|
||||||
|
for _, r := range js {
|
||||||
|
if r.Status == torr.TorrentGettingInfo {
|
||||||
|
ret = append(ret, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c.JSON(http.StatusOK, ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, js)
|
||||||
|
}
|
||||||
|
|
||||||
|
func torrentStat(c echo.Context) error {
|
||||||
|
jreq, err := getJsReqTorr(c)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
if jreq.Hash == "" {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Hash must be non-empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := metainfo.NewHashFromHex(jreq.Hash)
|
||||||
|
tor := bts.GetTorrent(hash)
|
||||||
|
if tor == nil {
|
||||||
|
return echo.NewHTTPError(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
stat := tor.Stats()
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, stat)
|
||||||
|
}
|
||||||
|
|
||||||
|
func torrentCache(c echo.Context) error {
|
||||||
|
jreq, err := getJsReqTorr(c)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
if jreq.Hash == "" {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Hash must be non-empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := metainfo.NewHashFromHex(jreq.Hash)
|
||||||
|
stat := bts.CacheState(hash)
|
||||||
|
if stat == nil {
|
||||||
|
return echo.NewHTTPError(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, stat)
|
||||||
|
}
|
||||||
|
|
||||||
|
func preload(hashHex, fileLink string, size int64) *echo.HTTPError {
|
||||||
|
if size > 0 {
|
||||||
|
hash := metainfo.NewHashFromHex(hashHex)
|
||||||
|
tor := bts.GetTorrent(hash)
|
||||||
|
if tor == nil {
|
||||||
|
torrDb, err := settings.LoadTorrentDB(hashHex)
|
||||||
|
if err != nil || torrDb == nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Torrent not found: "+hashHex)
|
||||||
|
}
|
||||||
|
m, err := metainfo.ParseMagnetURI(torrDb.Magnet)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Error parser magnet in db: "+hashHex)
|
||||||
|
}
|
||||||
|
tor, err = bts.AddTorrent(m, nil)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tor.WaitInfo() {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "torrent closed befor get info")
|
||||||
|
}
|
||||||
|
|
||||||
|
file := helpers.FindFileLink(fileLink, tor.Torrent)
|
||||||
|
if file == nil {
|
||||||
|
return echo.NewHTTPError(http.StatusNotFound, "file in torrent not found: "+fileLink)
|
||||||
|
}
|
||||||
|
tor.Preload(file, size)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func torrentPreload(c echo.Context) error {
|
||||||
|
hashHex, err := url.PathUnescape(c.Param("hash"))
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
fileLink, err := url.PathUnescape(c.Param("file"))
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if hashHex == "" {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Hash must be non-empty")
|
||||||
|
}
|
||||||
|
if fileLink == "" {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "File link must be non-empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
errHttp := preload(hashHex, fileLink, settings.Get().PreloadBufferSize)
|
||||||
|
if err != nil {
|
||||||
|
return errHttp
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.NoContent(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func torrentPreloadSize(c echo.Context) error {
|
||||||
|
hashHex, err := url.PathUnescape(c.Param("hash"))
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
fileLink, err := url.PathUnescape(c.Param("file"))
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
szPreload, err := url.PathUnescape(c.Param("size"))
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if hashHex == "" {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Hash must be non-empty")
|
||||||
|
}
|
||||||
|
if fileLink == "" {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "File link must be non-empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
var size = settings.Get().PreloadBufferSize
|
||||||
|
if szPreload != "" {
|
||||||
|
sz, err := strconv.Atoi(szPreload)
|
||||||
|
if err == nil && sz > 0 {
|
||||||
|
size = int64(sz) * 1024 * 1024
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errHttp := preload(hashHex, fileLink, size)
|
||||||
|
if err != nil {
|
||||||
|
return errHttp
|
||||||
|
}
|
||||||
|
//redirectUrl := c.Scheme() + "://" + c.Request().Host + filepath.Join("/torrent/view/", hashHex, fileLink)
|
||||||
|
//return c.Redirect(http.StatusFound, redirectUrl)
|
||||||
|
return c.NoContent(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func torrentDrop(c echo.Context) error {
|
||||||
|
jreq, err := getJsReqTorr(c)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
if jreq.Hash == "" {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Hash must be non-empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
bts.RemoveTorrent(metainfo.NewHashFromHex(jreq.Hash))
|
||||||
|
return c.NoContent(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func torrentRestart(c echo.Context) error {
|
||||||
|
fmt.Println("Restart torrent engine")
|
||||||
|
err := bts.Reconnect()
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
return c.String(http.StatusOK, "Ok")
|
||||||
|
}
|
||||||
|
|
||||||
|
func torrentPlayListAll(c echo.Context) error {
|
||||||
|
list, err := settings.LoadTorrentsDB()
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
m3u := helpers.MakeM3ULists(list, c.Scheme()+"://"+c.Request().Host)
|
||||||
|
|
||||||
|
c.Response().Header().Set("Content-Type", "audio/x-mpegurl")
|
||||||
|
c.Response().Header().Set("Content-Disposition", `attachment; filename="playlist.m3u"`)
|
||||||
|
http.ServeContent(c.Response(), c.Request(), "playlist.m3u", time.Now(), bytes.NewReader([]byte(m3u)))
|
||||||
|
return c.NoContent(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func torrentPlay(c echo.Context) error {
|
||||||
|
link := c.QueryParam("link")
|
||||||
|
if link == "" {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "link should not be empty")
|
||||||
|
}
|
||||||
|
fmt.Println("Play:", c.QueryParams())
|
||||||
|
|
||||||
|
qsave := c.QueryParam("save")
|
||||||
|
qpreload := c.QueryParam("preload")
|
||||||
|
qfile := c.QueryParam("file")
|
||||||
|
qstat := c.QueryParam("stat")
|
||||||
|
mm3u := c.QueryParam("m3u")
|
||||||
|
|
||||||
|
preload := int64(0)
|
||||||
|
stat := strings.ToLower(qstat) == "true"
|
||||||
|
|
||||||
|
if qpreload != "" {
|
||||||
|
preload, _ = strconv.ParseInt(qpreload, 10, 64)
|
||||||
|
if preload > 0 {
|
||||||
|
preload *= 1024 * 1024
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
magnet, err := helpers.GetMagnet(link)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error get magnet:", link, err)
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
tor := bts.GetTorrent(magnet.InfoHash)
|
||||||
|
if tor == nil {
|
||||||
|
tor, err = bts.AddTorrent(*magnet, nil)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if stat {
|
||||||
|
return c.JSON(http.StatusOK, getTorPlayState(tor))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tor.WaitInfo() {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "torrent closed befor get info")
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.ToLower(qsave) == "true" {
|
||||||
|
if t, err := settings.LoadTorrentDB(magnet.InfoHash.HexString()); t == nil && err == nil {
|
||||||
|
torrDb := toTorrentDB(tor)
|
||||||
|
if torrDb != nil {
|
||||||
|
settings.SaveTorrentDB(torrDb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.ToLower(mm3u) == "true" {
|
||||||
|
mt := tor.Torrent.Metainfo()
|
||||||
|
m3u := helpers.MakeM3UPlayList(tor.Stats(), mt.Magnet(tor.Name(), tor.Hash()).String(), c.Scheme()+"://"+c.Request().Host)
|
||||||
|
c.Response().Header().Set("Content-Type", "audio/x-mpegurl")
|
||||||
|
c.Response().Header().Set("Connection", "close")
|
||||||
|
name := utils.CleanFName(tor.Name()) + ".m3u"
|
||||||
|
c.Response().Header().Set("ETag", httptoo.EncodeQuotedString(fmt.Sprintf("%s/%s", tor.Hash().HexString(), name)))
|
||||||
|
c.Response().Header().Set("Content-Disposition", `attachment; filename="`+name+`"`)
|
||||||
|
http.ServeContent(c.Response(), c.Request(), name, time.Time{}, bytes.NewReader([]byte(m3u)))
|
||||||
|
return c.NoContent(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
files := helpers.GetPlayableFiles(tor.Stats())
|
||||||
|
|
||||||
|
if len(files) == 1 {
|
||||||
|
file := helpers.FindFile(files[0].Id, tor)
|
||||||
|
if file == nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprint("File", files[0], "not found in torrent", tor.Name()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return bts.Play(tor, file, preload, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if qfile == "" && len(files) > 1 {
|
||||||
|
return c.JSON(http.StatusOK, getTorPlayState(tor))
|
||||||
|
}
|
||||||
|
|
||||||
|
fileInd, _ := strconv.Atoi(qfile)
|
||||||
|
file := helpers.FindFile(fileInd, tor)
|
||||||
|
if file == nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprint("File", files[fileInd], "not found in torrent", tor.Name()))
|
||||||
|
}
|
||||||
|
return bts.Play(tor, file, preload, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func torrentView(c echo.Context) error {
|
||||||
|
hashHex, err := url.PathUnescape(c.Param("hash"))
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
fileLink, err := url.PathUnescape(c.Param("file"))
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := metainfo.NewHashFromHex(hashHex)
|
||||||
|
tor := bts.GetTorrent(hash)
|
||||||
|
if tor == nil {
|
||||||
|
torrDb, err := settings.LoadTorrentDB(hashHex)
|
||||||
|
if err != nil || torrDb == nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Torrent not found: "+hashHex)
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := metainfo.ParseMagnetURI(torrDb.Magnet)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Error parser magnet in db: "+hashHex)
|
||||||
|
}
|
||||||
|
|
||||||
|
tor, err = bts.AddTorrent(m, nil)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tor.WaitInfo() {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "torrent closed befor get info")
|
||||||
|
}
|
||||||
|
|
||||||
|
file := helpers.FindFileLink(fileLink, tor.Torrent)
|
||||||
|
if file == nil {
|
||||||
|
return echo.NewHTTPError(http.StatusNotFound, "File in torrent not found: "+fileLink)
|
||||||
|
}
|
||||||
|
return bts.View(tor, file, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func toTorrentDB(t *torr.Torrent) *settings.Torrent {
|
||||||
|
if t == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
tor := new(settings.Torrent)
|
||||||
|
tor.Name = t.Name()
|
||||||
|
tor.Hash = t.Hash().HexString()
|
||||||
|
tor.Timestamp = settings.StartTime.Unix()
|
||||||
|
mi := t.Torrent.Metainfo()
|
||||||
|
tor.Magnet = mi.Magnet(t.Name(), t.Torrent.InfoHash()).String()
|
||||||
|
tor.Size = t.Length()
|
||||||
|
files := t.Files()
|
||||||
|
sort.Slice(files, func(i, j int) bool {
|
||||||
|
return files[i].Path() < files[j].Path()
|
||||||
|
})
|
||||||
|
for _, f := range files {
|
||||||
|
tf := settings.File{
|
||||||
|
Name: f.Path(),
|
||||||
|
Size: f.Length(),
|
||||||
|
Viewed: false,
|
||||||
|
}
|
||||||
|
tor.Files = append(tor.Files, tf)
|
||||||
|
}
|
||||||
|
return tor
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTorrentJS(tor *settings.Torrent) (*TorrentJsonResponse, error) {
|
||||||
|
js := new(TorrentJsonResponse)
|
||||||
|
mag, err := metainfo.ParseMagnetURI(tor.Magnet)
|
||||||
|
js.Name = tor.Name
|
||||||
|
if err == nil && len(tor.Name) < len(mag.DisplayName) {
|
||||||
|
js.Name = mag.DisplayName
|
||||||
|
}
|
||||||
|
js.Magnet = tor.Magnet
|
||||||
|
js.Hash = tor.Hash
|
||||||
|
js.AddTime = tor.Timestamp
|
||||||
|
js.Length = tor.Size
|
||||||
|
//fname is fake param for file name
|
||||||
|
js.Playlist = "/torrent/play?link=" + url.QueryEscape(tor.Magnet) + "&m3u=true&fname=" + utils.CleanFName(tor.Name+".m3u")
|
||||||
|
var size int64 = 0
|
||||||
|
for _, f := range tor.Files {
|
||||||
|
size += f.Size
|
||||||
|
tf := TorFile{
|
||||||
|
Name: f.Name,
|
||||||
|
Link: "/torrent/view/" + js.Hash + "/" + utils.CleanFName(f.Name),
|
||||||
|
Preload: "/torrent/preload/" + js.Hash + "/" + utils.CleanFName(f.Name),
|
||||||
|
Size: f.Size,
|
||||||
|
Viewed: f.Viewed,
|
||||||
|
}
|
||||||
|
js.Files = append(js.Files, tf)
|
||||||
|
}
|
||||||
|
if tor.Size == 0 {
|
||||||
|
js.Length = size
|
||||||
|
}
|
||||||
|
|
||||||
|
js.Info = settings.GetInfo(tor.Hash)
|
||||||
|
|
||||||
|
return js, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getJsReqTorr(c echo.Context) (*TorrentJsonRequest, error) {
|
||||||
|
buf, _ := ioutil.ReadAll(c.Request().Body)
|
||||||
|
jsstr := string(buf)
|
||||||
|
decoder := json.NewDecoder(bytes.NewBufferString(jsstr))
|
||||||
|
js := new(TorrentJsonRequest)
|
||||||
|
err := decoder.Decode(js)
|
||||||
|
if err != nil {
|
||||||
|
if ute, ok := err.(*json.UnmarshalTypeError); ok {
|
||||||
|
return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unmarshal type error: expected=%v, got=%v, offset=%v", ute.Type, ute.Value, ute.Offset))
|
||||||
|
} else if se, ok := err.(*json.SyntaxError); ok {
|
||||||
|
return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Syntax error: offset=%v, error=%v", se.Offset, se.Error()))
|
||||||
|
} else {
|
||||||
|
return nil, echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return js, nil
|
||||||
|
}
|
||||||
67
src/server/web/TorrentState.go
Normal file
67
src/server/web/TorrentState.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"server/torr"
|
||||||
|
"server/web/helpers"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TorrentStat struct {
|
||||||
|
Name string
|
||||||
|
Hash string
|
||||||
|
|
||||||
|
TorrentStatus int
|
||||||
|
TorrentStatusString string
|
||||||
|
|
||||||
|
LoadedSize int64
|
||||||
|
TorrentSize int64
|
||||||
|
|
||||||
|
PreloadedBytes int64
|
||||||
|
PreloadSize int64
|
||||||
|
|
||||||
|
DownloadSpeed float64
|
||||||
|
UploadSpeed float64
|
||||||
|
|
||||||
|
TotalPeers int
|
||||||
|
PendingPeers int
|
||||||
|
ActivePeers int
|
||||||
|
ConnectedSeeders int
|
||||||
|
|
||||||
|
FileStats []FileStat
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileStat struct {
|
||||||
|
Id int
|
||||||
|
Path string
|
||||||
|
Length int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTorPlayState(tor *torr.Torrent) TorrentStat {
|
||||||
|
tst := tor.Stats()
|
||||||
|
ts := TorrentStat{}
|
||||||
|
ts.Name = tst.Name
|
||||||
|
ts.Hash = tst.Hash
|
||||||
|
ts.TorrentStatus = int(tst.TorrentStatus)
|
||||||
|
ts.TorrentStatusString = tst.TorrentStatusString
|
||||||
|
ts.LoadedSize = tst.LoadedSize
|
||||||
|
ts.TorrentSize = tst.TorrentSize
|
||||||
|
ts.PreloadedBytes = tst.PreloadedBytes
|
||||||
|
ts.PreloadSize = tst.PreloadSize
|
||||||
|
ts.DownloadSpeed = tst.DownloadSpeed
|
||||||
|
ts.UploadSpeed = tst.UploadSpeed
|
||||||
|
ts.TotalPeers = tst.TotalPeers
|
||||||
|
ts.PendingPeers = tst.PendingPeers
|
||||||
|
ts.ActivePeers = tst.ActivePeers
|
||||||
|
ts.ConnectedSeeders = tst.ConnectedSeeders
|
||||||
|
|
||||||
|
files := helpers.GetPlayableFiles(tst)
|
||||||
|
ts.FileStats = make([]FileStat, len(files))
|
||||||
|
for i, f := range files {
|
||||||
|
ts.FileStats[i] = FileStat{
|
||||||
|
Id: f.Id,
|
||||||
|
Path: f.Path,
|
||||||
|
Length: f.Length,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ts
|
||||||
|
}
|
||||||
42
src/server/web/helpers/Buffer.go
Normal file
42
src/server/web/helpers/Buffer.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package helpers
|
||||||
|
|
||||||
|
import "bytes"
|
||||||
|
|
||||||
|
type SeekingBuffer struct {
|
||||||
|
str string
|
||||||
|
buffer *bytes.Buffer
|
||||||
|
offset int64
|
||||||
|
size int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSeekingBuffer(str string) *SeekingBuffer {
|
||||||
|
return &SeekingBuffer{
|
||||||
|
str: str,
|
||||||
|
buffer: bytes.NewBufferString(str),
|
||||||
|
offset: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fb *SeekingBuffer) Read(p []byte) (n int, err error) {
|
||||||
|
n, err = fb.buffer.Read(p)
|
||||||
|
fb.offset += int64(n)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fb *SeekingBuffer) Seek(offset int64, whence int) (ret int64, err error) {
|
||||||
|
var newoffset int64
|
||||||
|
switch whence {
|
||||||
|
case 0:
|
||||||
|
newoffset = offset
|
||||||
|
case 1:
|
||||||
|
newoffset = fb.offset + offset
|
||||||
|
case 2:
|
||||||
|
newoffset = int64(len(fb.str)) - offset
|
||||||
|
}
|
||||||
|
if newoffset == fb.offset {
|
||||||
|
return newoffset, nil
|
||||||
|
}
|
||||||
|
fb.buffer = bytes.NewBufferString(fb.str[newoffset:])
|
||||||
|
fb.offset = newoffset
|
||||||
|
return fb.offset, nil
|
||||||
|
}
|
||||||
33
src/server/web/helpers/M3u.go
Normal file
33
src/server/web/helpers/M3u.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"server/settings"
|
||||||
|
"server/torr"
|
||||||
|
"server/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func MakeM3ULists(torrents []*settings.Torrent, host string) string {
|
||||||
|
m3u := "#EXTM3U\n"
|
||||||
|
|
||||||
|
for _, t := range torrents {
|
||||||
|
m3u += "#EXTINF:0," + t.Name + "\n"
|
||||||
|
m3u += host + "/torrent/play?link=" + url.QueryEscape(t.Magnet) + "&m3u=true&fname=" + utils.CleanFName(t.Name+".m3u") + "\n\n"
|
||||||
|
}
|
||||||
|
return m3u
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeM3UPlayList(tor torr.TorrentStats, magnet string, host string) string {
|
||||||
|
m3u := "#EXTM3U\n"
|
||||||
|
|
||||||
|
for _, f := range tor.FileStats {
|
||||||
|
if GetMimeType(f.Path) != "*/*" {
|
||||||
|
m3u += "#EXTINF:-1," + f.Path + "\n"
|
||||||
|
mag := url.QueryEscape(magnet)
|
||||||
|
m3u += host + "/torrent/play?link=" + mag + "&file=" + fmt.Sprint(f.Id) + "\n\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m3u
|
||||||
|
}
|
||||||
86
src/server/web/helpers/Magnet.go
Normal file
86
src/server/web/helpers/Magnet.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetMagnet(link string) (*metainfo.Magnet, error) {
|
||||||
|
url, err := url.Parse(link)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var mag *metainfo.Magnet
|
||||||
|
switch strings.ToLower(url.Scheme) {
|
||||||
|
case "magnet":
|
||||||
|
mag, err = getMag(url.String())
|
||||||
|
case "http", "https":
|
||||||
|
mag, err = getMagFromHttp(url.String())
|
||||||
|
case "":
|
||||||
|
mag, err = getMag("magnet:?xt=urn:btih:" + url.Path)
|
||||||
|
default:
|
||||||
|
mag, err = getMagFromFile(url.Path)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return mag, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMag(link string) (*metainfo.Magnet, error) {
|
||||||
|
mag, err := metainfo.ParseMagnetURI(link)
|
||||||
|
return &mag, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMagFromHttp(url string) (*metainfo.Magnet, error) {
|
||||||
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
client := new(http.Client)
|
||||||
|
client.Timeout = time.Duration(time.Second * 30)
|
||||||
|
req.Header.Set("User-Agent", "DWL/1.1.1 (Torrent)")
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return nil, errors.New(resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
minfo, err := metainfo.Load(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
info, err := minfo.UnmarshalInfo()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mag := minfo.Magnet(info.Name, minfo.HashInfoBytes())
|
||||||
|
return &mag, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMagFromFile(path string) (*metainfo.Magnet, error) {
|
||||||
|
|
||||||
|
minfo, err := metainfo.LoadFromFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
info, err := minfo.UnmarshalInfo()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
mag := minfo.Magnet(info.Name, minfo.HashInfoBytes())
|
||||||
|
return &mag, nil
|
||||||
|
}
|
||||||
89
src/server/web/helpers/Mime.go
Normal file
89
src/server/web/helpers/Mime.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"server/torr"
|
||||||
|
)
|
||||||
|
|
||||||
|
var extVideo = map[string]interface{}{
|
||||||
|
".3g2": nil,
|
||||||
|
".3gp": nil,
|
||||||
|
".aaf": nil,
|
||||||
|
".asf": nil,
|
||||||
|
".avchd": nil,
|
||||||
|
".avi": nil,
|
||||||
|
".drc": nil,
|
||||||
|
".flv": nil,
|
||||||
|
".m2ts": nil,
|
||||||
|
".ts": nil,
|
||||||
|
".m2v": nil,
|
||||||
|
".m4p": nil,
|
||||||
|
".m4v": nil,
|
||||||
|
".mkv": nil,
|
||||||
|
".mng": nil,
|
||||||
|
".mov": nil,
|
||||||
|
".mp2": nil,
|
||||||
|
".mp4": nil,
|
||||||
|
".mpe": nil,
|
||||||
|
".mpeg": nil,
|
||||||
|
".mpg": nil,
|
||||||
|
".mpv": nil,
|
||||||
|
".mxf": nil,
|
||||||
|
".nsv": nil,
|
||||||
|
".ogg": nil,
|
||||||
|
".ogv": nil,
|
||||||
|
".qt": nil,
|
||||||
|
".rm": nil,
|
||||||
|
".rmvb": nil,
|
||||||
|
".roq": nil,
|
||||||
|
".svi": nil,
|
||||||
|
".vob": nil,
|
||||||
|
".webm": nil,
|
||||||
|
".wmv": nil,
|
||||||
|
".yuv": nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
var extAudio = map[string]interface{}{
|
||||||
|
".aac": nil,
|
||||||
|
".aiff": nil,
|
||||||
|
".ape": nil,
|
||||||
|
".au": nil,
|
||||||
|
".flac": nil,
|
||||||
|
".gsm": nil,
|
||||||
|
".it": nil,
|
||||||
|
".m3u": nil,
|
||||||
|
".m4a": nil,
|
||||||
|
".mid": nil,
|
||||||
|
".mod": nil,
|
||||||
|
".mp3": nil,
|
||||||
|
".mpa": nil,
|
||||||
|
".pls": nil,
|
||||||
|
".ra": nil,
|
||||||
|
".s3m": nil,
|
||||||
|
".sid": nil,
|
||||||
|
".wav": nil,
|
||||||
|
".wma": nil,
|
||||||
|
".xm": nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetMimeType(filename string) string {
|
||||||
|
ext := filepath.Ext(filename)
|
||||||
|
if _, ok := extVideo[ext]; ok {
|
||||||
|
return "video/*"
|
||||||
|
}
|
||||||
|
if _, ok := extAudio[ext]; ok {
|
||||||
|
return "audio/*"
|
||||||
|
}
|
||||||
|
return "*/*"
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPlayableFiles(st torr.TorrentStats) []torr.TorrentFileStat {
|
||||||
|
files := make([]torr.TorrentFileStat, 0)
|
||||||
|
for _, f := range st.FileStats {
|
||||||
|
if GetMimeType(f.Path) != "*/*" {
|
||||||
|
files = append(files, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files
|
||||||
|
}
|
||||||
66
src/server/web/helpers/Torrent.go
Normal file
66
src/server/web/helpers/Torrent.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"server/settings"
|
||||||
|
"server/torr"
|
||||||
|
"server/utils"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent"
|
||||||
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Add(bts *torr.BTServer, magnet metainfo.Magnet, save bool) error {
|
||||||
|
fmt.Println("Adding torrent", magnet.String())
|
||||||
|
_, err := bts.AddTorrent(magnet, func(torr *torr.Torrent) {
|
||||||
|
torDb := new(settings.Torrent)
|
||||||
|
torDb.Name = torr.Name()
|
||||||
|
torDb.Hash = torr.Hash().HexString()
|
||||||
|
torDb.Size = torr.Length()
|
||||||
|
torDb.Magnet = magnet.String()
|
||||||
|
torDb.Timestamp = time.Now().Unix()
|
||||||
|
files := torr.Files()
|
||||||
|
sort.Slice(files, func(i, j int) bool {
|
||||||
|
return files[i].Path() < files[j].Path()
|
||||||
|
})
|
||||||
|
for _, f := range files {
|
||||||
|
ff := settings.File{
|
||||||
|
f.Path(),
|
||||||
|
f.Length(),
|
||||||
|
false,
|
||||||
|
}
|
||||||
|
torDb.Files = append(torDb.Files, ff)
|
||||||
|
}
|
||||||
|
|
||||||
|
if save {
|
||||||
|
err := settings.SaveTorrentDB(torDb)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error add torrent to db:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindFileLink(fileLink string, torr *torrent.Torrent) *torrent.File {
|
||||||
|
for _, f := range torr.Files() {
|
||||||
|
if utils.CleanFName(f.Path()) == fileLink {
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindFile(fileInd int, tor *torr.Torrent) *torrent.File {
|
||||||
|
files := tor.Files()
|
||||||
|
if len(files) == 0 || fileInd < 0 || fileInd >= len(files) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return files[fileInd]
|
||||||
|
}
|
||||||
22
src/server/web/mods/Mods.go
Normal file
22
src/server/web/mods/Mods.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package mods
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/labstack/echo"
|
||||||
|
)
|
||||||
|
|
||||||
|
func InitMods(e *echo.Echo) {
|
||||||
|
e.GET("/test", test)
|
||||||
|
}
|
||||||
|
|
||||||
|
func test(c echo.Context) error {
|
||||||
|
return c.HTML(200, `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<script src="http://code.jquery.com/jquery-1.11.3.min.js"></script>
|
||||||
|
<script src="/js/api.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
</body>`)
|
||||||
|
}
|
||||||
119
src/server/web/templates/AboutPage.go
Normal file
119
src/server/web/templates/AboutPage.go
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
package templates
|
||||||
|
|
||||||
|
import "server/version"
|
||||||
|
|
||||||
|
var aboutPage = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link href="` + faviconB64 + `" rel="icon" type="image/x-icon">
|
||||||
|
<script src="/js/api.js"></script>
|
||||||
|
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.1.0/css/all.css" integrity="sha384-lKuwvrZot6UHsBSfcMvOkWwlCMgc0TaWr+30HWe3a4ltaBwTZhyTEggF5tJv8tbt" crossorigin="anonymous">
|
||||||
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
|
||||||
|
<script src="http://code.jquery.com/jquery-1.11.3.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js" integrity="sha384-smHYKdLADwkXOn1EmN1qk/HfnUcbVRZyYmZ4qpPea6sjB/pTJ0euyQp0Mk8ck+5T" crossorigin="anonymous"></script>
|
||||||
|
<title>About</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<style type="text/css">
|
||||||
|
.inline{
|
||||||
|
display:inline;
|
||||||
|
padding-left: 2%;
|
||||||
|
}
|
||||||
|
.center {
|
||||||
|
display: block;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
|
<a class="btn navbar-btn pull-left" href="/"><i class="fas fa-arrow-left"></i></a>
|
||||||
|
<span class="navbar-brand mx-auto">
|
||||||
|
О программе
|
||||||
|
</span>
|
||||||
|
</nav>
|
||||||
|
<div class="content">
|
||||||
|
<img class="center" src='` + faviconB64 + `'/>
|
||||||
|
<h3 align="middle">TorrServer</h3>
|
||||||
|
<h4 align="middle">` + version.Version + `</h4>
|
||||||
|
|
||||||
|
<h4>Поддержка проекта:</h4>
|
||||||
|
<a class="inline" target="_blank" href="https://www.paypal.me/yourok">PayPal</a>
|
||||||
|
<br>
|
||||||
|
<a class="inline" target="_blank" href="https://money.yandex.ru/to/410013733697114/100">Yandex.Деньги</a>
|
||||||
|
<br>
|
||||||
|
<hr align="left" width="25%">
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<h4>Инструкция по использованию:</h4>
|
||||||
|
<a class="inline" target="_blank" href="https://4pda.ru/forum/index.php?showtopic=896840&st=0#entry72570782">4pda.ru</a>
|
||||||
|
<p class="inline">Спасибо <b>MadAndron</b></p>
|
||||||
|
<br>
|
||||||
|
<hr align="left" width="25%">
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<h4>Автор:</h4>
|
||||||
|
<b class="inline">YouROK</b>
|
||||||
|
<br>
|
||||||
|
<i class="inline">Email:</i>
|
||||||
|
<a target="_blank" class="inline" href="mailto:8yourok8@gmail.com">8YouROK8@gmail.com</a>
|
||||||
|
<br>
|
||||||
|
<i class="inline">Site: </i>
|
||||||
|
<a target="_blank" class="inline" href="https://github.com/YouROK">GitHub.com/YouROK</a>
|
||||||
|
<br>
|
||||||
|
<hr align="left" width="25%">
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<h4>Спасибо всем, кто тестировал и помогал:</h4>
|
||||||
|
<b class="inline">kuzzman</b>
|
||||||
|
<br>
|
||||||
|
<i class="inline">Site: </i>
|
||||||
|
<a target="_blank" class="inline" href="https://4pda.ru/forum/index.php?showuser=1259550">4pda.ru</a>
|
||||||
|
<a target="_blank" class="inline" href="http://tv-box.pp.ua">tv-box.pp.ua</a>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<b class="inline">MadAndron</b>
|
||||||
|
<br>
|
||||||
|
<i class="inline">Site:</i>
|
||||||
|
<a target="_blank" class="inline" href="https://4pda.ru/forum/index.php?showuser=1543999">4pda.ru</a>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<b class="inline">SpAwN_LMG</b>
|
||||||
|
<br>
|
||||||
|
<i class="inline">Site:</i>
|
||||||
|
<a target="_blank" class="inline" href="https://4pda.ru/forum/index.php?showuser=700929">4pda.ru</a>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<b class="inline">Zivio</b>
|
||||||
|
<br>
|
||||||
|
<i class="inline">Site:</i>
|
||||||
|
<a target="_blank" class="inline" href="https://4pda.ru/forum/index.php?showuser=1195633">4pda.ru</a>
|
||||||
|
<a target="_blank" class="inline" href="http://forum.hdtv.ru/index.php?showtopic=19020">forum.hdtv.ru</a>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<b class="inline">Tw1cker Руслан Пахнев</b>
|
||||||
|
<br>
|
||||||
|
<i class="inline">Site:</i>
|
||||||
|
<a target="_blank" class="inline" href="https://4pda.ru/forum/index.php?showuser=2002724">4pda.ru</a>
|
||||||
|
<a target="_blank" class="inline" href="https://github.com/Nemiroff">GitHub.com/Nemiroff</a>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
|
<footer class="page-footer navbar-dark bg-dark">
|
||||||
|
<span class="navbar-brand d-flex justify-content-center">
|
||||||
|
<center><h4>TorrServer ` + version.Version + `</h4></center>
|
||||||
|
</span>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
|
|
||||||
|
func (t *Template) parseAboutPage() {
|
||||||
|
parsePage(t, "aboutPage", aboutPage)
|
||||||
|
}
|
||||||
167
src/server/web/templates/CachePage.go
Normal file
167
src/server/web/templates/CachePage.go
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
package templates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"server/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
var cachePage = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link href="` + faviconB64 + `" rel="icon" type="image/x-icon">
|
||||||
|
<script src="/js/api.js"></script>
|
||||||
|
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.1.0/css/all.css" integrity="sha384-lKuwvrZot6UHsBSfcMvOkWwlCMgc0TaWr+30HWe3a4ltaBwTZhyTEggF5tJv8tbt" crossorigin="anonymous">
|
||||||
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
|
||||||
|
<script src="http://code.jquery.com/jquery-1.11.3.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js" integrity="sha384-smHYKdLADwkXOn1EmN1qk/HfnUcbVRZyYmZ4qpPea6sjB/pTJ0euyQp0Mk8ck+5T" crossorigin="anonymous"></script>
|
||||||
|
<title>TorrServer ` + version.Version + `</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<style>
|
||||||
|
.wrap {
|
||||||
|
white-space: normal;
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
margin: 1%;
|
||||||
|
}
|
||||||
|
.cache {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||||
|
}
|
||||||
|
.piece {
|
||||||
|
border: 1px dashed white;
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 2px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
|
<a class="btn navbar-btn pull-left" href="/"><i class="fas fa-arrow-left"></i></a>
|
||||||
|
<span class="navbar-brand mx-auto">
|
||||||
|
TorrServer ` + version.Version + `
|
||||||
|
</span>
|
||||||
|
</nav>
|
||||||
|
<div class="content">
|
||||||
|
<div id="torrents"></div>
|
||||||
|
<div id="cacheInfo"></div>
|
||||||
|
<div class="cache" id="cache"></div>
|
||||||
|
</div>
|
||||||
|
<footer class="page-footer navbar-dark bg-dark">
|
||||||
|
<span class="navbar-brand d-flex justify-content-center">
|
||||||
|
<a rel="external" style="text-decoration: none;" href="/about">Описание</a>
|
||||||
|
</span>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
<script>
|
||||||
|
$( document ).ready(function() {
|
||||||
|
setInterval(updateState, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
var cacheHash = "";
|
||||||
|
var hashTorrents = "";
|
||||||
|
|
||||||
|
function updateTorrents(){
|
||||||
|
listTorrent(function(data){
|
||||||
|
var currHashTorrs = "";
|
||||||
|
for(var key in data) {
|
||||||
|
var tor = data[key];
|
||||||
|
currHashTorrs += tor.Hash;
|
||||||
|
}
|
||||||
|
if (currHashTorrs != hashTorrents){
|
||||||
|
hashTorrents = currHashTorrs;
|
||||||
|
var html = "";
|
||||||
|
html += '<div class="btn-group-vertical d-flex" role="group">';
|
||||||
|
for(var key in data) {
|
||||||
|
var tor = data[key];
|
||||||
|
html += '<button type="button" class="btn btn-secondary wrap w-100" onclick="setCache(\''+tor.Hash+'\')">'+tor.Name+'</button>';
|
||||||
|
}
|
||||||
|
html += '</div>'
|
||||||
|
$("#torrents").empty();
|
||||||
|
$(html).appendTo($("#torrents"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCache(){
|
||||||
|
var cache = $("#cache");
|
||||||
|
if (cacheHash!=""){
|
||||||
|
cacheTorrent(cacheHash, function(data){
|
||||||
|
var html = "";
|
||||||
|
var st = data;
|
||||||
|
html += '<span>Hash: '+st.Hash+'</span><br>';
|
||||||
|
html += '<span>Capacity: '+humanizeSize(st.Capacity)+'</span><br>';
|
||||||
|
html += '<span>Filled: '+humanizeSize(st.Filled)+'</span><br>';
|
||||||
|
html += '<span>Pieces length: '+humanizeSize(st.PiecesLength)+'</span><br>';
|
||||||
|
html += '<span>Pieces count: '+st.PiecesCount+'</span><br>';
|
||||||
|
$("#cacheInfo").html(html);
|
||||||
|
makePieces(st.PiecesCount);
|
||||||
|
for(var i = 0; i < st.PiecesCount; i++) {
|
||||||
|
var color = "silver";
|
||||||
|
var size = "";
|
||||||
|
var piece = st.Pieces[i];
|
||||||
|
if (piece){
|
||||||
|
if (piece.Completed && piece.BufferSize >= st.PiecesLength)
|
||||||
|
color = "green";
|
||||||
|
else if (piece.Completed && piece.BufferSize == 0)
|
||||||
|
color = "silver";
|
||||||
|
else if (!piece.Completed && piece.BufferSize > 0)
|
||||||
|
color = "red";
|
||||||
|
size = ' ' + humanizeSize(piece.BufferSize);
|
||||||
|
}
|
||||||
|
setPiece(i,color,size);
|
||||||
|
}
|
||||||
|
},function(){
|
||||||
|
$("#cacheInfo").empty();
|
||||||
|
cache.empty();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makePieces(len){
|
||||||
|
var cache = $("#cache");
|
||||||
|
if (cache.children().length==len)
|
||||||
|
return;
|
||||||
|
var html = "";
|
||||||
|
for(var i = 0; i < len; i++) {
|
||||||
|
html += '<span class="piece" id="p'+i+'" style="background-color: silver;">'+i+'</span>';
|
||||||
|
}
|
||||||
|
cache.html(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPiece(i, color, size){
|
||||||
|
var piece = $("#p"+i);
|
||||||
|
piece.delay(100).css("background-color",color);
|
||||||
|
piece.text(i+''+size);
|
||||||
|
}
|
||||||
|
|
||||||
|
function contains(arr, elem) {
|
||||||
|
for (var i = 0; i < arr.length; i++) {
|
||||||
|
if (arr[i].Id === elem) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateState(){
|
||||||
|
updateTorrents();
|
||||||
|
updateCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCache(hash){
|
||||||
|
cacheHash = hash;
|
||||||
|
updateCache();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
|
|
||||||
|
func (t *Template) parseCachePage() {
|
||||||
|
parsePage(t, "cachePage", cachePage)
|
||||||
|
}
|
||||||
3
src/server/web/templates/FavIcon.go
Normal file
3
src/server/web/templates/FavIcon.go
Normal file
File diff suppressed because one or more lines are too long
300
src/server/web/templates/MainPage.go
Normal file
300
src/server/web/templates/MainPage.go
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
package templates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"server/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
var mainPage = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link href="` + faviconB64 + `" rel="icon" type="image/x-icon">
|
||||||
|
<script src="/js/api.js"></script>
|
||||||
|
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.1.0/css/all.css" integrity="sha384-lKuwvrZot6UHsBSfcMvOkWwlCMgc0TaWr+30HWe3a4ltaBwTZhyTEggF5tJv8tbt" crossorigin="anonymous">
|
||||||
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
|
||||||
|
<script src="http://code.jquery.com/jquery-1.11.3.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js" integrity="sha384-smHYKdLADwkXOn1EmN1qk/HfnUcbVRZyYmZ4qpPea6sjB/pTJ0euyQp0Mk8ck+5T" crossorigin="anonymous"></script>
|
||||||
|
<title>TorrServer ` + version.Version + `</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<style>
|
||||||
|
.wrap {
|
||||||
|
white-space: normal;
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
margin: 1%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
|
<span class="navbar-brand mx-auto">
|
||||||
|
TorrServer ` + version.Version + `
|
||||||
|
</span>
|
||||||
|
</nav>
|
||||||
|
<div class="content">
|
||||||
|
<div>
|
||||||
|
<label for="magnet">Добавить торрент: </label>
|
||||||
|
<input id="magnet" class="w-100" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<div class="btn-group d-flex" role="group">
|
||||||
|
<button id="buttonAdd" class="btn w-100" onclick="addTorr()"><i class="fas fa-plus"></i> Добавить</button>
|
||||||
|
<button id="buttonUpload" class="btn w-100"><i class="fas fa-file-upload"></i> Загрузить файл</button>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div>
|
||||||
|
<a href="/torrent/playlist.m3u" rel="external" class="btn btn-primary w-100" role="button" ><i class="fas fa-th-list"></i> Плейлист всех торрентов</a>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<h3>Торренты: </h3>
|
||||||
|
<div id="torrents"></div>
|
||||||
|
<br>
|
||||||
|
<div class="btn-group-vertical d-flex" role="group">
|
||||||
|
<a href="/settings" rel="external" class="btn btn-primary w-100" role="button"><i class="fas fa-cog"></i> Настройки</a>
|
||||||
|
<a href="/cache" rel="external" class="btn btn-primary w-100" role="button"><i class="fas fa-info"></i> Кэш</a>
|
||||||
|
<button id="buttonShutdown" class="btn btn-primary w-100" onclick="shutdown()"><i class="fas fa-power-off"></i> Закрыть сервер</button>
|
||||||
|
</div>
|
||||||
|
<form id="uploadForm" style="display:none" action="/torrent/upload" method="post">
|
||||||
|
<input type="file" id="filesUpload" style="display:none" multiple onchange="uploadTorrent()" name="files"/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<footer class="page-footer navbar-dark bg-dark">
|
||||||
|
<span class="navbar-brand d-flex justify-content-center">
|
||||||
|
<a rel="external" style="text-decoration: none;" href="/about">Описание</a>
|
||||||
|
</span>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<div class="modal fade" id="preloadModal" role="dialog">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title wrap" id="preloadName"></h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p id="preloadStatus"></p>
|
||||||
|
<p id="preloadBuffer"></p>
|
||||||
|
<p id="preloadPeers"></p>
|
||||||
|
<p id="preloadSpeed"></p>
|
||||||
|
<div class="progress">
|
||||||
|
<div id="preloadProgress" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%"></div>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<a id="preloadFileLink" role="button" href="" class="btn btn-secondary wrap w-100"></a>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-danger" data-dismiss="modal">Закрыть</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function addTorr(){
|
||||||
|
var magnet = $("#magnet").val();
|
||||||
|
$("#magnet").val("");
|
||||||
|
if(magnet!=""){
|
||||||
|
addTorrent(magnet,true,
|
||||||
|
function( data ) {
|
||||||
|
loadTorrents();
|
||||||
|
},
|
||||||
|
function( data ) {
|
||||||
|
alert(data.responseJSON.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeTorr(hash){
|
||||||
|
if(hash!=""){
|
||||||
|
removeTorrent(hash,
|
||||||
|
function( data ) {
|
||||||
|
loadTorrents();
|
||||||
|
},
|
||||||
|
function( data ) {
|
||||||
|
alert(data.responseJSON.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function shutdown(){
|
||||||
|
shutdownServer(function( data ) {
|
||||||
|
alert(data.responseJSON.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$( document ).ready(function() {
|
||||||
|
watchInfo();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#buttonUpload').click(function() {
|
||||||
|
$('#filesUpload').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
function uploadTorrent() {
|
||||||
|
var form = $("#uploadForm");
|
||||||
|
var formData = new FormData(document.getElementById("uploadForm"));
|
||||||
|
var data = new FormData();
|
||||||
|
$.each($('#filesUpload')[0].files, function(i, file) {
|
||||||
|
data.append('file-'+i, file);
|
||||||
|
});
|
||||||
|
$.ajax({
|
||||||
|
cache: false,
|
||||||
|
processData: false,
|
||||||
|
contentType: false,
|
||||||
|
type: form.attr('method'),
|
||||||
|
url: form.attr('action'),
|
||||||
|
data: data
|
||||||
|
}).done(function(data) {
|
||||||
|
loadTorrents();
|
||||||
|
}).fail(function(data) {
|
||||||
|
alert(data.responseJSON.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#uploadForm').submit(function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
var form = $(this);
|
||||||
|
$.ajax({
|
||||||
|
type: form.attr('method'),
|
||||||
|
url: form.attr('action'),
|
||||||
|
data: form.serialize()
|
||||||
|
}).done(function(data) {
|
||||||
|
loadTorrents();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadTorrents() {
|
||||||
|
listTorrent(
|
||||||
|
function( data ) {
|
||||||
|
var torrents = $("#torrents");
|
||||||
|
torrents.empty();
|
||||||
|
var html = "";
|
||||||
|
var queueInfo = [];
|
||||||
|
for(var key in data) {
|
||||||
|
var tor = data[key];
|
||||||
|
if (tor.Status==1){
|
||||||
|
queueInfo.push(tor);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
html += tor2Html(tor);
|
||||||
|
}
|
||||||
|
if (queueInfo.length>0){
|
||||||
|
html += "<br><hr><h3>Got info: </h3>";
|
||||||
|
for(var key in queueInfo) {
|
||||||
|
var tor = queueInfo[key];
|
||||||
|
html += tor2Html(tor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$(html).appendTo(torrents);
|
||||||
|
},
|
||||||
|
function( data ) {
|
||||||
|
alert(data.responseJSON.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function tor2Html(tor){
|
||||||
|
var html = '';
|
||||||
|
var name = "";
|
||||||
|
if (tor.Status==1)
|
||||||
|
name = tor.Name+' '+humanizeSize(tor.Length)+' '+tor.Hash;
|
||||||
|
else
|
||||||
|
name = tor.Name+' '+humanizeSize(tor.Length);
|
||||||
|
|
||||||
|
html += '<div class="btn-group d-flex" role="group">';
|
||||||
|
html += ' <button type="button" class="btn btn-secondary wrap w-100" data-toggle="collapse" data-target="#info_'+tor.Hash+'">'+name+'</button>';
|
||||||
|
if (tor.Status!=1)
|
||||||
|
html += ' <a role="button" class="btn btn-secondary" href="'+tor.Playlist+'"><i class="fas fa-th-list"></i> Плейлист</a>';
|
||||||
|
else
|
||||||
|
html += ' <button type="button" class="btn btn-secondary" onclick="showPreload(\'\', \''+ tor.Hash +'\');"><i class="fas fa-info"></i></a>';
|
||||||
|
html += ' <button type="button" class="btn btn-secondary" onclick="removeTorrent(\''+tor.Hash+'\');"><i class="fas fa-trash-alt"></i> Удалить</button>';
|
||||||
|
html += '</div>';
|
||||||
|
html += '<div class="collapse" id="info_'+tor.Hash+'">';
|
||||||
|
for(var i in tor.Files){
|
||||||
|
var file = tor.Files[i];
|
||||||
|
var ico = "";
|
||||||
|
if (file.Viewed)
|
||||||
|
ico = '<i class="far fa-eye"></i> ';
|
||||||
|
html += ' <div class="btn-group d-flex" role="group">';
|
||||||
|
html += ' <a role="button" href="'+file.Link+'" class="btn btn-secondary wrap w-100">'+ico+file.Name+" "+humanizeSize(file.Size)+'</a>';
|
||||||
|
html += ' <button type="button" class="btn btn-secondary" onclick="showPreload(\''+ file.Preload +'\', \''+ file.Link +'\', \''+ tor.Hash +'\');"><i class="fas fa-info"></i></button>';
|
||||||
|
html += ' </div>';
|
||||||
|
}
|
||||||
|
html += '<hr></div>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function watchInfo(){
|
||||||
|
var lastTorrentCount = 0;
|
||||||
|
var lastGettingInfo = 0;
|
||||||
|
setInterval(function() {
|
||||||
|
listTorrent(
|
||||||
|
function( data ) {
|
||||||
|
var gettingInfo = 0;
|
||||||
|
for(var key in data) {
|
||||||
|
var tor = data[key];
|
||||||
|
if (tor.Status==1)
|
||||||
|
gettingInfo++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastTorrentCount!=data.length || gettingInfo!=lastGettingInfo){
|
||||||
|
loadTorrents();
|
||||||
|
lastTorrentCount = data.length;
|
||||||
|
lastGettingInfo = gettingInfo;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPreload(preloadlink, fileLink, hash){
|
||||||
|
$('#preloadFileLink').hide(0);
|
||||||
|
$('#preloadFileLink').attr("href","");
|
||||||
|
$('#preloadProgress').width('100%');
|
||||||
|
if (preloadlink!='')
|
||||||
|
preloadTorrent(preloadlink);
|
||||||
|
var ptimer = setInterval(function() {
|
||||||
|
statTorrent(hash,function(data){
|
||||||
|
if (data!=null){
|
||||||
|
$('#preloadStatus').text("Status: " + data.TorrentStatusString);
|
||||||
|
$('#preloadName').text(data.Name);
|
||||||
|
$('#preloadPeers').text("Peers: [" + data.ConnectedSeeders + "] " + data.ActivePeers + " / " + data.TotalPeers);
|
||||||
|
if (data.DownloadSpeed>0)
|
||||||
|
$('#preloadSpeed').text("Speed: "+ humanizeSize(data.DownloadSpeed) + "/Sec");
|
||||||
|
else
|
||||||
|
$('#preloadSpeed').text("Speed:");
|
||||||
|
|
||||||
|
if (data.PreloadSize>0 && data.PreloadedBytes<data.PreloadSize){
|
||||||
|
var prc = data.PreloadedBytes * 100 / data.PreloadSize;
|
||||||
|
if (prc>100) prc = 100;
|
||||||
|
$('#preloadProgress').width(prc+'%');
|
||||||
|
$('#preloadBuffer').text("Loaded: " + humanizeSize(data.PreloadedBytes) + " / " + humanizeSize(data.PreloadSize)+" "+prc+"%");
|
||||||
|
}else{
|
||||||
|
$('#preloadProgress').width('100%');
|
||||||
|
$('#preloadBuffer').text("Loaded: " + humanizeSize(data.BytesReadUsefulData));
|
||||||
|
$('#preloadProgress').width('100%');
|
||||||
|
if (data.BytesReadUsefulData>0 && fileLink && !$('#preloadFileLink').attr("href")){
|
||||||
|
$('#preloadFileLink').text(data.Name);
|
||||||
|
$('#preloadFileLink').attr("href", fileLink);
|
||||||
|
$('#preloadFileLink').show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},function(){
|
||||||
|
$('#preloadModal').modal('hide');
|
||||||
|
})
|
||||||
|
}, 500);
|
||||||
|
$('#preloadModal').modal('show');
|
||||||
|
$("#preloadModal").on('hidden.bs.modal', function () {
|
||||||
|
clearInterval(ptimer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
|
|
||||||
|
func (t *Template) parseMainPage() {
|
||||||
|
parsePage(t, "mainPage", mainPage)
|
||||||
|
}
|
||||||
200
src/server/web/templates/SettingsPage.go
Normal file
200
src/server/web/templates/SettingsPage.go
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
package templates
|
||||||
|
|
||||||
|
import "server/version"
|
||||||
|
|
||||||
|
var settingsPage = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link href="` + faviconB64 + `" rel="icon" type="image/x-icon">
|
||||||
|
<script src="/js/api.js"></script>
|
||||||
|
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.1.0/css/all.css" integrity="sha384-lKuwvrZot6UHsBSfcMvOkWwlCMgc0TaWr+30HWe3a4ltaBwTZhyTEggF5tJv8tbt" crossorigin="anonymous">
|
||||||
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
|
||||||
|
<script src="http://code.jquery.com/jquery-1.11.3.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js" integrity="sha384-smHYKdLADwkXOn1EmN1qk/HfnUcbVRZyYmZ4qpPea6sjB/pTJ0euyQp0Mk8ck+5T" crossorigin="anonymous"></script>
|
||||||
|
<title>TorrServer ` + version.Version + ` Settings</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<style>
|
||||||
|
.wrap {
|
||||||
|
white-space: normal;
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
margin: 1%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
|
<a class="btn navbar-btn pull-left" href="/"><i class="fas fa-arrow-left"></i></a>
|
||||||
|
<span class="navbar-brand mx-auto">
|
||||||
|
TorrServer ` + version.Version + `
|
||||||
|
</span>
|
||||||
|
</nav>
|
||||||
|
<div class="content">
|
||||||
|
<form id="settings">
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<div class="input-group-text">Размер кэша</div>
|
||||||
|
</div>
|
||||||
|
<input id="CacheSize" class="form-control" type="number" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<div class="input-group-text">Размер буфера предзагрузки</div>
|
||||||
|
</div>
|
||||||
|
<input id="PreloadBufferSize" class="form-control" type="number" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<small class="form-text text-muted">Размеры кэша и буфера указываются в мегабайтах</small>
|
||||||
|
<br>
|
||||||
|
<div class="form-check">
|
||||||
|
<input id="DisableTCP" class="form-check-input" type="checkbox" autocomplete="off">
|
||||||
|
<label for="DisableTCP">Отключить TCP</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input id="DisableUTP" class="form-check-input" type="checkbox" autocomplete="off">
|
||||||
|
<label for="DisableUTP">Отключить UTP</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input id="DisableUPNP" class="form-check-input" type="checkbox" autocomplete="off">
|
||||||
|
<label for="DisableUPNP">Отключить UPNP</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input id="DisableDHT" class="form-check-input" type="checkbox" autocomplete="off">
|
||||||
|
<label for="DisableDHT">Отключить DHT</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input id="DisableUpload" class="form-check-input" type="checkbox" autocomplete="off">
|
||||||
|
<label for="DisableUpload">Отключить Отдачу</label>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<div class="input-group-text">Шифрование</div>
|
||||||
|
</div>
|
||||||
|
<select id="Encryption" class="form-control">
|
||||||
|
<option value="0">По умолчанию</option>
|
||||||
|
<option value="1">Отключить</option>
|
||||||
|
<option value="2">Принудительно</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<div class="input-group-text">Количество соединений</div>
|
||||||
|
</div>
|
||||||
|
<input id="ConnectionsLimit" class="form-control" type="number" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<div class="input-group-text">Ограничение загрузки</div>
|
||||||
|
</div>
|
||||||
|
<input id="DownloadRateLimit" class="form-control" type="number" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<div class="input-group-text">Ограничение отдачи</div>
|
||||||
|
</div>
|
||||||
|
<input id="UploadRateLimit" class="form-control" type="number" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<small class="form-text text-muted">Ограничение устанавливается в Килобайтах, 0 - не ограничивать</small>
|
||||||
|
<br>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<div class="input-group-text">Ретрекеры</div>
|
||||||
|
</div>
|
||||||
|
<select id="RetrackersMode" class="form-control">
|
||||||
|
<option value="0">Оставить как есть</option>
|
||||||
|
<option value="1">Добавить</option>
|
||||||
|
<option value="2">Удалить</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<br>
|
||||||
|
<div class="btn-group d-flex" role="group">
|
||||||
|
<button id="buttonSave" class="btn btn-primary w-100" data-icon="check" onclick="saveSettings()"><i class="far fa-save"></i> Сохранить</button>
|
||||||
|
<button id="buttonRefresh" class="btn btn-primary w-100" data-icon="refresh" onclick="refreshSettings()"><i class="fas fa-sync-alt"></i> Получить с сервера</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer class="page-footer navbar-dark bg-dark">
|
||||||
|
<span class="navbar-brand d-flex justify-content-center">
|
||||||
|
<a rel="external" style="text-decoration: none;" href="/about">Описание</a>
|
||||||
|
</span>
|
||||||
|
</footer>
|
||||||
|
<script>
|
||||||
|
function saveSettings() {
|
||||||
|
var data = {};
|
||||||
|
data.CacheSize = Number($('#CacheSize').val())*(1024*1024);
|
||||||
|
data.PreloadBufferSize = Number($('#PreloadBufferSize').val())*(1024*1024);
|
||||||
|
|
||||||
|
data.DisableTCP = $('#DisableTCP').prop('checked');
|
||||||
|
data.DisableUTP = $('#DisableUTP').prop('checked');
|
||||||
|
data.DisableUPNP = $('#DisableUPNP').prop('checked');
|
||||||
|
data.DisableDHT = $('#DisableDHT').prop('checked');
|
||||||
|
data.DisableUpload = $('#DisableUpload').prop('checked');
|
||||||
|
data.Encryption = Number($('#Encryption').val());
|
||||||
|
|
||||||
|
data.ConnectionsLimit = Number($('#ConnectionsLimit').val());
|
||||||
|
|
||||||
|
data.DownloadRateLimit = Number($('#DownloadRateLimit').val());
|
||||||
|
data.UploadRateLimit = Number($('#UploadRateLimit').val());
|
||||||
|
|
||||||
|
data.RetrackersMode = Number($('#RetrackersMode').val());
|
||||||
|
|
||||||
|
$.post("/settings/write", JSON.stringify(data))
|
||||||
|
.done(function(data) {
|
||||||
|
restartService();
|
||||||
|
alert(data);
|
||||||
|
})
|
||||||
|
.fail(function(data) {
|
||||||
|
alert(data.responseJSON.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshSettings() {
|
||||||
|
$.post("/settings/read")
|
||||||
|
.done(function(data) {
|
||||||
|
$('#CacheSize').val(data.CacheSize/(1024*1024));
|
||||||
|
$('#PreloadBufferSize').val(data.PreloadBufferSize/(1024*1024));
|
||||||
|
|
||||||
|
$('#DisableTCP').prop('checked', data.DisableTCP);
|
||||||
|
$('#DisableUTP').prop('checked', data.DisableUTP);
|
||||||
|
$('#DisableUPNP').prop('checked', data.DisableUPNP);
|
||||||
|
$('#DisableDHT').prop('checked', data.DisableDHT);
|
||||||
|
$('#DisableUpload').prop('checked', data.DisableUpload);
|
||||||
|
$('#Encryption').val(data.Encryption);
|
||||||
|
|
||||||
|
$('#ConnectionsLimit').val(data.ConnectionsLimit);
|
||||||
|
|
||||||
|
$('#DownloadRateLimit').val(data.DownloadRateLimit);
|
||||||
|
$('#UploadRateLimit').val(data.UploadRateLimit);
|
||||||
|
|
||||||
|
$('#RetrackersMode').val(data.RetrackersMode);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
refreshSettings();
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on("wheel", "input[type=number]", function (e) {
|
||||||
|
$(this).blur();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
`
|
||||||
|
|
||||||
|
func (t *Template) parseSettingsPage() {
|
||||||
|
parsePage(t, "settingsPage", settingsPage)
|
||||||
|
}
|
||||||
45
src/server/web/templates/Template.go
Normal file
45
src/server/web/templates/Template.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package templates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/labstack/echo"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Template struct {
|
||||||
|
templates *template.Template
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitTemplate(e *echo.Echo) {
|
||||||
|
temp := new(Template)
|
||||||
|
|
||||||
|
temp.parseMainPage()
|
||||||
|
temp.parseSettingsPage()
|
||||||
|
temp.parseAboutPage()
|
||||||
|
temp.parseCachePage()
|
||||||
|
|
||||||
|
e.Renderer = temp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
|
||||||
|
return t.templates.ExecuteTemplate(w, name, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePage(temp *Template, name, page string) error {
|
||||||
|
s := page
|
||||||
|
var tmpl *template.Template
|
||||||
|
if temp.templates == nil {
|
||||||
|
temp.templates = template.New(name)
|
||||||
|
}
|
||||||
|
if name == temp.templates.Name() {
|
||||||
|
tmpl = temp.templates
|
||||||
|
} else {
|
||||||
|
tmpl = temp.templates.New(name)
|
||||||
|
}
|
||||||
|
_, err := tmpl.Parse(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
133
src/server/web/templates/api_js.go
Normal file
133
src/server/web/templates/api_js.go
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
package templates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"server/settings"
|
||||||
|
"server/web/helpers"
|
||||||
|
|
||||||
|
"github.com/labstack/echo"
|
||||||
|
)
|
||||||
|
|
||||||
|
var apijs = `
|
||||||
|
function addTorrent(link, save, info, done, fail){
|
||||||
|
var reqJson = JSON.stringify({ Link: link, Info: info, DontSave: !save});
|
||||||
|
$.post('/torrent/add',reqJson)
|
||||||
|
.done(function( data ) {
|
||||||
|
if (done)
|
||||||
|
done(data);
|
||||||
|
})
|
||||||
|
.fail(function( data ) {
|
||||||
|
if (fail)
|
||||||
|
fail(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTorrent(hash, done, fail){
|
||||||
|
var reqJson = JSON.stringify({ Hash: hash});
|
||||||
|
$.post('/torrent/get',reqJson)
|
||||||
|
.done(function( data ) {
|
||||||
|
if (done)
|
||||||
|
done(data);
|
||||||
|
})
|
||||||
|
.fail(function( data ) {
|
||||||
|
if (fail)
|
||||||
|
fail(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeTorrent(hash, done, fail){
|
||||||
|
var reqJson = JSON.stringify({ Hash: hash});
|
||||||
|
$.post('/torrent/rem',reqJson)
|
||||||
|
.done(function( data ) {
|
||||||
|
if (done)
|
||||||
|
done(data);
|
||||||
|
})
|
||||||
|
.fail(function( data ) {
|
||||||
|
if (fail)
|
||||||
|
fail(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function statTorrent(hash, done, fail){
|
||||||
|
var reqJson = JSON.stringify({ Hash: hash});
|
||||||
|
$.post('/torrent/stat',reqJson)
|
||||||
|
.done(function( data ) {
|
||||||
|
if (done)
|
||||||
|
done(data);
|
||||||
|
})
|
||||||
|
.fail(function( data ) {
|
||||||
|
if (fail)
|
||||||
|
fail(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function cacheTorrent(hash, done, fail){
|
||||||
|
var reqJson = JSON.stringify({ Hash: hash});
|
||||||
|
$.post('/torrent/cache',reqJson)
|
||||||
|
.done(function( data ) {
|
||||||
|
if (done)
|
||||||
|
done(data);
|
||||||
|
})
|
||||||
|
.fail(function( data ) {
|
||||||
|
if (fail)
|
||||||
|
fail(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function listTorrent(done, fail){
|
||||||
|
$.post('/torrent/list')
|
||||||
|
.done(function( data ) {
|
||||||
|
if (done)
|
||||||
|
done(data);
|
||||||
|
})
|
||||||
|
.fail(function( data ) {
|
||||||
|
if (fail)
|
||||||
|
fail(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function restartService(done, fail){
|
||||||
|
$.get('/torrent/restart')
|
||||||
|
.done(function( data ) {
|
||||||
|
if (done)
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.fail(function( data ) {
|
||||||
|
if (fail)
|
||||||
|
fail(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function preloadTorrent(preloadLink, done, fail){
|
||||||
|
$.get(preloadLink)
|
||||||
|
.done(function( data ) {
|
||||||
|
if (done)
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.fail(function( data ) {
|
||||||
|
if (fail)
|
||||||
|
fail(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function shutdownServer(fail){
|
||||||
|
$.post('/shutdown')
|
||||||
|
.fail(function( data ) {
|
||||||
|
if (fail)
|
||||||
|
fail(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function humanizeSize(size) {
|
||||||
|
if (typeof size == 'undefined' || size == 0)
|
||||||
|
return "";
|
||||||
|
var i = Math.floor( Math.log(size) / Math.log(1024) );
|
||||||
|
return ( size / Math.pow(1024, i) ).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i];
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
func Api_JS(c echo.Context) error {
|
||||||
|
http.ServeContent(c.Response(), c.Request(), "api.js", settings.StartTime, helpers.NewSeekingBuffer(apijs))
|
||||||
|
return c.NoContent(http.StatusOK)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user