mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
Add nuvio-providers as submodule
This commit is contained in:
parent
1266e482da
commit
3071aeb29f
15 changed files with 4 additions and 8502 deletions
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[submodule "local-scrapers-repo"]
|
||||
path = local-scrapers-repo
|
||||
url = https://github.com/tapframe/nuvio-providers.git
|
||||
1
local-scrapers-repo
Submodule
1
local-scrapers-repo
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit e48fd1a255b90cac10cd60fdddab8410783434a1
|
||||
|
|
@ -1,674 +0,0 @@
|
|||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://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 <https://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
|
||||
<https://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
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,150 +0,0 @@
|
|||
{
|
||||
"name": "Tapframe's Repo",
|
||||
"version": "1.0.0",
|
||||
"scrapers": [
|
||||
{
|
||||
|
||||
|
||||
"id": "4khdhub",
|
||||
"name": "4KHDHub",
|
||||
"description": "4KHDHub direct links",
|
||||
"version": "1.0.1",
|
||||
"author": "Nuvio Team",
|
||||
"supportedTypes": [
|
||||
"movie",
|
||||
"tv"
|
||||
],
|
||||
"filename": "providers/4khdhub.js",
|
||||
"enabled": true,
|
||||
"formats": ["mkv"],
|
||||
"logo": "https://4khdhub.fans/favicon.ico",
|
||||
"contentLanguage": [
|
||||
"en"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "uhdmovies",
|
||||
"name": "UHDMovies",
|
||||
"description": "High-quality movie and TV show streams from UHD Movies",
|
||||
"version": "1.0.1",
|
||||
"author": "Nuvio Team",
|
||||
"supportedTypes": [
|
||||
"movie",
|
||||
"tv"
|
||||
],
|
||||
"filename": "providers/uhdmovies.js",
|
||||
"enabled": true,
|
||||
"formats": ["mkv"],
|
||||
"logo": "https://uhdmovies.cat/wp-content/uploads/2021/03/cropped-output-onlinepngtools-1-32x32.png",
|
||||
"contentLanguage": [
|
||||
"en"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "moviesmod",
|
||||
"name": "MoviesMod",
|
||||
"description": "Movie and TV show streams from MoviesMod with multiple quality options",
|
||||
"version": "1.0.1",
|
||||
"author": "Nuvio Team",
|
||||
"supportedTypes": [
|
||||
"movie",
|
||||
"tv"
|
||||
],
|
||||
"filename": "providers/moviesmod.js",
|
||||
"enabled": true,
|
||||
"formats": ["mkv"],
|
||||
"logo": "https://moviesmod.tube/wp-content/uploads/2022/10/moviesmod.png",
|
||||
"contentLanguage": [
|
||||
"en"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "hdrezka",
|
||||
"name": "HDRezka",
|
||||
"description": "High-quality movie and TV show streams",
|
||||
"version": "1.0.0",
|
||||
"author": "Nuvio Team",
|
||||
"supportedTypes": [
|
||||
"movie",
|
||||
"tv"
|
||||
],
|
||||
"filename": "providers/hdrezka.js",
|
||||
"enabled": true,
|
||||
"logo": "https://hdrezka.ag/templates/hdrezka/images/favicon.ico",
|
||||
"contentLanguage": [
|
||||
"ru",
|
||||
"en"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "dahmermovies",
|
||||
"name": "DahmerMovies",
|
||||
"description": "High-quality movie and TV show streams from Dahmer Movies, Can be slow at times to fetch and stream. Only use if needed.",
|
||||
"version": "1.0.0",
|
||||
"author": "Nuvio Team",
|
||||
"supportedTypes": [
|
||||
"movie",
|
||||
"tv"
|
||||
],
|
||||
"filename": "providers/dahmermovies.js",
|
||||
"enabled": true,
|
||||
"formats": ["mkv"],
|
||||
"logo": "https://image.similarpng.com/file/similarpng/very-thumbnail/2021/05/Letter-D-logo-design-template-with-geometric-shape-style-on-transparent-background-PNG.png",
|
||||
"contentLanguage": [
|
||||
"en"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "watch32",
|
||||
"name": "Watch32",
|
||||
"description": "Lightweight M3U8 Links",
|
||||
"version": "1.0.1",
|
||||
"author": "Nuvio Team",
|
||||
"supportedTypes": [
|
||||
"movie",
|
||||
"tv"
|
||||
],
|
||||
"filename": "providers/watch32.js",
|
||||
"enabled": true,
|
||||
"logo": "https://img.watch32.sx/xxrz/400x400/100/a9/5e/a95e15a880a9df3c045f6a5224daf576/a95e15a880a9df3c045f6a5224daf576.png",
|
||||
"contentLanguage": [
|
||||
"en"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "xprime",
|
||||
"name": "Xprime",
|
||||
"description": "Faster Streaming links from Xprime.tv",
|
||||
"version": "1.0.0",
|
||||
"author": "Nuvio Team",
|
||||
"supportedTypes": [
|
||||
"movie",
|
||||
"tv"
|
||||
],
|
||||
"filename": "providers/xprime.js",
|
||||
"enabled": true,
|
||||
"logo": "https://xprime.tv/favicon/favicon.ico",
|
||||
"contentLanguage": [
|
||||
"en"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "netmirror",
|
||||
"name": "NetMirror",
|
||||
"description": "Streaming links from netmirror. Automatically disabled on iOS due to compatibility issues.",
|
||||
"version": "1.0.1",
|
||||
"author": "Nuvio Team",
|
||||
"supportedTypes": [
|
||||
"movie",
|
||||
"tv"
|
||||
],
|
||||
"filename": "providers/netmirror.js",
|
||||
"enabled": true,
|
||||
"logo": "https://net2025.cc/favicon.ico",
|
||||
"contentLanguage": [
|
||||
"en"
|
||||
],
|
||||
"disabledPlatforms": ["ios"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,774 +0,0 @@
|
|||
// 4KHDHub Scraper for Nuvio Local Scrapers
|
||||
// React Native compatible – no Node core modules, no async/await
|
||||
|
||||
const cheerio = require('cheerio-without-node-native');
|
||||
console.log('[4KHDHub] Using cheerio-without-node-native for DOM parsing');
|
||||
|
||||
// Constants
|
||||
const TMDB_API_KEY = '439c478a771f35c05022f9feabcca01c';
|
||||
const DOMAINS_URL = 'https://raw.githubusercontent.com/phisher98/TVVVV/refs/heads/main/domains.json';
|
||||
|
||||
// Caches (in-memory only)
|
||||
let domainsCache = null;
|
||||
let resolvedUrlsCache = {}; // key -> array of resolved file-host URLs
|
||||
|
||||
// Headers
|
||||
const DEFAULT_HEADERS = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US,en;q=0.5',
|
||||
'Connection': 'keep-alive'
|
||||
};
|
||||
|
||||
// Polyfill atob/btoa for Node test environments if missing (kept lightweight, no imports)
|
||||
if (typeof atob === 'undefined') {
|
||||
try {
|
||||
// eslint-disable-next-line no-undef
|
||||
global.atob = function (b64) { return Buffer.from(b64, 'base64').toString('binary'); };
|
||||
} catch (e) {
|
||||
// ignore for RN
|
||||
}
|
||||
}
|
||||
if (typeof btoa === 'undefined') {
|
||||
try {
|
||||
// eslint-disable-next-line no-undef
|
||||
global.btoa = function (str) { return Buffer.from(str, 'binary').toString('base64'); };
|
||||
} catch (e) {
|
||||
// ignore for RN
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: HTTP
|
||||
function makeRequest(url, options = {}) {
|
||||
return fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
...DEFAULT_HEADERS,
|
||||
...(options.headers || {})
|
||||
}
|
||||
}).then(function (response) {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
// Base64 and misc helpers (RN-safe)
|
||||
function base64Decode(str) {
|
||||
try {
|
||||
// Convert base64 -> binary string -> UTF-8
|
||||
// escape/unescape is deprecated but works in RN environments for this use case
|
||||
// eslint-disable-next-line no-undef
|
||||
return decodeURIComponent(escape(atob(str)));
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function base64Encode(str) {
|
||||
try {
|
||||
// eslint-disable-next-line no-undef
|
||||
return btoa(unescape(encodeURIComponent(str)));
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function rot13(str) {
|
||||
return (str || '').replace(/[A-Za-z]/g, function (char) {
|
||||
var start = char <= 'Z' ? 65 : 97;
|
||||
return String.fromCharCode(((char.charCodeAt(0) - start + 13) % 26) + start);
|
||||
});
|
||||
}
|
||||
|
||||
function decodeFilename(filename) {
|
||||
if (!filename) return filename;
|
||||
try {
|
||||
var decoded = filename;
|
||||
if (decoded.indexOf('UTF-8') === 0) {
|
||||
decoded = decoded.substring(5);
|
||||
}
|
||||
return decodeURIComponent(decoded);
|
||||
} catch (e) {
|
||||
return filename;
|
||||
}
|
||||
}
|
||||
|
||||
function getIndexQuality(str) {
|
||||
var match = (str || '').match(/(\d{3,4})[pP]/);
|
||||
return match ? parseInt(match[1], 10) : 2160;
|
||||
}
|
||||
|
||||
function cleanTitle(title) {
|
||||
var decodedTitle = decodeFilename(title || '');
|
||||
var parts = decodedTitle.split(/[.\-_]/);
|
||||
var qualityTags = ['WEBRip','WEB-DL','WEB','BluRay','HDRip','DVDRip','HDTV','CAM','TS','R5','DVDScr','BRRip','BDRip','DVD','PDTV','HD'];
|
||||
var audioTags = ['AAC','AC3','DTS','MP3','FLAC','DD5','EAC3','Atmos'];
|
||||
var subTags = ['ESub','ESubs','Subs','MultiSub','NoSub','EnglishSub','HindiSub'];
|
||||
var codecTags = ['x264','x265','H264','HEVC','AVC'];
|
||||
|
||||
var startIndex = parts.findIndex(function (part) {
|
||||
return qualityTags.some(function (tag) { return part.toLowerCase().indexOf(tag.toLowerCase()) !== -1; });
|
||||
});
|
||||
|
||||
var endIndex = parts.map(function (part, index) {
|
||||
var hasTag = subTags.concat(audioTags).concat(codecTags).some(function (tag) {
|
||||
return part.toLowerCase().indexOf(tag.toLowerCase()) !== -1;
|
||||
});
|
||||
return hasTag ? index : -1;
|
||||
}).filter(function (i) { return i !== -1; }).pop() || -1;
|
||||
|
||||
if (startIndex !== -1 && endIndex !== -1 && endIndex >= startIndex) {
|
||||
return parts.slice(startIndex, endIndex + 1).join('.');
|
||||
} else if (startIndex !== -1) {
|
||||
return parts.slice(startIndex).join('.');
|
||||
} else {
|
||||
return parts.slice(-3).join('.');
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeTitle(title) {
|
||||
return (title || '')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s]/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function calculateSimilarity(str1, str2) {
|
||||
var s1 = normalizeTitle(str1);
|
||||
var s2 = normalizeTitle(str2);
|
||||
if (s1 === s2) return 1.0;
|
||||
var len1 = s1.length;
|
||||
var len2 = s2.length;
|
||||
if (len1 === 0) return len2 === 0 ? 1.0 : 0.0;
|
||||
if (len2 === 0) return 0.0;
|
||||
var matrix = Array(len1 + 1).fill(null).map(function () { return Array(len2 + 1).fill(0); });
|
||||
for (var i = 0; i <= len1; i++) matrix[i][0] = i;
|
||||
for (var j = 0; j <= len2; j++) matrix[0][j] = j;
|
||||
for (i = 1; i <= len1; i++) {
|
||||
for (j = 1; j <= len2; j++) {
|
||||
var cost = s1[i - 1] === s2[j - 1] ? 0 : 1;
|
||||
matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost);
|
||||
}
|
||||
}
|
||||
var maxLen = Math.max(len1, len2);
|
||||
return (maxLen - matrix[len1][len2]) / maxLen;
|
||||
}
|
||||
|
||||
function findBestMatch(results, query) {
|
||||
if (!results || results.length === 0) return null;
|
||||
if (results.length === 1) return results[0];
|
||||
var scored = results.map(function (r) {
|
||||
var score = 0;
|
||||
if (normalizeTitle(r.title) === normalizeTitle(query)) score += 100;
|
||||
var sim = calculateSimilarity(r.title, query); score += sim * 50;
|
||||
if (normalizeTitle(r.title).indexOf(normalizeTitle(query)) !== -1) score += 15; // quick containment bonus
|
||||
var lengthDiff = Math.abs(r.title.length - query.length);
|
||||
score += Math.max(0, 10 - lengthDiff / 5);
|
||||
if (/(19|20)\d{2}/.test(r.title)) score += 5;
|
||||
return { item: r, score: score };
|
||||
});
|
||||
scored.sort(function (a, b) { return b.score - a.score; });
|
||||
return scored[0].item;
|
||||
}
|
||||
|
||||
// URL utils – replicate UHDMovies validation style
|
||||
function validateVideoUrl(url, timeout) {
|
||||
console.log('[4KHDHub] Validating URL: ' + (url.substring(0, 100)) + '...');
|
||||
return fetch(url, {
|
||||
method: 'HEAD',
|
||||
headers: {
|
||||
'Range': 'bytes=0-1',
|
||||
'User-Agent': DEFAULT_HEADERS['User-Agent']
|
||||
}
|
||||
}).then(function (response) {
|
||||
if (response.ok || response.status === 206) {
|
||||
console.log('[4KHDHub] ✓ URL validation successful (' + response.status + ')');
|
||||
return true;
|
||||
} else {
|
||||
console.log('[4KHDHub] ✗ URL validation failed with status: ' + response.status);
|
||||
return false;
|
||||
}
|
||||
}).catch(function (error) {
|
||||
console.log('[4KHDHub] ✗ URL validation failed: ' + (error && error.message));
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function getFilenameFromUrl(url) {
|
||||
try {
|
||||
return fetch(url, {
|
||||
method: 'HEAD',
|
||||
headers: { 'User-Agent': DEFAULT_HEADERS['User-Agent'] }
|
||||
}).then(function (res) {
|
||||
var cd = res.headers.get('content-disposition');
|
||||
var filename = null;
|
||||
if (cd) {
|
||||
var match = cd.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/i);
|
||||
if (match && match[1]) filename = (match[1] || '').replace(/["']/g, '');
|
||||
}
|
||||
if (!filename) {
|
||||
try {
|
||||
var uo = new URL(url);
|
||||
filename = uo.pathname.split('/').pop() || '';
|
||||
if (filename && filename.indexOf('.') !== -1) filename = filename.replace(/\.[^.]+$/, '');
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
var decoded = decodeFilename(filename || '');
|
||||
return decoded || null;
|
||||
}).catch(function () { return null; });
|
||||
} catch (e) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
// Domains
|
||||
function getDomains() {
|
||||
if (domainsCache) return Promise.resolve(domainsCache);
|
||||
return makeRequest(DOMAINS_URL).then(function (res) { return res.json(); }).then(function (data) {
|
||||
domainsCache = data;
|
||||
return domainsCache;
|
||||
}).catch(function () { return null; });
|
||||
}
|
||||
|
||||
// Resolve redirect link style used by 4KHDHub
|
||||
function getRedirectLinks(url) {
|
||||
return makeRequest(url).then(function (res) { return res.text(); }).then(function (html) {
|
||||
var regex = /s\('o','([A-Za-z0-9+/=]+)'|ck\('_wp_http_\d+','([^']+)'/g;
|
||||
var combined = '';
|
||||
var m;
|
||||
while ((m = regex.exec(html)) !== null) {
|
||||
var val = m[1] || m[2];
|
||||
if (val) combined += val;
|
||||
}
|
||||
try {
|
||||
var decoded = base64Decode(rot13(base64Decode(base64Decode(combined))));
|
||||
var obj = JSON.parse(decoded);
|
||||
var encodedurl = base64Decode(obj.o || '').trim();
|
||||
var data = base64Decode(obj.data || '').trim();
|
||||
var blog = (obj.blog_url || '').trim();
|
||||
if (encodedurl) return encodedurl;
|
||||
if (blog && data) {
|
||||
return makeRequest(blog + '?re=' + data).then(function (r) { return r.text(); }).then(function (txt) { return (txt || '').trim(); }).catch(function () { return ''; });
|
||||
}
|
||||
return '';
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}).catch(function () { return ''; });
|
||||
}
|
||||
|
||||
// Search content
|
||||
function searchContent(query) {
|
||||
return getDomains().then(function (domains) {
|
||||
if (!domains || !domains['4khdhub']) throw new Error('Failed to get domain information');
|
||||
var baseUrl = domains['4khdhub'];
|
||||
var searchUrl = baseUrl + '/?s=' + encodeURIComponent(query);
|
||||
return makeRequest(searchUrl).then(function (res) { return res.text(); }).then(function (html) {
|
||||
var $ = cheerio.load(html);
|
||||
var results = [];
|
||||
|
||||
// Primary parsing for new movie-card structure
|
||||
$('a').each(function (i, el) {
|
||||
var $el = $(el);
|
||||
var title = $el.find('h3.movie-card-title').text().trim();
|
||||
var href = $el.attr('href');
|
||||
var poster = $el.find('img').attr('src') || '';
|
||||
var year = $el.find('p.movie-card-meta').text().trim();
|
||||
|
||||
if (title && href) {
|
||||
var absoluteUrl = href.indexOf('http') === 0 ? href : (baseUrl + (href.indexOf('/') === 0 ? '' : '/') + href);
|
||||
results.push({ title: title, url: absoluteUrl, poster: poster, year: year });
|
||||
}
|
||||
});
|
||||
|
||||
// Fallback parsing for legacy card-grid structure
|
||||
if (results.length === 0) {
|
||||
$('div.card-grid a').each(function (i, el) {
|
||||
var $el = $(el);
|
||||
var title = $el.find('h3').text().trim();
|
||||
var href = $el.attr('href');
|
||||
var poster = $el.find('img').attr('src') || '';
|
||||
if (title && href) {
|
||||
var absoluteUrl = href.indexOf('http') === 0 ? href : (baseUrl + (href.indexOf('/') === 0 ? '' : '/') + href);
|
||||
results.push({ title: title, url: absoluteUrl, poster: poster });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Final fallback for general anchors
|
||||
if (results.length === 0) {
|
||||
$('a[href]').each(function (i, el) {
|
||||
var $el2 = $(el);
|
||||
var h = $el2.attr('href') || '';
|
||||
var t = ($el2.text() || '').trim();
|
||||
if (t && h && /\/\d{4}\//.test(h)) {
|
||||
var abs = h.indexOf('http') === 0 ? h : (baseUrl + (h.indexOf('/') === 0 ? '' : '/') + h);
|
||||
results.push({ title: t, url: abs, poster: '' });
|
||||
}
|
||||
});
|
||||
}
|
||||
return results;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Load content page and collect download links (and episodes for TV)
|
||||
function loadContent(url) {
|
||||
return makeRequest(url).then(function (res) { return res.text(); }).then(function (html) {
|
||||
var $ = cheerio.load(html);
|
||||
var title = ($('h1.page-title').text() || '').split('(')[0].trim();
|
||||
var poster = $('meta[property="og:image"]').attr('content') || '';
|
||||
var tags = [];
|
||||
$('div.mt-2 span.badge').each(function (i, el) { tags.push($(el).text()); });
|
||||
var year = parseInt(($('div.mt-2 span').first().text() || '').replace(/[^0-9]/g, ''), 10) || null;
|
||||
var description = $('div.content-section p.mt-4').text().trim() || '';
|
||||
var trailer = $('#trailer-btn').attr('data-trailer-url') || '';
|
||||
var isMovie = tags.indexOf('Movies') !== -1;
|
||||
|
||||
// Collect all relevant links across multiple selectors (do not stop at first)
|
||||
var hrefsSet = new Set();
|
||||
var selectors = [
|
||||
'div.download-item a',
|
||||
'.download-item a',
|
||||
'a[href*="hubdrive"]',
|
||||
'a[href*="hubcloud"]',
|
||||
'a[href*="pixeldrain"]',
|
||||
'a[href*="buzz"]',
|
||||
'a[href*="10gbps"]',
|
||||
'a[href*="drive"]',
|
||||
'a.btn[href]',
|
||||
'a.btn',
|
||||
'a[href]'
|
||||
];
|
||||
for (var s = 0; s < selectors.length; s++) {
|
||||
$(selectors[s]).each(function (i, el) {
|
||||
var h = ($(el).attr('href') || '').trim();
|
||||
if (!h) return;
|
||||
// Keep only plausible download/intermediate links
|
||||
var keep = /hubdrive|hubcloud|pixeldrain|buzz|10gbps|workers\.dev|r2\.dev|id=|download|s3|fsl/i.test(h);
|
||||
if (keep) hrefsSet.add(h);
|
||||
});
|
||||
}
|
||||
var hrefs = Array.from(hrefsSet);
|
||||
|
||||
var content = { title: title, poster: poster, tags: tags, year: year, description: description, trailer: trailer, type: isMovie ? 'movie' : 'series' };
|
||||
if (isMovie) {
|
||||
content.downloadLinks = hrefs;
|
||||
return content;
|
||||
}
|
||||
|
||||
// Series handling (best-effort; falls back to general links)
|
||||
var episodesMap = {};
|
||||
$('div.episodes-list div.season-item').each(function (i, seasonEl) {
|
||||
var $season = $(seasonEl);
|
||||
var seasonText = $season.find('div.episode-number').text() || '';
|
||||
var seasonMatch = seasonText.match(/S?([1-9][0-9]*)/);
|
||||
var seasonNum = seasonMatch ? parseInt(seasonMatch[1], 10) : null;
|
||||
$season.find('div.episode-download-item').each(function (j, epEl) {
|
||||
var $ep = $(epEl);
|
||||
var epText = $ep.find('div.episode-file-info span.badge-psa').text() || '';
|
||||
var epMatch = epText.match(/Episode-0*([1-9][0-9]*)/);
|
||||
var episodeNum = epMatch ? parseInt(epMatch[1], 10) : null;
|
||||
var epLinks = [];
|
||||
$ep.find('a').each(function (k, a) {
|
||||
var h = $(a).attr('href'); if (h && h.trim()) epLinks.push(h);
|
||||
});
|
||||
if (seasonNum && episodeNum && epLinks.length > 0) {
|
||||
var key = seasonNum + '-' + episodeNum;
|
||||
if (!episodesMap[key]) episodesMap[key] = { season: seasonNum, episode: episodeNum, downloadLinks: [] };
|
||||
episodesMap[key].downloadLinks = episodesMap[key].downloadLinks.concat(epLinks);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var episodes = Object.keys(episodesMap).map(function (k) {
|
||||
var ep = episodesMap[k];
|
||||
ep.downloadLinks = Array.from(new Set(ep.downloadLinks));
|
||||
return ep;
|
||||
});
|
||||
|
||||
if (episodes.length === 0 && hrefs.length > 0) {
|
||||
content.episodes = [{ season: 1, episode: 1, downloadLinks: hrefs }];
|
||||
} else {
|
||||
content.episodes = episodes;
|
||||
}
|
||||
return content;
|
||||
});
|
||||
}
|
||||
|
||||
// Extract HubCloud links -> [{name,title,url,quality}]
|
||||
function extractHubCloudLinks(url, referer) {
|
||||
var origin;
|
||||
try { origin = new URL(url).origin; } catch (e) { origin = ''; }
|
||||
|
||||
function toAbsolute(href, base) {
|
||||
try { return new URL(href, base).href; } catch (e) { return href; }
|
||||
}
|
||||
|
||||
function resolveBuzzServer(buttonLink) {
|
||||
var baseOrigin = (function () { try { return new URL(buttonLink).origin; } catch (e) { return origin; } })();
|
||||
var dlUrl = buttonLink.replace(/\/?$/, '') + '/download';
|
||||
return fetch(dlUrl, { headers: { 'Referer': buttonLink, 'User-Agent': DEFAULT_HEADERS['User-Agent'] }, redirect: 'manual' })
|
||||
.then(function (res) {
|
||||
var hx = res.headers.get('hx-redirect') || res.headers.get('location');
|
||||
if (hx) return toAbsolute(hx, baseOrigin);
|
||||
// Fallback: if manual redirect unsupported, use final response URL
|
||||
return res.url || buttonLink;
|
||||
}).catch(function () { return buttonLink; });
|
||||
}
|
||||
|
||||
function resolveTenGbps(initialLink, headerDetails, size, qualityLabel, quality) {
|
||||
var current = initialLink;
|
||||
var baseOrigin = (function () { try { return new URL(initialLink).origin; } catch (e) { return origin; } })();
|
||||
var maxHops = 6;
|
||||
function step() {
|
||||
return fetch(current, { redirect: 'manual', headers: { 'User-Agent': DEFAULT_HEADERS['User-Agent'] } })
|
||||
.then(function (res) {
|
||||
var loc = res.headers.get('location');
|
||||
if (!loc) {
|
||||
// Try current as final
|
||||
return null;
|
||||
}
|
||||
if (loc.indexOf('id=') !== -1) {
|
||||
var linkParam = (loc.split('link=')[1] || '').trim();
|
||||
if (linkParam) {
|
||||
try { linkParam = decodeURIComponent(linkParam); } catch (e) { /* ignore */ }
|
||||
return linkParam;
|
||||
}
|
||||
return null;
|
||||
} else {
|
||||
current = toAbsolute(loc, baseOrigin);
|
||||
return step();
|
||||
}
|
||||
});
|
||||
}
|
||||
return step().then(function (finalUrl) {
|
||||
if (!finalUrl) return null;
|
||||
return getFilenameFromUrl(finalUrl).then(function (actualFilename) {
|
||||
var displayFilename = actualFilename || headerDetails || 'Unknown';
|
||||
var titleParts = [];
|
||||
if (displayFilename) titleParts.push(displayFilename);
|
||||
if (size) titleParts.push(size);
|
||||
var finalTitle = titleParts.join('\n');
|
||||
return { name: '4KHDHub - 10Gbps Server' + qualityLabel, title: finalTitle, url: finalUrl, quality: quality };
|
||||
}).catch(function () {
|
||||
var displayFilename = headerDetails || 'Unknown';
|
||||
var titleParts = [];
|
||||
if (displayFilename) titleParts.push(displayFilename);
|
||||
if (size) titleParts.push(size);
|
||||
var finalTitle = titleParts.join('\n');
|
||||
return { name: '4KHDHub - 10Gbps Server' + qualityLabel, title: finalTitle, url: finalUrl, quality: quality };
|
||||
});
|
||||
}).catch(function () { return null; });
|
||||
}
|
||||
|
||||
return makeRequest(url).then(function (res) { return res.text(); }).then(function (html) {
|
||||
var $ = cheerio.load(html);
|
||||
var href = url;
|
||||
if (url.indexOf('hubcloud.php') === -1) {
|
||||
var rawHref = $('#download').attr('href') || $('a[href*="hubcloud.php"]').attr('href') || $('.download-btn').attr('href') || $('a[href*="download"]').attr('href');
|
||||
if (!rawHref) throw new Error('Download element not found');
|
||||
href = toAbsolute(rawHref, origin);
|
||||
}
|
||||
return makeRequest(href).then(function (res2) { return res2.text(); }).then(function (html2) {
|
||||
var $$ = cheerio.load(html2);
|
||||
|
||||
function buildTask(buttonText, buttonLink, headerDetails, size, quality) {
|
||||
var qualityLabel = quality ? (' - ' + quality + 'p') : '';
|
||||
// Pixeldrain normalization
|
||||
var pd = buttonLink.match(/pixeldrain\.(?:net|dev)\/u\/([a-zA-Z0-9]+)/);
|
||||
if (pd && pd[1]) buttonLink = 'https://pixeldrain.net/api/file/' + pd[1];
|
||||
|
||||
if (buttonText.indexOf('BuzzServer') !== -1) {
|
||||
return resolveBuzzServer(buttonLink).then(function (finalUrl) {
|
||||
return getFilenameFromUrl(finalUrl).then(function (actualFilename) {
|
||||
var displayFilename = actualFilename || headerDetails || 'Unknown';
|
||||
var titleParts = [];
|
||||
if (displayFilename) titleParts.push(displayFilename);
|
||||
if (size) titleParts.push(size);
|
||||
var finalTitle = titleParts.join('\n');
|
||||
return { name: '4KHDHub - BuzzServer' + qualityLabel, title: finalTitle, url: finalUrl, quality: quality, size: size || null, fileName: actualFilename || null };
|
||||
}).catch(function () {
|
||||
var displayFilename = headerDetails || 'Unknown';
|
||||
var titleParts = [];
|
||||
if (displayFilename) titleParts.push(displayFilename);
|
||||
if (size) titleParts.push(size);
|
||||
var finalTitle = titleParts.join('\n');
|
||||
return { name: '4KHDHub - BuzzServer' + qualityLabel, title: finalTitle, url: finalUrl, quality: quality, size: size || null, fileName: null };
|
||||
});
|
||||
}).catch(function () { return null; });
|
||||
}
|
||||
if (buttonText.indexOf('10Gbps') !== -1) {
|
||||
return resolveTenGbps(buttonLink, headerDetails, size, qualityLabel, quality);
|
||||
}
|
||||
return getFilenameFromUrl(buttonLink).then(function (actualFilename) {
|
||||
var displayFilename = actualFilename || headerDetails || 'Unknown';
|
||||
var titleParts = [];
|
||||
if (displayFilename) titleParts.push(displayFilename);
|
||||
if (size) titleParts.push(size);
|
||||
var finalTitle = titleParts.join('\n');
|
||||
var name;
|
||||
if (buttonText.indexOf('FSL Server') !== -1) name = '4KHDHub - FSL Server' + qualityLabel;
|
||||
else if (buttonText.indexOf('S3 Server') !== -1) name = '4KHDHub - S3 Server' + qualityLabel;
|
||||
else if (/pixeldra/i.test(buttonText) || /pixeldra/i.test(buttonLink)) name = '4KHDHub - Pixeldrain' + qualityLabel;
|
||||
else if (buttonText.indexOf('Download File') !== -1) name = '4KHDHub - HubCloud' + qualityLabel;
|
||||
else name = '4KHDHub - HubCloud' + qualityLabel;
|
||||
return { name: name, title: finalTitle, url: buttonLink, quality: quality, size: size || null, fileName: actualFilename || null };
|
||||
}).catch(function () {
|
||||
var displayFilename = headerDetails || 'Unknown';
|
||||
var titleParts = [];
|
||||
if (displayFilename) titleParts.push(displayFilename);
|
||||
if (size) titleParts.push(size);
|
||||
var finalTitle = titleParts.join('\n');
|
||||
var name = '4KHDHub - HubCloud' + qualityLabel;
|
||||
return { name: name, title: finalTitle, url: buttonLink, quality: quality, size: size || null, fileName: null };
|
||||
});
|
||||
}
|
||||
|
||||
// Iterate per card to capture per-quality sections
|
||||
var tasks = [];
|
||||
var cards = $$('.card');
|
||||
if (cards.length > 0) {
|
||||
cards.each(function (ci, card) {
|
||||
var $card = $$(card);
|
||||
var header = $card.find('div.card-header').text() || $$('div.card-header').first().text() || '';
|
||||
var size = $card.find('i#size').text() || $$('i#size').first().text() || '';
|
||||
var quality = getIndexQuality(header);
|
||||
var headerDetails = cleanTitle(header);
|
||||
var localBtns = $card.find('div.card-body h2 a.btn');
|
||||
if (localBtns.length === 0) localBtns = $card.find('a.btn, .btn, a[href]');
|
||||
localBtns.each(function (i, el) {
|
||||
var $btn = $$(el);
|
||||
var text = ($btn.text() || '').trim();
|
||||
var link = $btn.attr('href');
|
||||
if (!link) return;
|
||||
link = toAbsolute(link, href);
|
||||
// Only consider plausible buttons
|
||||
if (!/(hubcloud|hubdrive|pixeldrain|buzz|10gbps|workers\.dev|r2\.dev|download|api\/file)/i.test(link) && text.toLowerCase().indexOf('download') === -1) return;
|
||||
tasks.push(buildTask(text, link, headerDetails, size, quality));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback: whole page buttons
|
||||
if (tasks.length === 0) {
|
||||
var buttons = $$.root().find('div.card-body h2 a.btn');
|
||||
if (buttons.length === 0) {
|
||||
var altSelectors = ['a.btn', '.btn', 'a[href]'];
|
||||
for (var si = 0; si < altSelectors.length && buttons.length === 0; si++) {
|
||||
buttons = $$.root().find(altSelectors[si]);
|
||||
}
|
||||
}
|
||||
var size = $$('i#size').first().text() || '';
|
||||
var header = $$('div.card-header').first().text() || '';
|
||||
var quality = getIndexQuality(header);
|
||||
var headerDetails = cleanTitle(header);
|
||||
buttons.each(function (i, el) {
|
||||
var $btn = $$(el);
|
||||
var text = ($btn.text() || '').trim();
|
||||
var link = $btn.attr('href');
|
||||
if (!link) return;
|
||||
link = toAbsolute(link, href);
|
||||
tasks.push(buildTask(text, link, headerDetails, size, quality));
|
||||
});
|
||||
}
|
||||
|
||||
if (tasks.length === 0) return [];
|
||||
return Promise.all(tasks).then(function (arr) { return (arr || []).filter(function (x) { return !!x; }); });
|
||||
});
|
||||
}).catch(function () { return []; });
|
||||
}
|
||||
|
||||
// Extract HubDrive links (wrapper around HubCloud if needed)
|
||||
function extractHubDriveLinks(url, referer) {
|
||||
return makeRequest(url).then(function (res) { return res.text(); }).then(function (html) {
|
||||
var $ = cheerio.load(html);
|
||||
var size = $('i#size').text() || '';
|
||||
var header = $('div.card-header').text() || '';
|
||||
var quality = getIndexQuality(header);
|
||||
var headerDetails = cleanTitle(header);
|
||||
var filename = (headerDetails || header || 'Unknown').replace(/^4kHDHub\.com\s*[-_]?\s*/i, '').replace(/\.[a-z0-9]{2,4}$/i, '').replace(/[._]/g, ' ').trim();
|
||||
var primaryBtn = $('.btn.btn-primary.btn-user.btn-success1.m-1').attr('href') || $('a.btn.btn-primary').attr('href') || $('a[href*="download"]').attr('href') || $('a.btn').attr('href');
|
||||
if (!primaryBtn) return [];
|
||||
if ((primaryBtn || '').toLowerCase().indexOf('hubcloud') !== -1) {
|
||||
return extractHubCloudLinks(primaryBtn, '4KHDHub');
|
||||
}
|
||||
var qualityLabel = quality ? (' - ' + quality + 'p') : '';
|
||||
return getFilenameFromUrl(primaryBtn).then(function (actualFilename) {
|
||||
var displayFilename = actualFilename || filename || 'Unknown';
|
||||
var titleParts = [];
|
||||
if (displayFilename) titleParts.push(displayFilename);
|
||||
if (size) titleParts.push(size);
|
||||
var finalTitle = titleParts.join('\n');
|
||||
return [{ name: '4KHDHub - HubDrive' + qualityLabel, title: finalTitle, url: primaryBtn, quality: quality }];
|
||||
}).catch(function () {
|
||||
var displayFilename = filename || 'Unknown';
|
||||
var titleParts = [];
|
||||
if (displayFilename) titleParts.push(displayFilename);
|
||||
if (size) titleParts.push(size);
|
||||
var finalTitle = titleParts.join('\n');
|
||||
return [{ name: '4KHDHub - HubDrive' + qualityLabel, title: finalTitle, url: primaryBtn, quality: quality }];
|
||||
});
|
||||
}).catch(function () { return []; });
|
||||
}
|
||||
|
||||
// Dispatcher for a single link to final streams
|
||||
function processExtractorLink(link) {
|
||||
var lower = (link || '').toLowerCase();
|
||||
if (lower.indexOf('hubdrive') !== -1) {
|
||||
return extractHubDriveLinks(link, '4KHDHub');
|
||||
} else if (lower.indexOf('hubcloud') !== -1) {
|
||||
return extractHubCloudLinks(link, '4KHDHub');
|
||||
} else if (lower.indexOf('workers.dev') !== -1 || lower.indexOf('r2.dev') !== -1) {
|
||||
// Cloudflare Workers / R2 links – treat as HubCloud direct files
|
||||
return getFilenameFromUrl(link).then(function (actualFilename) {
|
||||
var displayFilename = actualFilename || 'HubCloud File';
|
||||
var titleParts = [];
|
||||
if (displayFilename) titleParts.push(displayFilename);
|
||||
var finalTitle = titleParts.join('\n');
|
||||
return [{ name: '4KHDHub - HubCloud - 1080p', title: finalTitle, url: link, quality: 1080, size: null, fileName: actualFilename || null }];
|
||||
}).catch(function () {
|
||||
return [{ name: '4KHDHub - HubCloud - 1080p', title: 'HubCloud File', url: link, quality: 1080, size: null, fileName: null }];
|
||||
});
|
||||
} else if (lower.indexOf('pixeldrain') !== -1) {
|
||||
// Normalize pixeldrain URLs to API endpoint
|
||||
var converted = link;
|
||||
var m = link.match(/pixeldrain\.(?:net|dev)\/u\/([a-zA-Z0-9]+)/);
|
||||
if (m && m[1]) converted = 'https://pixeldrain.net/api/file/' + m[1];
|
||||
return getFilenameFromUrl(converted).then(function (actualFilename) {
|
||||
var displayFilename = actualFilename || 'Pixeldrain File';
|
||||
var title = displayFilename + '\nPixeldrain';
|
||||
return [{ name: '4KHDHub - Pixeldrain - 1080p', title: title, url: converted, quality: 1080, size: null, fileName: actualFilename || null }];
|
||||
}).catch(function () {
|
||||
var title = 'Pixeldrain File\nPixeldrain';
|
||||
return [{ name: '4KHDHub - Pixeldrain - 1080p', title: title, url: converted, quality: 1080, size: null, fileName: null }];
|
||||
});
|
||||
} else if (/\.m(ov|p4|kv)$|\.avi$/i.test(link)) {
|
||||
// Direct video link
|
||||
var filename = (function () {
|
||||
try { return decodeFilename(new URL(link).pathname.split('/').pop().replace(/\.[^/.]+$/, '').replace(/[._]/g, ' ')); } catch (e) { return 'Direct Link'; }
|
||||
})();
|
||||
return getFilenameFromUrl(link).then(function (actualFilename) {
|
||||
var displayFilename = actualFilename || filename || 'Unknown';
|
||||
return [{ name: '4KHDHub Direct Link', title: displayFilename + '\n[Direct Link]', url: link, quality: 1080, size: null, fileName: actualFilename || null }];
|
||||
}).catch(function () {
|
||||
return [{ name: '4KHDHub Direct Link', title: filename + '\n[Direct Link]', url: link, quality: 1080, size: null, fileName: null }];
|
||||
});
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
// Convert a list of resolved hosting URLs to final stream entries
|
||||
function extractStreamingLinks(downloadLinks) {
|
||||
var tasks = (downloadLinks || []).map(function (lnk) {
|
||||
return processExtractorLink(lnk).then(function (res) { return res || []; }).catch(function () { return []; });
|
||||
});
|
||||
return Promise.all(tasks).then(function (arrs) {
|
||||
var flat = [].concat.apply([], arrs).filter(function (x) { return x && x.url; });
|
||||
// Filter out .zip and suspicious AMP
|
||||
var suspicious = ['www-google-com.cdn.ampproject.org', 'bloggingvector.shop', 'cdn.ampproject.org'];
|
||||
flat = flat.filter(function (x) {
|
||||
var u = (x.url || '').toLowerCase();
|
||||
if (u.endsWith('.zip')) return false;
|
||||
return !suspicious.some(function (p) { return u.indexOf(p) !== -1; });
|
||||
});
|
||||
// Deduplicate by URL
|
||||
var seen = {};
|
||||
var unique = [];
|
||||
flat.forEach(function (x) { if (!seen[x.url]) { seen[x.url] = 1; unique.push(x); } });
|
||||
return unique;
|
||||
});
|
||||
}
|
||||
|
||||
// TMDB helper
|
||||
function getTMDBDetails(tmdbId, mediaType) {
|
||||
var url = 'https://api.themoviedb.org/3/' + mediaType + '/' + tmdbId + '?api_key=' + TMDB_API_KEY;
|
||||
return makeRequest(url).then(function (res) { return res.json(); }).then(function (data) {
|
||||
if (mediaType === 'movie') {
|
||||
return { title: data.title, original_title: data.original_title, year: data.release_date ? data.release_date.split('-')[0] : null };
|
||||
} else {
|
||||
return { title: data.name, original_title: data.original_name, year: data.first_air_date ? data.first_air_date.split('-')[0] : null };
|
||||
}
|
||||
}).catch(function () { return null; });
|
||||
}
|
||||
|
||||
// Main entry – Promise-based, no async/await
|
||||
function getStreams(tmdbId, type, season, episode) {
|
||||
type = type || 'movie';
|
||||
var cacheKey = '4khdhub_resolved_urls_v1_' + tmdbId + '_' + type + (season ? ('_s' + season + 'e' + (episode || '')) : '');
|
||||
var disableValidation = ((typeof URL_VALIDATION_ENABLED !== 'undefined') && (URL_VALIDATION_ENABLED === false)) ||
|
||||
((typeof DISABLE_4KHDHUB_URL_VALIDATION !== 'undefined') && (DISABLE_4KHDHUB_URL_VALIDATION === true));
|
||||
|
||||
function finalizeToStreams(links) {
|
||||
var tasks = links.map(function (link) { return disableValidation ? Promise.resolve(true) : validateVideoUrl(link.url); });
|
||||
return Promise.all(tasks).then(function (vals) {
|
||||
var validated = links.filter(function (l, idx) { return !!vals[idx]; });
|
||||
return validated.map(function (l) {
|
||||
return {
|
||||
name: l.name,
|
||||
title: l.title || l.name,
|
||||
url: l.url,
|
||||
quality: (l.quality ? (l.quality + 'p') : '1080p'),
|
||||
size: l.size || null,
|
||||
fileName: l.fileName || null,
|
||||
type: 'direct',
|
||||
behaviorHints: { bingeGroup: '4khdhub-streams' }
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var cached = resolvedUrlsCache[cacheKey];
|
||||
if (cached && cached.length > 0) {
|
||||
return extractStreamingLinks(cached).then(function (streams) { return finalizeToStreams(streams); });
|
||||
}
|
||||
|
||||
var tmdbType = (type === 'series' ? 'tv' : type);
|
||||
return getTMDBDetails(tmdbId, tmdbType).then(function (tmdb) {
|
||||
if (!tmdb || !tmdb.title) return [];
|
||||
return searchContent(tmdb.title).then(function (results) {
|
||||
if (!results || results.length === 0) return [];
|
||||
var best = findBestMatch(results, tmdb.title) || results[0];
|
||||
return loadContent(best.url).then(function (content) {
|
||||
var downloadLinks = [];
|
||||
if (type === 'movie') {
|
||||
downloadLinks = content.downloadLinks || [];
|
||||
} else if ((type === 'series' || type === 'tv') && season && episode) {
|
||||
var target = (content.episodes || []).find(function (ep) { return ep.season === parseInt(season, 10) && ep.episode === parseInt(episode, 10); });
|
||||
downloadLinks = target ? (target.downloadLinks || []) : [];
|
||||
}
|
||||
if (!downloadLinks || downloadLinks.length === 0) return [];
|
||||
|
||||
// Resolve redirect-style links to file hosts
|
||||
var resolverTasks = downloadLinks.map(function (lnk) {
|
||||
var needs = (lnk || '').toLowerCase().indexOf('id=') !== -1;
|
||||
if (needs) {
|
||||
return getRedirectLinks(lnk).then(function (r) { return r && r.trim() ? r : null; }).catch(function () { return null; });
|
||||
}
|
||||
return Promise.resolve(lnk);
|
||||
});
|
||||
|
||||
return Promise.all(resolverTasks).then(function (resolvedArr) {
|
||||
var resolved = resolvedArr.filter(function (x) { return x && x.trim(); });
|
||||
if (resolved.length === 0) return [];
|
||||
resolvedUrlsCache[cacheKey] = resolved; // cache in-memory
|
||||
return extractStreamingLinks(resolved).then(function (links) { return finalizeToStreams(links); });
|
||||
});
|
||||
});
|
||||
});
|
||||
}).catch(function () { return []; });
|
||||
}
|
||||
|
||||
// Export
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { getStreams };
|
||||
} else {
|
||||
// RN global
|
||||
// eslint-disable-next-line no-undef
|
||||
global.getStreams = getStreams;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1,329 +0,0 @@
|
|||
// Dahmer Movies Scraper for Nuvio Local Scrapers
|
||||
// React Native compatible version
|
||||
|
||||
console.log('[DahmerMovies] Initializing Dahmer Movies scraper');
|
||||
|
||||
// Constants
|
||||
const TMDB_API_KEY = "439c478a771f35c05022f9feabcca01c";
|
||||
const DAHMER_MOVIES_API = 'https://a.111477.xyz';
|
||||
const TIMEOUT = 60000; // 60 seconds
|
||||
|
||||
// Quality mapping
|
||||
const Qualities = {
|
||||
Unknown: 0,
|
||||
P144: 144,
|
||||
P240: 240,
|
||||
P360: 360,
|
||||
P480: 480,
|
||||
P720: 720,
|
||||
P1080: 1080,
|
||||
P1440: 1440,
|
||||
P2160: 2160
|
||||
};
|
||||
|
||||
// Helper function to make HTTP requests
|
||||
function makeRequest(url, options = {}) {
|
||||
const requestOptions = {
|
||||
timeout: TIMEOUT,
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US,en;q=0.5',
|
||||
'Connection': 'keep-alive',
|
||||
...options.headers
|
||||
},
|
||||
...options
|
||||
};
|
||||
|
||||
return fetch(url, requestOptions).then(function(response) {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
function getEpisodeSlug(season = null, episode = null) {
|
||||
if (season === null && episode === null) {
|
||||
return ['', ''];
|
||||
}
|
||||
const seasonSlug = season < 10 ? `0${season}` : `${season}`;
|
||||
const episodeSlug = episode < 10 ? `0${episode}` : `${episode}`;
|
||||
return [seasonSlug, episodeSlug];
|
||||
}
|
||||
|
||||
function getIndexQuality(str) {
|
||||
if (!str) return Qualities.Unknown;
|
||||
const match = str.match(/(\d{3,4})[pP]/);
|
||||
return match ? parseInt(match[1]) : Qualities.Unknown;
|
||||
}
|
||||
|
||||
// Extract quality with codec information
|
||||
function getQualityWithCodecs(str) {
|
||||
if (!str) return 'Unknown';
|
||||
|
||||
// Extract base quality (resolution)
|
||||
const qualityMatch = str.match(/(\d{3,4})[pP]/);
|
||||
const baseQuality = qualityMatch ? `${qualityMatch[1]}p` : 'Unknown';
|
||||
|
||||
// Extract codec information (excluding HEVC and bit depth)
|
||||
const codecs = [];
|
||||
const lowerStr = str.toLowerCase();
|
||||
|
||||
// HDR formats
|
||||
if (lowerStr.includes('dv') || lowerStr.includes('dolby vision')) codecs.push('DV');
|
||||
if (lowerStr.includes('hdr10+')) codecs.push('HDR10+');
|
||||
else if (lowerStr.includes('hdr10') || lowerStr.includes('hdr')) codecs.push('HDR');
|
||||
|
||||
// Special formats
|
||||
if (lowerStr.includes('remux')) codecs.push('REMUX');
|
||||
if (lowerStr.includes('imax')) codecs.push('IMAX');
|
||||
|
||||
// Combine quality with codecs using pipeline separator
|
||||
if (codecs.length > 0) {
|
||||
return `${baseQuality} | ${codecs.join(' | ')}`;
|
||||
}
|
||||
|
||||
return baseQuality;
|
||||
}
|
||||
|
||||
function getIndexQualityTags(str, fullTag = false) {
|
||||
if (!str) return '';
|
||||
|
||||
if (fullTag) {
|
||||
const match = str.match(/(.*)\.(?:mkv|mp4|avi)/i);
|
||||
return match ? match[1].trim() : str;
|
||||
} else {
|
||||
const match = str.match(/\d{3,4}[pP]\.?(.*?)\.(mkv|mp4|avi)/i);
|
||||
return match ? match[1].replace(/\./g, ' ').trim() : str;
|
||||
}
|
||||
}
|
||||
|
||||
function encodeUrl(url) {
|
||||
try {
|
||||
return encodeURI(url);
|
||||
} catch (e) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
function decode(input) {
|
||||
try {
|
||||
return decodeURIComponent(input);
|
||||
} catch (e) {
|
||||
return input;
|
||||
}
|
||||
}
|
||||
|
||||
// Format file size from bytes to human readable format
|
||||
function formatFileSize(sizeText) {
|
||||
if (!sizeText) return null;
|
||||
|
||||
// If it's already formatted (contains GB, MB, etc.), return as is
|
||||
if (/\d+(\.\d+)?\s*(GB|MB|KB|TB)/i.test(sizeText)) {
|
||||
return sizeText;
|
||||
}
|
||||
|
||||
// If it's a number (bytes), convert to human readable
|
||||
const bytes = parseInt(sizeText);
|
||||
if (isNaN(bytes)) return sizeText;
|
||||
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
const size = (bytes / Math.pow(1024, i)).toFixed(2);
|
||||
|
||||
return `${size} ${sizes[i]}`;
|
||||
}
|
||||
|
||||
// Parse HTML using basic string manipulation (React Native compatible)
|
||||
function parseLinks(html) {
|
||||
const links = [];
|
||||
|
||||
// Parse table rows to get both links and file sizes
|
||||
const rowRegex = /<tr[^>]*>(.*?)<\/tr>/gis;
|
||||
let rowMatch;
|
||||
|
||||
while ((rowMatch = rowRegex.exec(html)) !== null) {
|
||||
const rowContent = rowMatch[1];
|
||||
|
||||
// Extract link from the row
|
||||
const linkMatch = rowContent.match(/<a[^>]*href=["']([^"']*)["'][^>]*>([^<]*)<\/a>/i);
|
||||
if (!linkMatch) continue;
|
||||
|
||||
const href = linkMatch[1];
|
||||
const text = linkMatch[2].trim();
|
||||
|
||||
// Skip parent directory and empty links
|
||||
if (!text || href === '../' || text === '../') continue;
|
||||
|
||||
// Extract file size from the same row
|
||||
let size = null;
|
||||
const sizeMatch = rowContent.match(/<td[^>]*class=["']filesize["'][^>]*[^>]*>([^<]+)<\/td>/i);
|
||||
if (sizeMatch) {
|
||||
size = sizeMatch[1].trim();
|
||||
}
|
||||
|
||||
links.push({ text, href, size });
|
||||
}
|
||||
|
||||
// Fallback to simple link parsing if table parsing fails
|
||||
if (links.length === 0) {
|
||||
const linkRegex = /<a[^>]*href=["']([^"']*)["'][^>]*>([^<]*)<\/a>/gi;
|
||||
let match;
|
||||
|
||||
while ((match = linkRegex.exec(html)) !== null) {
|
||||
const href = match[1];
|
||||
const text = match[2].trim();
|
||||
if (text && href && href !== '../' && text !== '../') {
|
||||
links.push({ text, href, size: null });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return links;
|
||||
}
|
||||
|
||||
// Main Dahmer Movies fetcher function
|
||||
function invokeDahmerMovies(title, year, season = null, episode = null) {
|
||||
console.log(`[DahmerMovies] Searching for: ${title} (${year})${season ? ` Season ${season}` : ''}${episode ? ` Episode ${episode}` : ''}`);
|
||||
|
||||
// Construct URL based on content type (with proper encoding)
|
||||
const encodedUrl = season === null
|
||||
? `${DAHMER_MOVIES_API}/movies/${encodeURIComponent(title.replace(/:/g, '') + ' (' + year + ')')}/`
|
||||
: `${DAHMER_MOVIES_API}/tvs/${encodeURIComponent(title.replace(/:/g, ' -'))}/Season ${season}/`;
|
||||
|
||||
console.log(`[DahmerMovies] Fetching from: ${encodedUrl}`);
|
||||
|
||||
return makeRequest(encodedUrl).then(function(response) {
|
||||
return response.text();
|
||||
}).then(function(html) {
|
||||
console.log(`[DahmerMovies] Response length: ${html.length}`);
|
||||
|
||||
// Parse HTML to extract links
|
||||
const paths = parseLinks(html);
|
||||
console.log(`[DahmerMovies] Found ${paths.length} total links`);
|
||||
|
||||
// Filter based on content type
|
||||
let filteredPaths;
|
||||
if (season === null) {
|
||||
// For movies, filter by quality (1080p or 2160p)
|
||||
filteredPaths = paths.filter(path =>
|
||||
/(1080p|2160p)/i.test(path.text)
|
||||
);
|
||||
console.log(`[DahmerMovies] Filtered to ${filteredPaths.length} movie links (1080p/2160p only)`);
|
||||
} else {
|
||||
// For TV shows, filter by season and episode
|
||||
const [seasonSlug, episodeSlug] = getEpisodeSlug(season, episode);
|
||||
const episodePattern = new RegExp(`S${seasonSlug}E${episodeSlug}`, 'i');
|
||||
filteredPaths = paths.filter(path =>
|
||||
episodePattern.test(path.text)
|
||||
);
|
||||
console.log(`[DahmerMovies] Filtered to ${filteredPaths.length} TV episode links (S${seasonSlug}E${episodeSlug})`);
|
||||
}
|
||||
|
||||
if (filteredPaths.length === 0) {
|
||||
console.log('[DahmerMovies] No matching content found');
|
||||
return [];
|
||||
}
|
||||
|
||||
// Process and return results
|
||||
const results = filteredPaths.map(path => {
|
||||
const quality = getIndexQuality(path.text);
|
||||
const qualityWithCodecs = getQualityWithCodecs(path.text);
|
||||
const tags = getIndexQualityTags(path.text);
|
||||
|
||||
// Construct proper URL - handle relative paths correctly
|
||||
let fullUrl;
|
||||
if (path.href.startsWith('http')) {
|
||||
// Already a full URL - need to encode it properly
|
||||
try {
|
||||
// Parse the URL and let the URL constructor handle encoding
|
||||
const url = new URL(path.href);
|
||||
// Reconstruct the URL with properly encoded pathname
|
||||
fullUrl = `${url.protocol}//${url.host}${url.pathname}`;
|
||||
} catch (error) {
|
||||
// Fallback: manually encode if URL parsing fails
|
||||
console.log(`[DahmerMovies] URL parsing failed, manually encoding: ${path.href}`);
|
||||
fullUrl = path.href.replace(/ /g, '%20');
|
||||
}
|
||||
} else {
|
||||
// Relative path - combine with encoded base URL
|
||||
const baseUrl = encodedUrl.endsWith('/') ? encodedUrl : encodedUrl + '/';
|
||||
const relativePath = path.href.startsWith('/') ? path.href.substring(1) : path.href;
|
||||
const encodedFilename = encodeURIComponent(relativePath);
|
||||
fullUrl = baseUrl + encodedFilename;
|
||||
}
|
||||
|
||||
return {
|
||||
name: "DahmerMovies",
|
||||
title: `DahmerMovies ${tags || path.text}`,
|
||||
url: fullUrl,
|
||||
quality: qualityWithCodecs, // Use enhanced quality with codecs
|
||||
size: formatFileSize(path.size), // Format file size
|
||||
type: 'direct',
|
||||
filename: path.text
|
||||
};
|
||||
});
|
||||
|
||||
// Sort by quality (highest first)
|
||||
results.sort((a, b) => {
|
||||
const qualityA = getIndexQuality(a.filename);
|
||||
const qualityB = getIndexQuality(b.filename);
|
||||
return qualityB - qualityA;
|
||||
});
|
||||
|
||||
console.log(`[DahmerMovies] Successfully processed ${results.length} streams`);
|
||||
return results;
|
||||
|
||||
}).catch(function(error) {
|
||||
if (error.name === 'AbortError') {
|
||||
console.log('[DahmerMovies] Request timeout - server took too long to respond');
|
||||
} else {
|
||||
console.log(`[DahmerMovies] Error: ${error.message}`);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
// Main function to get streams for TMDB content
|
||||
function getStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
|
||||
console.log(`[DahmerMovies] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}${seasonNum ? `, S${seasonNum}E${episodeNum}` : ''}`);
|
||||
|
||||
// Get TMDB info
|
||||
const tmdbUrl = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY}`;
|
||||
return makeRequest(tmdbUrl).then(function(tmdbResponse) {
|
||||
return tmdbResponse.json();
|
||||
}).then(function(tmdbData) {
|
||||
const title = mediaType === 'tv' ? tmdbData.name : tmdbData.title;
|
||||
const year = mediaType === 'tv' ? tmdbData.first_air_date?.substring(0, 4) : tmdbData.release_date?.substring(0, 4);
|
||||
|
||||
if (!title) {
|
||||
throw new Error('Could not extract title from TMDB response');
|
||||
}
|
||||
|
||||
console.log(`[DahmerMovies] TMDB Info: "${title}" (${year})`);
|
||||
|
||||
// Call the main scraper function
|
||||
return invokeDahmerMovies(
|
||||
title,
|
||||
year ? parseInt(year) : null,
|
||||
seasonNum,
|
||||
episodeNum
|
||||
);
|
||||
|
||||
}).catch(function(error) {
|
||||
console.error(`[DahmerMovies] Error in getStreams: ${error.message}`);
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
// Export the main function
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { getStreams };
|
||||
} else {
|
||||
// For React Native environment
|
||||
global.getStreams = getStreams;
|
||||
}
|
||||
|
|
@ -1,437 +0,0 @@
|
|||
// HDRezka Scraper for Nuvio Local Scrapers
|
||||
// React Native compatible version - No async/await for sandbox compatibility
|
||||
|
||||
// Import cheerio for HTML parsing (React Native compatible)
|
||||
const cheerio = require('cheerio-without-node-native');
|
||||
|
||||
console.log('[HDRezka] Using cheerio-without-node-native for DOM parsing');
|
||||
|
||||
// Constants
|
||||
const TMDB_API_KEY = "439c478a771f35c05022f9feabcca01c";
|
||||
const REZKA_BASE = 'https://hdrezka.ag/';
|
||||
const BASE_HEADERS = {
|
||||
'X-Hdrezka-Android-App': '1',
|
||||
'X-Hdrezka-Android-App-Version': '2.2.0',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US,en;q=0.5',
|
||||
'Connection': 'keep-alive'
|
||||
};
|
||||
|
||||
// Helper function to make HTTP requests
|
||||
function makeRequest(url, options = {}) {
|
||||
return fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
...BASE_HEADERS,
|
||||
...options.headers
|
||||
}
|
||||
}).then(function (response) {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
// Generate random favs parameter
|
||||
function generateRandomFavs() {
|
||||
const randomHex = () => Math.floor(Math.random() * 16).toString(16);
|
||||
const generateSegment = (length) => Array.from({ length }, randomHex).join('');
|
||||
|
||||
return `${generateSegment(8)}-${generateSegment(4)}-${generateSegment(4)}-${generateSegment(4)}-${generateSegment(12)}`;
|
||||
}
|
||||
|
||||
// Extract title and year from search result
|
||||
function extractTitleAndYear(input) {
|
||||
const regex = /^(.*?),.*?(\d{4})/;
|
||||
const match = input.match(regex);
|
||||
|
||||
if (match) {
|
||||
const title = match[1];
|
||||
const year = match[2];
|
||||
return { title: title.trim(), year: year ? parseInt(year, 10) : null };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parse video links from HDRezka response (optimized)
|
||||
function parseVideoLinks(inputString) {
|
||||
if (!inputString) {
|
||||
console.log('[HDRezka] No video links found');
|
||||
return {};
|
||||
}
|
||||
|
||||
console.log(`[HDRezka] Parsing video links from stream URL data`);
|
||||
const linksArray = inputString.split(',');
|
||||
const result = {};
|
||||
|
||||
// Pre-compile regex patterns for better performance
|
||||
const simplePattern = /\[([^<\]]+)\](https?:\/\/[^\s,]+\.mp4|null)/;
|
||||
const qualityPattern = /\[<span[^>]*>([^<]+)/;
|
||||
const urlPattern = /\][^[]*?(https?:\/\/[^\s,]+\.mp4|null)/;
|
||||
|
||||
for (const link of linksArray) {
|
||||
// Try simple format first (non-HTML)
|
||||
let match = link.match(simplePattern);
|
||||
|
||||
// If not found, try HTML format with more flexible pattern
|
||||
if (!match) {
|
||||
const qualityMatch = link.match(qualityPattern);
|
||||
const urlMatch = link.match(urlPattern);
|
||||
|
||||
if (qualityMatch && urlMatch) {
|
||||
match = [null, qualityMatch[1].trim(), urlMatch[1]];
|
||||
}
|
||||
}
|
||||
|
||||
if (match) {
|
||||
const qualityText = match[1].trim();
|
||||
const mp4Url = match[2];
|
||||
|
||||
// Skip null URLs (premium content that requires login)
|
||||
if (mp4Url !== 'null') {
|
||||
result[qualityText] = { type: 'mp4', url: mp4Url };
|
||||
console.log(`[HDRezka] Found ${qualityText}: ${mp4Url.substring(0, 50)}...`);
|
||||
} else {
|
||||
console.log(`[HDRezka] Premium quality ${qualityText} requires login (null URL)`);
|
||||
}
|
||||
} else {
|
||||
console.log(`[HDRezka] Could not parse quality from: ${link.substring(0, 100)}...`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[HDRezka] Found ${Object.keys(result).length} valid qualities: ${Object.keys(result).join(', ')}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Parse subtitles from HDRezka response (optimized)
|
||||
function parseSubtitles(inputString) {
|
||||
if (!inputString) {
|
||||
console.log('[HDRezka] No subtitles found');
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log(`[HDRezka] Parsing subtitles data`);
|
||||
const linksArray = inputString.split(',');
|
||||
const captions = [];
|
||||
|
||||
// Pre-compile regex pattern for better performance
|
||||
const subtitlePattern = /\[([^\]]+)\](https?:\/\/\S+?)(?=,\[|$)/;
|
||||
|
||||
for (const link of linksArray) {
|
||||
const match = link.match(subtitlePattern);
|
||||
|
||||
if (match) {
|
||||
const language = match[1];
|
||||
const url = match[2];
|
||||
|
||||
captions.push({
|
||||
id: url,
|
||||
language,
|
||||
hasCorsRestrictions: false,
|
||||
type: 'vtt',
|
||||
url: url,
|
||||
});
|
||||
console.log(`[HDRezka] Found subtitle ${language}: ${url.substring(0, 50)}...`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[HDRezka] Found ${captions.length} subtitles`);
|
||||
return captions;
|
||||
}
|
||||
|
||||
// Search for content and find media ID
|
||||
function searchAndFindMediaId(media) {
|
||||
console.log(`[HDRezka] Searching for title: ${media.title}, type: ${media.type}, year: ${media.releaseYear || 'any'}`);
|
||||
|
||||
const itemRegexPattern = /<a href="([^"]+)"><span class="enty">([^<]+)<\/span> \(([^)]+)\)/g;
|
||||
const idRegexPattern = /\/(\d+)-[^/]+\.html$/;
|
||||
|
||||
const fullUrl = new URL('/engine/ajax/search.php', REZKA_BASE);
|
||||
fullUrl.searchParams.append('q', media.title);
|
||||
|
||||
console.log(`[HDRezka] Making search request to: ${fullUrl.toString()}`);
|
||||
return makeRequest(fullUrl.toString()).then(function (response) {
|
||||
return response.text();
|
||||
}).then(function (searchData) {
|
||||
console.log(`[HDRezka] Search response length: ${searchData.length}`);
|
||||
|
||||
const movieData = [];
|
||||
let match;
|
||||
|
||||
while ((match = itemRegexPattern.exec(searchData)) !== null) {
|
||||
const url = match[1];
|
||||
const titleAndYear = match[3];
|
||||
|
||||
const result = extractTitleAndYear(titleAndYear);
|
||||
if (result !== null) {
|
||||
const id = url.match(idRegexPattern)?.[1] || null;
|
||||
const isMovie = url.includes('/films/');
|
||||
const isShow = url.includes('/series/');
|
||||
const type = isMovie ? 'movie' : isShow ? 'tv' : 'unknown';
|
||||
|
||||
movieData.push({
|
||||
id: id ?? '',
|
||||
year: result.year ?? 0,
|
||||
type,
|
||||
url,
|
||||
title: match[2]
|
||||
});
|
||||
console.log(`[HDRezka] Found: id=${id}, title=${match[2]}, type=${type}, year=${result.year}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by year if provided
|
||||
let filteredItems = movieData;
|
||||
if (media.releaseYear) {
|
||||
filteredItems = movieData.filter(item => item.year === media.releaseYear);
|
||||
console.log(`[HDRezka] Items filtered by year ${media.releaseYear}: ${filteredItems.length}`);
|
||||
}
|
||||
|
||||
// Filter by type if provided
|
||||
if (media.type) {
|
||||
filteredItems = filteredItems.filter(item => item.type === media.type);
|
||||
console.log(`[HDRezka] Items filtered by type ${media.type}: ${filteredItems.length}`);
|
||||
}
|
||||
|
||||
if (filteredItems.length === 0 && movieData.length > 0) {
|
||||
console.log(`[HDRezka] No exact match found, using first result: ${movieData[0].title}`);
|
||||
return movieData[0];
|
||||
}
|
||||
|
||||
if (filteredItems.length > 0) {
|
||||
console.log(`[HDRezka] Selected item: id=${filteredItems[0].id}, title=${filteredItems[0].title}`);
|
||||
return filteredItems[0];
|
||||
} else {
|
||||
console.log(`[HDRezka] No matching items found`);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Get translator ID from media page
|
||||
function getTranslatorId(url, id, media) {
|
||||
console.log(`[HDRezka] Getting translator ID for url=${url}, id=${id}`);
|
||||
|
||||
// Make sure the URL is absolute
|
||||
const fullUrl = url.startsWith('http') ? url : `${REZKA_BASE}${url.startsWith('/') ? url.substring(1) : url}`;
|
||||
console.log(`[HDRezka] Making request to: ${fullUrl}`);
|
||||
|
||||
return makeRequest(fullUrl).then(function (response) {
|
||||
return response.text();
|
||||
}).then(function (responseText) {
|
||||
console.log(`[HDRezka] Translator page response length: ${responseText.length}`);
|
||||
|
||||
// Translator ID 238 represents the Original + subtitles player.
|
||||
if (responseText.includes(`data-translator_id="238"`)) {
|
||||
console.log(`[HDRezka] Found translator ID 238 (Original + subtitles)`);
|
||||
return '238';
|
||||
}
|
||||
|
||||
const functionName = media.type === 'movie' ? 'initCDNMoviesEvents' : 'initCDNSeriesEvents';
|
||||
const regexPattern = new RegExp(`sof\.tv\.${functionName}\\(${id}, ([^,]+)`, 'i');
|
||||
const match = responseText.match(regexPattern);
|
||||
const translatorId = match ? match[1] : null;
|
||||
|
||||
console.log(`[HDRezka] Extracted translator ID: ${translatorId}`);
|
||||
return translatorId;
|
||||
});
|
||||
}
|
||||
|
||||
// Get stream data from HDRezka
|
||||
function getStreamData(id, translatorId, media) {
|
||||
console.log(`[HDRezka] Getting stream for id=${id}, translatorId=${translatorId}`);
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.append('id', id);
|
||||
searchParams.append('translator_id', translatorId);
|
||||
|
||||
if (media.type === 'tv') {
|
||||
searchParams.append('season', media.season.number.toString());
|
||||
searchParams.append('episode', media.episode.number.toString());
|
||||
console.log(`[HDRezka] TV params: season=${media.season.number}, episode=${media.episode.number}`);
|
||||
}
|
||||
|
||||
const randomFavs = generateRandomFavs();
|
||||
searchParams.append('favs', randomFavs);
|
||||
searchParams.append('action', media.type === 'tv' ? 'get_stream' : 'get_movie');
|
||||
|
||||
const fullUrl = `${REZKA_BASE}ajax/get_cdn_series/`;
|
||||
console.log(`[HDRezka] Making stream request with action=${media.type === 'tv' ? 'get_stream' : 'get_movie'}`);
|
||||
|
||||
return makeRequest(fullUrl, {
|
||||
method: 'POST',
|
||||
body: searchParams,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
}).then(function (response) {
|
||||
return response.text();
|
||||
}).then(function (rawText) {
|
||||
console.log(`[HDRezka] Stream response length: ${rawText.length}`);
|
||||
|
||||
try {
|
||||
const parsedResponse = JSON.parse(rawText);
|
||||
console.log(`[HDRezka] Parsed response successfully`);
|
||||
|
||||
// Process video qualities and subtitles synchronously
|
||||
const qualities = parseVideoLinks(parsedResponse.url);
|
||||
const captions = parseSubtitles(parsedResponse.subtitle);
|
||||
|
||||
return { qualities, captions };
|
||||
} catch (e) {
|
||||
console.error(`[HDRezka] Failed to parse JSON response: ${e.message}`);
|
||||
console.log(`[HDRezka] Raw response: ${rawText.substring(0, 200)}...`);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Get file size using HEAD request
|
||||
function getFileSize(url) {
|
||||
console.log(`[HDRezka] Getting file size for: ${url.substring(0, 60)}...`);
|
||||
|
||||
return fetch(url, {
|
||||
method: 'HEAD',
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||
}
|
||||
}).then(function (response) {
|
||||
if (response.ok) {
|
||||
const contentLength = response.headers.get('content-length');
|
||||
if (contentLength) {
|
||||
const bytes = parseInt(contentLength, 10);
|
||||
const sizeFormatted = formatFileSize(bytes);
|
||||
console.log(`[HDRezka] File size: ${sizeFormatted}`);
|
||||
return sizeFormatted;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[HDRezka] Could not determine file size`);
|
||||
return null;
|
||||
}).catch(function (error) {
|
||||
console.log(`[HDRezka] Error getting file size: ${error.message}`);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
// Format file size in human readable format
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
// Parse quality for sorting
|
||||
function parseQualityForSort(qualityString) {
|
||||
if (!qualityString) return 0;
|
||||
const match = qualityString.match(/(\d{3,4})p/i);
|
||||
return match ? parseInt(match[1], 10) : 0;
|
||||
}
|
||||
|
||||
// Main function to get streams for TMDB content
|
||||
function getStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
|
||||
console.log(`[HDRezka] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}${seasonNum ? `, S${seasonNum}E${episodeNum}` : ''}`);
|
||||
|
||||
// Get TMDB info
|
||||
const tmdbUrl = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY}`;
|
||||
return makeRequest(tmdbUrl).then(function (tmdbResponse) {
|
||||
return tmdbResponse.json();
|
||||
}).then(function (tmdbData) {
|
||||
const title = mediaType === 'tv' ? tmdbData.name : tmdbData.title;
|
||||
const year = mediaType === 'tv' ? tmdbData.first_air_date?.substring(0, 4) : tmdbData.release_date?.substring(0, 4);
|
||||
|
||||
if (!title) {
|
||||
throw new Error('Could not extract title from TMDB response');
|
||||
}
|
||||
|
||||
console.log(`[HDRezka] TMDB Info: "${title}" (${year})`);
|
||||
|
||||
// Create media object
|
||||
const media = {
|
||||
type: mediaType === 'tv' ? 'tv' : 'movie',
|
||||
title: title,
|
||||
releaseYear: year ? parseInt(year) : null
|
||||
};
|
||||
|
||||
// Add season/episode for TV shows
|
||||
if (mediaType === 'tv') {
|
||||
media.season = { number: seasonNum || 1 };
|
||||
media.episode = { number: episodeNum || 1 };
|
||||
}
|
||||
|
||||
// Step 1: Search and find media ID
|
||||
return searchAndFindMediaId(media).then(function (searchResult) {
|
||||
if (!searchResult || !searchResult.id) {
|
||||
console.log('[HDRezka] No search result found');
|
||||
return [];
|
||||
}
|
||||
|
||||
// Step 2: Get translator ID
|
||||
return getTranslatorId(searchResult.url, searchResult.id, media).then(function (translatorId) {
|
||||
if (!translatorId) {
|
||||
console.log('[HDRezka] No translator ID found');
|
||||
return [];
|
||||
}
|
||||
|
||||
// Step 3: Get stream data
|
||||
return getStreamData(searchResult.id, translatorId, media).then(function (streamData) {
|
||||
if (!streamData || !streamData.qualities) {
|
||||
console.log('[HDRezka] No stream data found');
|
||||
return [];
|
||||
}
|
||||
|
||||
// Convert to Nuvio stream format with size detection
|
||||
const streamEntries = Object.entries(streamData.qualities);
|
||||
const streamPromises = streamEntries
|
||||
.filter(([quality, data]) => data.url && data.url !== 'null')
|
||||
.map(([quality, data]) => {
|
||||
const cleanQuality = quality.replace(/p.*$/, 'p'); // "1080p Ultra" -> "1080p"
|
||||
|
||||
// Get file size using Promise chain
|
||||
return getFileSize(data.url).then(function (fileSize) {
|
||||
return {
|
||||
name: "HDRezka",
|
||||
title: `${title} ${year ? `(${year})` : ''} ${quality}${mediaType === 'tv' ? ` S${seasonNum}E${episodeNum}` : ''}`,
|
||||
url: data.url,
|
||||
quality: cleanQuality,
|
||||
size: fileSize,
|
||||
type: 'direct'
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.all(streamPromises).then(function (streams) {
|
||||
// Sort by quality (highest first) - optimized
|
||||
if (streams.length > 1) {
|
||||
streams.sort(function (a, b) {
|
||||
const qualityA = parseQualityForSort(a.quality);
|
||||
const qualityB = parseQualityForSort(b.quality);
|
||||
return qualityB - qualityA;
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`[HDRezka] Successfully processed ${streams.length} streams`);
|
||||
return streams;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}).catch(function (error) {
|
||||
console.error(`[HDRezka] Error in getStreams: ${error.message}`);
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
// Export the main function
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { getStreams };
|
||||
} else {
|
||||
// For React Native environment
|
||||
global.getStreams = getStreams;
|
||||
}
|
||||
|
|
@ -1,857 +0,0 @@
|
|||
// MoviesMod Scraper for Nuvio Local Scrapers
|
||||
// React Native compatible version with Cheerio support
|
||||
|
||||
// Import cheerio-without-node-native for React Native
|
||||
const cheerio = require('cheerio-without-node-native');
|
||||
|
||||
console.log('[MoviesMod] Using cheerio-without-node-native for DOM parsing');
|
||||
|
||||
// Escape regex special characters
|
||||
function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
// Constants
|
||||
const TMDB_API_KEY = "439c478a771f35c05022f9feabcca01c";
|
||||
const FALLBACK_DOMAIN = 'https://moviesmod.chat';
|
||||
const DOMAIN_CACHE_TTL = 4 * 60 * 60 * 1000; // 4 hours
|
||||
|
||||
// Global variables for domain caching
|
||||
let moviesModDomain = FALLBACK_DOMAIN;
|
||||
let domainCacheTimestamp = 0;
|
||||
|
||||
// Fetch latest domain from GitHub
|
||||
async function getMoviesModDomain() {
|
||||
const now = Date.now();
|
||||
if (now - domainCacheTimestamp < DOMAIN_CACHE_TTL) {
|
||||
return moviesModDomain;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('[MoviesMod] Fetching latest domain...');
|
||||
const response = await fetch('https://raw.githubusercontent.com/phisher98/TVVVV/refs/heads/main/domains.json', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data && data.moviesmod) {
|
||||
moviesModDomain = data.moviesmod;
|
||||
domainCacheTimestamp = now;
|
||||
console.log(`[MoviesMod] Updated domain to: ${moviesModDomain}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[MoviesMod] Failed to fetch latest domain: ${error.message}`);
|
||||
}
|
||||
|
||||
return moviesModDomain;
|
||||
}
|
||||
|
||||
// Helper function to make HTTP requests
|
||||
async function makeRequest(url, options = {}) {
|
||||
const defaultHeaders = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US,en;q=0.5',
|
||||
'Accept-Encoding': 'gzip, deflate',
|
||||
'Connection': 'keep-alive',
|
||||
'Upgrade-Insecure-Requests': '1'
|
||||
};
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
...defaultHeaders,
|
||||
...options.headers
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
// Helper function to extract quality from text
|
||||
function extractQuality(text) {
|
||||
if (!text) return 'Unknown';
|
||||
|
||||
const qualityMatch = text.match(/(480p|720p|1080p|2160p|4k)/i);
|
||||
if (qualityMatch) {
|
||||
return qualityMatch[1];
|
||||
}
|
||||
|
||||
const cleanMatch = text.match(/(480p|720p|1080p|2160p|4k)[^)]*\)/i);
|
||||
if (cleanMatch) {
|
||||
return cleanMatch[0];
|
||||
}
|
||||
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
// Parse quality for sorting
|
||||
function parseQualityForSort(qualityString) {
|
||||
if (!qualityString) return 0;
|
||||
const match = qualityString.match(/(\d{3,4})p/i);
|
||||
return match ? parseInt(match[1], 10) : 0;
|
||||
}
|
||||
|
||||
// Get technical details from quality string
|
||||
function getTechDetails(qualityString) {
|
||||
if (!qualityString) return [];
|
||||
const details = [];
|
||||
const lowerText = qualityString.toLowerCase();
|
||||
if (lowerText.includes('10bit')) details.push('10-bit');
|
||||
if (lowerText.includes('hevc') || lowerText.includes('x265')) details.push('HEVC');
|
||||
if (lowerText.includes('hdr')) details.push('HDR');
|
||||
return details;
|
||||
}
|
||||
|
||||
// Simple string similarity function
|
||||
function findBestMatch(mainString, targetStrings) {
|
||||
if (!targetStrings || targetStrings.length === 0) {
|
||||
return { bestMatch: { target: '', rating: 0 }, bestMatchIndex: -1 };
|
||||
}
|
||||
|
||||
const ratings = targetStrings.map(target => {
|
||||
if (!target) return 0;
|
||||
|
||||
const main = mainString.toLowerCase();
|
||||
const targ = target.toLowerCase();
|
||||
|
||||
if (main === targ) return 1;
|
||||
if (targ.includes(main) || main.includes(targ)) return 0.8;
|
||||
|
||||
// Simple word matching
|
||||
const mainWords = main.split(/\s+/);
|
||||
const targWords = targ.split(/\s+/);
|
||||
let matches = 0;
|
||||
|
||||
for (const word of mainWords) {
|
||||
if (word.length > 2 && targWords.some(tw => tw.includes(word) || word.includes(tw))) {
|
||||
matches++;
|
||||
}
|
||||
}
|
||||
|
||||
return matches / Math.max(mainWords.length, targWords.length);
|
||||
});
|
||||
|
||||
const bestRating = Math.max(...ratings);
|
||||
const bestIndex = ratings.indexOf(bestRating);
|
||||
|
||||
return {
|
||||
bestMatch: { target: targetStrings[bestIndex], rating: bestRating },
|
||||
bestMatchIndex: bestIndex
|
||||
};
|
||||
}
|
||||
|
||||
// Search for content on MoviesMod
|
||||
async function searchMoviesMod(query) {
|
||||
try {
|
||||
const baseUrl = await getMoviesModDomain();
|
||||
const searchUrl = `${baseUrl}/?s=${encodeURIComponent(query)}`;
|
||||
console.log(`[MoviesMod] Searching: ${searchUrl}`);
|
||||
|
||||
const response = await makeRequest(searchUrl);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const results = [];
|
||||
$('.latestPost').each((i, element) => {
|
||||
const linkElement = $(element).find('a');
|
||||
const title = linkElement.attr('title');
|
||||
const url = linkElement.attr('href');
|
||||
if (title && url) {
|
||||
results.push({ title, url });
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`[MoviesMod] Found ${results.length} search results`);
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error(`[MoviesMod] Error searching: ${error.message}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Extract download links from a movie/series page
|
||||
async function extractDownloadLinks(moviePageUrl) {
|
||||
try {
|
||||
const response = await makeRequest(moviePageUrl);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
const links = [];
|
||||
const contentBox = $('.thecontent');
|
||||
|
||||
// Get all relevant headers (for movies and TV shows) in document order
|
||||
const headers = contentBox.find('h3:contains("Season"), h4');
|
||||
|
||||
headers.each((i, el) => {
|
||||
const header = $(el);
|
||||
const headerText = header.text().trim();
|
||||
|
||||
// Define the content block for this header
|
||||
const blockContent = header.nextUntil('h3, h4');
|
||||
|
||||
if (header.is('h3') && headerText.toLowerCase().includes('season')) {
|
||||
// TV Show Logic
|
||||
const linkElements = blockContent.find('a.maxbutton-episode-links, a.maxbutton-batch-zip');
|
||||
linkElements.each((j, linkEl) => {
|
||||
const buttonText = $(linkEl).text().trim();
|
||||
const linkUrl = $(linkEl).attr('href');
|
||||
if (linkUrl && !buttonText.toLowerCase().includes('batch')) {
|
||||
links.push({
|
||||
quality: `${headerText} - ${buttonText}`,
|
||||
url: linkUrl
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (header.is('h4')) {
|
||||
// Movie Logic
|
||||
const linkElement = blockContent.find('a[href*="modrefer.in"]').first();
|
||||
if (linkElement.length > 0) {
|
||||
const link = linkElement.attr('href');
|
||||
const cleanQuality = extractQuality(headerText);
|
||||
links.push({
|
||||
quality: cleanQuality,
|
||||
url: link
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`[MoviesMod] Extracted ${links.length} download links`);
|
||||
return links;
|
||||
} catch (error) {
|
||||
console.error(`[MoviesMod] Error extracting download links: ${error.message}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve intermediate links
|
||||
async function resolveIntermediateLink(initialUrl, refererUrl, quality) {
|
||||
try {
|
||||
const urlObject = new URL(initialUrl);
|
||||
|
||||
if (urlObject.hostname.includes('modrefer.in')) {
|
||||
const encodedUrl = urlObject.searchParams.get('url');
|
||||
if (!encodedUrl) {
|
||||
console.error('[MoviesMod] Could not find encoded URL in modrefer.in link.');
|
||||
return [];
|
||||
}
|
||||
|
||||
// Use atob for base64 decoding (React Native compatible)
|
||||
const decodedUrl = atob(encodedUrl);
|
||||
console.log(`[MoviesMod] Decoded modrefer URL: ${decodedUrl}`);
|
||||
|
||||
const response = await makeRequest(decodedUrl, {
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||
'Referer': refererUrl,
|
||||
}
|
||||
});
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
const finalLinks = [];
|
||||
|
||||
// Debug: Check what content is available on the page
|
||||
console.log(`[MoviesMod] Page title: ${$('title').text()}`);
|
||||
console.log(`[MoviesMod] Total links on page: ${$('a').length}`);
|
||||
console.log(`[MoviesMod] HTML length: ${html.length} characters`);
|
||||
|
||||
// Look for timed content links (this is the key part from OG)
|
||||
$('.timed-content-client_show_0_5_0 a').each((i, el) => {
|
||||
const link = $(el).attr('href');
|
||||
const text = $(el).text().trim();
|
||||
if (link) {
|
||||
finalLinks.push({
|
||||
server: text,
|
||||
url: link,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// If no timed content found, look for any driveseed or tech links
|
||||
if (finalLinks.length === 0) {
|
||||
console.log(`[MoviesMod] No timed content found, looking for direct links...`);
|
||||
$('a').each((i, el) => {
|
||||
const link = $(el).attr('href');
|
||||
const text = $(el).text().trim();
|
||||
if (link && (link.includes('driveseed.org') || link.includes('tech.unblockedgames.world') || link.includes('tech.examzculture.in') || link.includes('tech.creativeexpressionsblog.com') || link.includes('tech.examdegree.site'))) {
|
||||
console.log(`[MoviesMod] Found direct link: ${text} -> ${link}`);
|
||||
finalLinks.push({
|
||||
server: text || 'Download Link',
|
||||
url: link,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Also look for any additional download buttons or links that might be hidden
|
||||
if (finalLinks.length === 0) {
|
||||
console.log(`[MoviesMod] Looking for alternative download patterns...`);
|
||||
$('button, .download-btn, .btn, [class*="download"], [class*="btn"]').each((i, el) => {
|
||||
const $el = $(el);
|
||||
const link = $el.attr('href') || $el.attr('data-href') || $el.find('a').attr('href');
|
||||
const text = $el.text().trim();
|
||||
if (link && (link.includes('driveseed.org') || link.includes('tech.unblockedgames.world') || link.includes('tech.examzculture.in') || link.includes('tech.creativeexpressionsblog.com') || link.includes('tech.examdegree.site'))) {
|
||||
console.log(`[MoviesMod] Found alternative link: ${text} -> ${link}`);
|
||||
finalLinks.push({
|
||||
server: text || 'Alternative Download',
|
||||
url: link,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`[MoviesMod] Found ${finalLinks.length} total links`);
|
||||
return finalLinks;
|
||||
}
|
||||
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error(`[MoviesMod] Error resolving intermediate link: ${error.message}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve tech.unblockedgames.world SID links to driveleech URLs
|
||||
async function resolveTechUnblockedLink(sidUrl) {
|
||||
console.log(`[MoviesMod] Resolving SID link: ${sidUrl}`);
|
||||
|
||||
try {
|
||||
// Step 1: Get the initial page
|
||||
const response = await makeRequest(sidUrl);
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const initialForm = $('#landing');
|
||||
const wp_http_step1 = initialForm.find('input[name="_wp_http"]').val();
|
||||
const action_url_step1 = initialForm.attr('action');
|
||||
|
||||
if (!wp_http_step1 || !action_url_step1) {
|
||||
console.error(" [SID] Error: Could not find _wp_http in initial form.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Step 2: POST to the first form's action URL
|
||||
const step1Data = new URLSearchParams({ '_wp_http': wp_http_step1 });
|
||||
const responseStep1 = await makeRequest(action_url_step1, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Referer': sidUrl,
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: step1Data.toString()
|
||||
});
|
||||
|
||||
// Step 3: Parse verification page for second form
|
||||
const html2 = await responseStep1.text();
|
||||
const $2 = cheerio.load(html2);
|
||||
const verificationForm = $2('#landing');
|
||||
const action_url_step2 = verificationForm.attr('action');
|
||||
const wp_http2 = verificationForm.find('input[name="_wp_http2"]').val();
|
||||
const token = verificationForm.find('input[name="token"]').val();
|
||||
|
||||
if (!action_url_step2) {
|
||||
console.error(" [SID] Error: Could not find verification form.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Step 4: POST to the verification URL
|
||||
const step2Data = new URLSearchParams({ '_wp_http2': wp_http2, 'token': token });
|
||||
const responseStep2 = await makeRequest(action_url_step2, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Referer': responseStep1.url,
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: step2Data.toString()
|
||||
});
|
||||
|
||||
// Step 5: Find dynamic cookie and link from JavaScript
|
||||
const finalHtml = await responseStep2.text();
|
||||
let finalLinkPath = null;
|
||||
let cookieName = null;
|
||||
let cookieValue = null;
|
||||
|
||||
const cookieMatch = finalHtml.match(/s_343\('([^']+)',\s*'([^']+)'/);
|
||||
const linkMatch = finalHtml.match(/c\.setAttribute\("href",\s*"([^"]+)"\)/);
|
||||
|
||||
if (cookieMatch) {
|
||||
cookieName = cookieMatch[1].trim();
|
||||
cookieValue = cookieMatch[2].trim();
|
||||
}
|
||||
if (linkMatch) {
|
||||
finalLinkPath = linkMatch[1].trim();
|
||||
}
|
||||
|
||||
if (!finalLinkPath || !cookieName || !cookieValue) {
|
||||
console.error(" [SID] Error: Could not extract dynamic cookie/link from JS.");
|
||||
return null;
|
||||
}
|
||||
|
||||
const { origin } = new URL(sidUrl);
|
||||
const finalUrl = new URL(finalLinkPath, origin).href;
|
||||
|
||||
// Step 6: Make final request with cookie
|
||||
const finalResponse = await makeRequest(finalUrl, {
|
||||
headers: {
|
||||
'Referer': responseStep2.url,
|
||||
'Cookie': `${cookieName}=${cookieValue}`
|
||||
}
|
||||
});
|
||||
|
||||
// Step 7: Extract driveleech URL from meta refresh tag
|
||||
const metaHtml = await finalResponse.text();
|
||||
const $3 = cheerio.load(metaHtml);
|
||||
const metaRefresh = $3('meta[http-equiv="refresh"]');
|
||||
|
||||
if (metaRefresh.length > 0) {
|
||||
const content = metaRefresh.attr('content');
|
||||
const urlMatch = content.match(/url=(.*)/i);
|
||||
if (urlMatch && urlMatch[1]) {
|
||||
const driveleechUrl = urlMatch[1].replace(/"/g, "").replace(/'/g, "");
|
||||
console.log(` [SID] SUCCESS! Resolved Driveleech URL: ${driveleechUrl}`);
|
||||
return driveleechUrl;
|
||||
}
|
||||
}
|
||||
|
||||
console.error(" [SID] Error: Could not find meta refresh tag with Driveleech URL.");
|
||||
return null;
|
||||
|
||||
} catch (error) {
|
||||
console.error(` [SID] Error during SID resolution: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve driveseed.org links to get download options
|
||||
async function resolveDriveseedLink(driveseedUrl) {
|
||||
try {
|
||||
const response = await makeRequest(driveseedUrl, {
|
||||
headers: {
|
||||
'Referer': 'https://links.modpro.blog/',
|
||||
}
|
||||
});
|
||||
const html = await response.text();
|
||||
|
||||
const redirectMatch = html.match(/window\.location\.replace\("([^"]+)"\)/);
|
||||
|
||||
if (redirectMatch && redirectMatch[1]) {
|
||||
const finalPath = redirectMatch[1];
|
||||
const finalUrl = `https://driveseed.org${finalPath}`;
|
||||
|
||||
const finalResponse = await makeRequest(finalUrl, {
|
||||
headers: {
|
||||
'Referer': driveseedUrl,
|
||||
}
|
||||
});
|
||||
const finalHtml = await finalResponse.text();
|
||||
const $ = cheerio.load(finalHtml);
|
||||
|
||||
const downloadOptions = [];
|
||||
let size = null;
|
||||
let fileName = null;
|
||||
|
||||
// Extract size and filename from the list
|
||||
$('ul.list-group li').each((i, el) => {
|
||||
const text = $(el).text();
|
||||
if (text.includes('Size :')) {
|
||||
size = text.split(':')[1].trim();
|
||||
} else if (text.includes('Name :')) {
|
||||
fileName = text.split(':')[1].trim();
|
||||
}
|
||||
});
|
||||
|
||||
// Find Resume Cloud button (primary)
|
||||
const resumeCloudLink = $('a:contains("Resume Cloud")').attr('href');
|
||||
if (resumeCloudLink) {
|
||||
downloadOptions.push({
|
||||
title: 'Resume Cloud',
|
||||
type: 'resume',
|
||||
url: `https://driveseed.org${resumeCloudLink}`,
|
||||
priority: 1
|
||||
});
|
||||
}
|
||||
|
||||
// Find Resume Worker Bot (fallback)
|
||||
const workerSeedLink = $('a:contains("Resume Worker Bot")').attr('href');
|
||||
if (workerSeedLink) {
|
||||
downloadOptions.push({
|
||||
title: 'Resume Worker Bot',
|
||||
type: 'worker',
|
||||
url: workerSeedLink,
|
||||
priority: 2
|
||||
});
|
||||
}
|
||||
|
||||
// Find any other download links as additional fallbacks
|
||||
$('a[href*="/download/"]').each((i, el) => {
|
||||
const href = $(el).attr('href');
|
||||
const text = $(el).text().trim();
|
||||
if (href && text && !downloadOptions.some(opt => opt.url === href)) {
|
||||
downloadOptions.push({
|
||||
title: text,
|
||||
type: 'generic',
|
||||
url: href.startsWith('http') ? href : `https://driveseed.org${href}`,
|
||||
priority: 4
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Find Instant Download (final fallback)
|
||||
const instantDownloadLink = $('a:contains("Instant Download")').attr('href');
|
||||
if (instantDownloadLink) {
|
||||
downloadOptions.push({
|
||||
title: 'Instant Download',
|
||||
type: 'instant',
|
||||
url: instantDownloadLink,
|
||||
priority: 3
|
||||
});
|
||||
}
|
||||
|
||||
// Sort by priority
|
||||
downloadOptions.sort((a, b) => a.priority - b.priority);
|
||||
return { downloadOptions, size, fileName };
|
||||
}
|
||||
return { downloadOptions: [], size: null, fileName: null };
|
||||
} catch (error) {
|
||||
console.error(`[MoviesMod] Error resolving Driveseed link: ${error.message}`);
|
||||
return { downloadOptions: [], size: null, fileName: null };
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve Resume Cloud link to final download URL
|
||||
async function resolveResumeCloudLink(resumeUrl) {
|
||||
try {
|
||||
const response = await makeRequest(resumeUrl, {
|
||||
headers: {
|
||||
'Referer': 'https://driveseed.org/',
|
||||
}
|
||||
});
|
||||
const html = await response.text();
|
||||
const $ = cheerio.load(html);
|
||||
const downloadLink = $('a:contains("Cloud Resume Download")').attr('href');
|
||||
return downloadLink || null;
|
||||
} catch (error) {
|
||||
console.error(`[MoviesMod] Error resolving Resume Cloud link: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve Video Seed (Instant Download) link
|
||||
async function resolveVideoSeedLink(videoSeedUrl) {
|
||||
try {
|
||||
const urlParams = new URLSearchParams(new URL(videoSeedUrl).search);
|
||||
const keys = urlParams.get('url');
|
||||
|
||||
if (keys) {
|
||||
const apiUrl = `${new URL(videoSeedUrl).origin}/api`;
|
||||
const formData = new URLSearchParams();
|
||||
formData.append('keys', keys);
|
||||
|
||||
const apiResponse = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'x-token': new URL(videoSeedUrl).hostname,
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||
}
|
||||
});
|
||||
|
||||
if (apiResponse.ok) {
|
||||
const responseData = await apiResponse.json();
|
||||
if (responseData && responseData.url) {
|
||||
return responseData.url;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error(`[MoviesMod] Error resolving VideoSeed link: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate if a video URL is working
|
||||
async function validateVideoUrl(url, timeout = 10000) {
|
||||
try {
|
||||
console.log(`[MoviesMod] Validating URL: ${url.substring(0, 100)}...`);
|
||||
const response = await fetch(url, {
|
||||
method: 'HEAD',
|
||||
headers: {
|
||||
'Range': 'bytes=0-1',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok || response.status === 206) {
|
||||
console.log(`[MoviesMod] ✓ URL validation successful (${response.status})`);
|
||||
return true;
|
||||
} else {
|
||||
console.log(`[MoviesMod] ✗ URL validation failed with status: ${response.status}`);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`[MoviesMod] ✗ URL validation failed: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Process a single download link to get final stream
|
||||
async function processDownloadLink(link, selectedResult, mediaType, episodeNum) {
|
||||
try {
|
||||
console.log(`[MoviesMod] Processing quality: ${link.quality}`);
|
||||
|
||||
const finalLinks = await resolveIntermediateLink(link.url, selectedResult.url, link.quality);
|
||||
if (!finalLinks || finalLinks.length === 0) {
|
||||
console.log(`[MoviesMod] No final links found for ${link.quality}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Filter for specific episode if needed
|
||||
let targetLinks = finalLinks;
|
||||
if ((mediaType === 'tv' || mediaType === 'series') && episodeNum !== null) {
|
||||
targetLinks = finalLinks.filter(targetLink => {
|
||||
const serverName = targetLink.server.toLowerCase();
|
||||
const episodePatterns = [
|
||||
new RegExp(`episode\\s+${episodeNum}\\b`, 'i'),
|
||||
new RegExp(`ep\\s+${episodeNum}\\b`, 'i'),
|
||||
new RegExp(`e${episodeNum}\\b`, 'i'),
|
||||
new RegExp(`\\b${episodeNum}\\b`)
|
||||
];
|
||||
|
||||
return episodePatterns.some(pattern => pattern.test(serverName));
|
||||
});
|
||||
|
||||
if (targetLinks.length === 0) {
|
||||
console.log(`[MoviesMod] No episode ${episodeNum} found for ${link.quality}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Process each target link
|
||||
for (const targetLink of targetLinks) {
|
||||
try {
|
||||
let currentUrl = targetLink.url;
|
||||
|
||||
// Handle SID links if they appear
|
||||
if (currentUrl && (currentUrl.includes('tech.unblockedgames.world') || currentUrl.includes('tech.creativeexpressionsblog.com') || currentUrl.includes('tech.examzculture.in') || currentUrl.includes('tech.examdegree.site'))) {
|
||||
console.log(`[MoviesMod] Resolving SID link: ${targetLink.server}`);
|
||||
const resolvedUrl = await resolveTechUnblockedLink(currentUrl);
|
||||
if (!resolvedUrl) {
|
||||
console.log(`[MoviesMod] Failed to resolve SID link for ${targetLink.server}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip broken link report pages
|
||||
if (resolvedUrl.includes('report-broken-links') || resolvedUrl.includes('moviesmod.wiki')) {
|
||||
console.log(`[MoviesMod] Skipping broken link report page for ${targetLink.server}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
currentUrl = resolvedUrl;
|
||||
}
|
||||
|
||||
if (currentUrl && currentUrl.includes('driveseed.org')) {
|
||||
const { downloadOptions, size, fileName } = await resolveDriveseedLink(currentUrl);
|
||||
|
||||
if (!downloadOptions || downloadOptions.length === 0) {
|
||||
console.log(`[MoviesMod] No download options found for ${targetLink.server} - ${currentUrl}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try download methods in order of priority
|
||||
let finalDownloadUrl = null;
|
||||
let usedMethod = null;
|
||||
|
||||
for (const option of downloadOptions) {
|
||||
try {
|
||||
console.log(`[MoviesMod] Trying ${option.title} for ${link.quality}...`);
|
||||
|
||||
if (option.type === 'resume') {
|
||||
finalDownloadUrl = await resolveResumeCloudLink(option.url);
|
||||
} else if (option.type === 'instant') {
|
||||
finalDownloadUrl = await resolveVideoSeedLink(option.url);
|
||||
}
|
||||
|
||||
if (finalDownloadUrl) {
|
||||
// Check if URL validation is enabled
|
||||
if (typeof URL_VALIDATION_ENABLED !== 'undefined' && !URL_VALIDATION_ENABLED) {
|
||||
usedMethod = option.title;
|
||||
console.log(`[MoviesMod] ✓ URL validation disabled, accepting ${usedMethod} result`);
|
||||
break;
|
||||
}
|
||||
|
||||
const isValid = await validateVideoUrl(finalDownloadUrl);
|
||||
if (isValid) {
|
||||
usedMethod = option.title;
|
||||
console.log(`[MoviesMod] ✓ Successfully resolved using ${usedMethod}`);
|
||||
break;
|
||||
} else {
|
||||
console.log(`[MoviesMod] ✗ ${option.title} returned invalid URL`);
|
||||
finalDownloadUrl = null;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`[MoviesMod] ✗ ${option.title} failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (finalDownloadUrl) {
|
||||
const actualQuality = extractQuality(link.quality);
|
||||
const sizeInfo = size || link.quality.match(/\[([^\]]+)\]/)?.[1];
|
||||
const cleanFileName = fileName ? fileName.replace(/\.[^/.]+$/, "").replace(/[._]/g, ' ') : `Stream from ${link.quality}`;
|
||||
const techDetails = getTechDetails(link.quality);
|
||||
const techDetailsString = techDetails.length > 0 ? ` • ${techDetails.join(' • ')}` : '';
|
||||
|
||||
return {
|
||||
name: `MoviesMod`,
|
||||
title: `${cleanFileName}\n${sizeInfo || ''}${techDetailsString}`,
|
||||
url: finalDownloadUrl,
|
||||
quality: actualQuality,
|
||||
size: sizeInfo,
|
||||
fileName: fileName,
|
||||
type: 'direct'
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[MoviesMod] Error processing target link: ${error.message}`);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error(`[MoviesMod] Error processing quality ${link.quality}: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Main function to get streams for TMDB content
|
||||
async function getStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
|
||||
console.log(`[MoviesMod] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}${seasonNum ? `, S${seasonNum}E${episodeNum}` : ''}`);
|
||||
|
||||
try {
|
||||
// Get TMDB info
|
||||
const tmdbUrl = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY}`;
|
||||
const tmdbResponse = await makeRequest(tmdbUrl);
|
||||
const tmdbData = await tmdbResponse.json();
|
||||
|
||||
const title = mediaType === 'tv' ? tmdbData.name : tmdbData.title;
|
||||
const year = mediaType === 'tv' ? tmdbData.first_air_date?.substring(0, 4) : tmdbData.release_date?.substring(0, 4);
|
||||
|
||||
if (!title) {
|
||||
throw new Error('Could not extract title from TMDB response');
|
||||
}
|
||||
|
||||
console.log(`[MoviesMod] TMDB Info: "${title}" (${year})`);
|
||||
|
||||
// Search for the media
|
||||
const searchResults = await searchMoviesMod(title);
|
||||
if (searchResults.length === 0) {
|
||||
console.log(`[MoviesMod] No search results found`);
|
||||
return [];
|
||||
}
|
||||
|
||||
// Use string similarity to find the best match
|
||||
const titles = searchResults.map(r => r.title);
|
||||
const bestMatch = findBestMatch(title, titles);
|
||||
|
||||
console.log(`[MoviesMod] Best match for "${title}" is "${bestMatch.bestMatch.target}" with a rating of ${bestMatch.bestMatch.rating.toFixed(2)}`);
|
||||
|
||||
let selectedResult = null;
|
||||
if (bestMatch.bestMatch.rating > 0.3) {
|
||||
selectedResult = searchResults[bestMatch.bestMatchIndex];
|
||||
|
||||
// Additional check for year if it's a movie
|
||||
if (mediaType === 'movie' && year) {
|
||||
if (!selectedResult.title.includes(year)) {
|
||||
console.warn(`[MoviesMod] Title match found, but year mismatch. Matched: "${selectedResult.title}", Expected year: ${year}. Discarding match.`);
|
||||
selectedResult = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectedResult) {
|
||||
// Try stricter search
|
||||
console.log('[MoviesMod] Similarity match failed. Trying stricter search...');
|
||||
const titleRegex = new RegExp(`\\b${escapeRegExp(title.toLowerCase())}\\b`);
|
||||
|
||||
if (mediaType === 'movie') {
|
||||
selectedResult = searchResults.find(r =>
|
||||
titleRegex.test(r.title.toLowerCase()) &&
|
||||
(!year || r.title.includes(year))
|
||||
);
|
||||
} else {
|
||||
selectedResult = searchResults.find(r =>
|
||||
titleRegex.test(r.title.toLowerCase()) &&
|
||||
r.title.toLowerCase().includes('season')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectedResult) {
|
||||
console.log(`[MoviesMod] No suitable search result found for "${title} (${year})"`);
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log(`[MoviesMod] Selected: ${selectedResult.title}`);
|
||||
|
||||
// Extract download links
|
||||
const downloadLinks = await extractDownloadLinks(selectedResult.url);
|
||||
if (downloadLinks.length === 0) {
|
||||
console.log(`[MoviesMod] No download links found`);
|
||||
return [];
|
||||
}
|
||||
|
||||
let relevantLinks = downloadLinks;
|
||||
if ((mediaType === 'tv' || mediaType === 'series') && seasonNum !== null) {
|
||||
relevantLinks = downloadLinks.filter(link =>
|
||||
link.quality.toLowerCase().includes(`season ${seasonNum}`) ||
|
||||
link.quality.toLowerCase().includes(`s${seasonNum}`)
|
||||
);
|
||||
}
|
||||
|
||||
// Filter out 480p links
|
||||
relevantLinks = relevantLinks.filter(link => !link.quality.toLowerCase().includes('480p'));
|
||||
console.log(`[MoviesMod] ${relevantLinks.length} links remaining after 480p filter.`);
|
||||
|
||||
if (relevantLinks.length === 0) {
|
||||
console.log(`[MoviesMod] No relevant links found after filtering`);
|
||||
return [];
|
||||
}
|
||||
|
||||
// Process links to get final streams - using Promise.all like UHDMovies
|
||||
const streamPromises = relevantLinks.map(link => processDownloadLink(link, selectedResult, mediaType, episodeNum));
|
||||
const streams = (await Promise.all(streamPromises)).filter(Boolean);
|
||||
|
||||
// Sort by quality descending
|
||||
streams.sort((a, b) => {
|
||||
const qualityA = parseQualityForSort(a.quality);
|
||||
const qualityB = parseQualityForSort(b.quality);
|
||||
return qualityB - qualityA;
|
||||
});
|
||||
|
||||
console.log(`[MoviesMod] Successfully processed ${streams.length} streams`);
|
||||
return streams;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[MoviesMod] Error in getStreams: ${error.message}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Export the main function
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { getStreams };
|
||||
} else {
|
||||
// For React Native environment
|
||||
global.getStreams = getStreams;
|
||||
}
|
||||
|
|
@ -1,471 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const axios = require('axios');
|
||||
const cheerio = require('cheerio');
|
||||
const { URL } = require('url');
|
||||
|
||||
class MyFlixerExtractor {
|
||||
constructor() {
|
||||
this.mainUrl = 'https://watch32.sx';
|
||||
this.videostrUrl = 'https://videostr.net';
|
||||
}
|
||||
|
||||
async search(query) {
|
||||
try {
|
||||
const searchUrl = `${this.mainUrl}/search/${query.replace(/\s+/g, '-')}`;
|
||||
console.log(`Searching: ${searchUrl}`);
|
||||
|
||||
const response = await axios.get(searchUrl);
|
||||
const $ = cheerio.load(response.data);
|
||||
|
||||
const results = [];
|
||||
$('.flw-item').each((i, element) => {
|
||||
const title = $(element).find('h2.film-name > a').attr('title');
|
||||
const link = $(element).find('h2.film-name > a').attr('href');
|
||||
const poster = $(element).find('img.film-poster-img').attr('data-src');
|
||||
|
||||
if (title && link) {
|
||||
results.push({
|
||||
title,
|
||||
url: link.startsWith('http') ? link : `${this.mainUrl}${link}`,
|
||||
poster
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Search results found:');
|
||||
results.forEach((result, index) => {
|
||||
console.log(`${index + 1}. ${result.title}`);
|
||||
});
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error('Search error:', error.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async getContentDetails(url) {
|
||||
try {
|
||||
console.log(`Getting content details: ${url}`);
|
||||
const response = await axios.get(url);
|
||||
const $ = cheerio.load(response.data);
|
||||
|
||||
const contentId = $('.detail_page-watch').attr('data-id');
|
||||
const name = $('.detail_page-infor h2.heading-name > a').text();
|
||||
const isMovie = url.includes('movie');
|
||||
|
||||
if (isMovie) {
|
||||
return {
|
||||
type: 'movie',
|
||||
name,
|
||||
data: `list/${contentId}`
|
||||
};
|
||||
} else {
|
||||
// Get TV series episodes
|
||||
const episodes = [];
|
||||
const seasonsResponse = await axios.get(`${this.mainUrl}/ajax/season/list/${contentId}`);
|
||||
const $seasons = cheerio.load(seasonsResponse.data);
|
||||
|
||||
for (const season of $seasons('a.ss-item').toArray()) {
|
||||
const seasonId = $(season).attr('data-id');
|
||||
const seasonNum = $(season).text().replace('Season ', '');
|
||||
|
||||
const episodesResponse = await axios.get(`${this.mainUrl}/ajax/season/episodes/${seasonId}`);
|
||||
const $episodes = cheerio.load(episodesResponse.data);
|
||||
|
||||
$episodes('a.eps-item').each((i, episode) => {
|
||||
const epId = $(episode).attr('data-id');
|
||||
const title = $(episode).attr('title');
|
||||
const match = title.match(/Eps (\d+): (.+)/);
|
||||
|
||||
if (match) {
|
||||
episodes.push({
|
||||
id: epId,
|
||||
episode: parseInt(match[1]),
|
||||
name: match[2],
|
||||
season: parseInt(seasonNum.replace('Series', '').trim()),
|
||||
data: `servers/${epId}`
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'series',
|
||||
name,
|
||||
episodes
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Content details error:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async getServerLinks(data) {
|
||||
try {
|
||||
console.log(`Getting server links: ${data}`);
|
||||
const response = await axios.get(`${this.mainUrl}/ajax/episode/${data}`);
|
||||
const $ = cheerio.load(response.data);
|
||||
|
||||
const servers = [];
|
||||
$('a.link-item').each((i, element) => {
|
||||
const linkId = $(element).attr('data-linkid') || $(element).attr('data-id');
|
||||
if (linkId) {
|
||||
servers.push(linkId);
|
||||
}
|
||||
});
|
||||
|
||||
return servers;
|
||||
} catch (error) {
|
||||
console.error('Server links error:', error.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async getSourceUrl(linkId) {
|
||||
try {
|
||||
console.log(`Getting source URL for linkId: ${linkId}`);
|
||||
const response = await axios.get(`${this.mainUrl}/ajax/episode/sources/${linkId}`);
|
||||
return response.data.link;
|
||||
} catch (error) {
|
||||
console.error('Source URL error:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async extractVideostrM3u8(url) {
|
||||
try {
|
||||
console.log(`Extracting from Videostr: ${url}`);
|
||||
|
||||
const headers = {
|
||||
'Accept': '*/*',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Referer': this.videostrUrl,
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||
};
|
||||
|
||||
// Extract ID from URL
|
||||
const id = url.split('/').pop().split('?')[0];
|
||||
|
||||
// Get nonce from embed page
|
||||
const embedResponse = await axios.get(url, { headers });
|
||||
const embedHtml = embedResponse.data;
|
||||
|
||||
// Try to find 48-character nonce
|
||||
let nonce = embedHtml.match(/\b[a-zA-Z0-9]{48}\b/);
|
||||
if (nonce) {
|
||||
nonce = nonce[0];
|
||||
} else {
|
||||
// Try to find three 16-character segments
|
||||
const matches = embedHtml.match(/\b([a-zA-Z0-9]{16})\b.*?\b([a-zA-Z0-9]{16})\b.*?\b([a-zA-Z0-9]{16})\b/);
|
||||
if (matches) {
|
||||
nonce = matches[1] + matches[2] + matches[3];
|
||||
}
|
||||
}
|
||||
|
||||
if (!nonce) {
|
||||
throw new Error('Could not extract nonce');
|
||||
}
|
||||
|
||||
console.log(`Extracted nonce: ${nonce}`);
|
||||
|
||||
// Get sources from API
|
||||
const apiUrl = `${this.videostrUrl}/embed-1/v3/e-1/getSources?id=${id}&_k=${nonce}`;
|
||||
console.log(`API URL: ${apiUrl}`);
|
||||
|
||||
const sourcesResponse = await axios.get(apiUrl, { headers });
|
||||
const sourcesData = sourcesResponse.data;
|
||||
|
||||
if (!sourcesData.sources) {
|
||||
throw new Error('No sources found in response');
|
||||
}
|
||||
|
||||
let m3u8Url = sourcesData.sources;
|
||||
|
||||
// Check if sources is already an M3U8 URL
|
||||
if (!m3u8Url.includes('.m3u8')) {
|
||||
console.log('Sources are encrypted, attempting to decrypt...');
|
||||
|
||||
// Get decryption key
|
||||
const keyResponse = await axios.get('https://raw.githubusercontent.com/yogesh-hacker/MegacloudKeys/refs/heads/main/keys.json');
|
||||
const key = keyResponse.data.vidstr;
|
||||
|
||||
if (!key) {
|
||||
throw new Error('Could not get decryption key');
|
||||
}
|
||||
|
||||
// Decrypt using Google Apps Script
|
||||
const decodeUrl = 'https://script.google.com/macros/s/AKfycbx-yHTwupis_JD0lNzoOnxYcEYeXmJZrg7JeMxYnEZnLBy5V0--UxEvP-y9txHyy1TX9Q/exec';
|
||||
const fullUrl = `${decodeUrl}?encrypted_data=${encodeURIComponent(m3u8Url)}&nonce=${encodeURIComponent(nonce)}&secret=${encodeURIComponent(key)}`;
|
||||
|
||||
const decryptResponse = await axios.get(fullUrl);
|
||||
const decryptedData = decryptResponse.data;
|
||||
|
||||
// Extract file URL from decrypted response
|
||||
const fileMatch = decryptedData.match(/"file":"(.*?)"/);
|
||||
if (fileMatch) {
|
||||
m3u8Url = fileMatch[1];
|
||||
} else {
|
||||
throw new Error('Could not extract video URL from decrypted response');
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Final M3U8 URL: ${m3u8Url}`);
|
||||
|
||||
// Filter only megacdn links
|
||||
if (!m3u8Url.includes('megacdn.co')) {
|
||||
console.log('Skipping non-megacdn link');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parse master playlist to extract quality streams
|
||||
const qualities = await this.parseM3U8Qualities(m3u8Url);
|
||||
|
||||
return {
|
||||
m3u8Url,
|
||||
qualities,
|
||||
headers: {
|
||||
'Referer': 'https://videostr.net/',
|
||||
'Origin': 'https://videostr.net/'
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Videostr extraction error:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async parseM3U8Qualities(masterUrl) {
|
||||
try {
|
||||
const response = await axios.get(masterUrl, {
|
||||
headers: {
|
||||
'Referer': 'https://videostr.net/',
|
||||
'Origin': 'https://videostr.net/'
|
||||
}
|
||||
});
|
||||
|
||||
const playlist = response.data;
|
||||
const qualities = [];
|
||||
|
||||
// Parse M3U8 master playlist
|
||||
const lines = playlist.split('\n');
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (line.startsWith('#EXT-X-STREAM-INF:')) {
|
||||
const nextLine = lines[i + 1]?.trim();
|
||||
if (nextLine && !nextLine.startsWith('#')) {
|
||||
// Extract resolution and bandwidth
|
||||
const resolutionMatch = line.match(/RESOLUTION=(\d+x\d+)/);
|
||||
const bandwidthMatch = line.match(/BANDWIDTH=(\d+)/);
|
||||
|
||||
const resolution = resolutionMatch ? resolutionMatch[1] : 'Unknown';
|
||||
const bandwidth = bandwidthMatch ? parseInt(bandwidthMatch[1]) : 0;
|
||||
|
||||
// Determine quality label
|
||||
let quality = 'Unknown';
|
||||
if (resolution.includes('1920x1080')) quality = '1080p';
|
||||
else if (resolution.includes('1280x720')) quality = '720p';
|
||||
else if (resolution.includes('640x360')) quality = '360p';
|
||||
else if (resolution.includes('854x480')) quality = '480p';
|
||||
|
||||
qualities.push({
|
||||
quality,
|
||||
resolution,
|
||||
bandwidth,
|
||||
url: nextLine.startsWith('http') ? nextLine : new URL(nextLine, masterUrl).href
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by bandwidth (highest first)
|
||||
qualities.sort((a, b) => b.bandwidth - a.bandwidth);
|
||||
|
||||
return qualities;
|
||||
} catch (error) {
|
||||
console.error('Error parsing M3U8 qualities:', error.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async extractM3u8Links(query, episodeNumber = null, seasonNumber = null) {
|
||||
try {
|
||||
// Search for content
|
||||
const searchResults = await this.search(query);
|
||||
if (searchResults.length === 0) {
|
||||
console.log('No search results found');
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log(`Found ${searchResults.length} results`);
|
||||
|
||||
// Try to find exact match first, then partial match
|
||||
let selectedResult = searchResults.find(result =>
|
||||
result.title.toLowerCase() === query.toLowerCase()
|
||||
);
|
||||
|
||||
if (!selectedResult) {
|
||||
// Look for best partial match (contains all words from query)
|
||||
const queryWords = query.toLowerCase().split(' ');
|
||||
selectedResult = searchResults.find(result => {
|
||||
const titleLower = result.title.toLowerCase();
|
||||
return queryWords.every(word => titleLower.includes(word));
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback to first result if no good match found
|
||||
if (!selectedResult) {
|
||||
selectedResult = searchResults[0];
|
||||
}
|
||||
|
||||
console.log(`Selected: ${selectedResult.title}`);
|
||||
|
||||
// Get content details
|
||||
const contentDetails = await this.getContentDetails(selectedResult.url);
|
||||
if (!contentDetails) {
|
||||
console.log('Could not get content details');
|
||||
return [];
|
||||
}
|
||||
|
||||
let dataToProcess = [];
|
||||
|
||||
if (contentDetails.type === 'movie') {
|
||||
dataToProcess.push(contentDetails.data);
|
||||
} else {
|
||||
// For TV series, filter by episode/season if specified
|
||||
let episodes = contentDetails.episodes;
|
||||
|
||||
if (seasonNumber) {
|
||||
episodes = episodes.filter(ep => ep.season === seasonNumber);
|
||||
}
|
||||
|
||||
if (episodeNumber) {
|
||||
episodes = episodes.filter(ep => ep.episode === episodeNumber);
|
||||
}
|
||||
|
||||
if (episodes.length === 0) {
|
||||
console.log('No matching episodes found');
|
||||
return [];
|
||||
}
|
||||
|
||||
// Use first matching episode or all if no specific episode requested
|
||||
const targetEpisode = episodeNumber ? episodes[0] : episodes[0];
|
||||
console.log(`Selected episode: S${targetEpisode.season}E${targetEpisode.episode} - ${targetEpisode.name}`);
|
||||
dataToProcess.push(targetEpisode.data);
|
||||
}
|
||||
|
||||
const allM3u8Links = [];
|
||||
|
||||
// Process all data in parallel
|
||||
const allPromises = [];
|
||||
|
||||
for (const data of dataToProcess) {
|
||||
// Get server links
|
||||
const serverLinksPromise = this.getServerLinks(data).then(async (serverLinks) => {
|
||||
console.log(`Found ${serverLinks.length} servers`);
|
||||
|
||||
// Process all server links in parallel
|
||||
const linkPromises = serverLinks.map(async (linkId) => {
|
||||
try {
|
||||
// Get source URL
|
||||
const sourceUrl = await this.getSourceUrl(linkId);
|
||||
if (!sourceUrl) return null;
|
||||
|
||||
console.log(`Source URL: ${sourceUrl}`);
|
||||
|
||||
// Check if it's a videostr URL
|
||||
if (sourceUrl.includes('videostr.net')) {
|
||||
const result = await this.extractVideostrM3u8(sourceUrl);
|
||||
if (result) {
|
||||
return {
|
||||
source: 'videostr',
|
||||
m3u8Url: result.m3u8Url,
|
||||
qualities: result.qualities,
|
||||
headers: result.headers
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error(`Error processing link ${linkId}:`, error.message);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.all(linkPromises);
|
||||
});
|
||||
|
||||
allPromises.push(serverLinksPromise);
|
||||
}
|
||||
|
||||
// Wait for all promises to complete
|
||||
const results = await Promise.all(allPromises);
|
||||
|
||||
// Flatten and filter results
|
||||
for (const serverResults of results) {
|
||||
for (const result of serverResults) {
|
||||
if (result) {
|
||||
allM3u8Links.push(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allM3u8Links;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Extraction error:', error.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CLI usage
|
||||
if (require.main === module) {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0) {
|
||||
console.log('Usage: node myflixer-extractor.js "<search query>" [episode] [season]');
|
||||
console.log('Examples:');
|
||||
console.log(' node myflixer-extractor.js "Avengers Endgame"');
|
||||
console.log(' node myflixer-extractor.js "Breaking Bad" 1 1 # Season 1, Episode 1');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const query = args[0];
|
||||
const episode = args[1] ? parseInt(args[1]) : null;
|
||||
const season = args[2] ? parseInt(args[2]) : null;
|
||||
|
||||
const extractor = new MyFlixerExtractor();
|
||||
|
||||
extractor.extractM3u8Links(query, episode, season)
|
||||
.then(links => {
|
||||
if (links.length === 0) {
|
||||
console.log('No M3U8 links found');
|
||||
} else {
|
||||
console.log('\n=== EXTRACTED M3U8 LINKS ===');
|
||||
links.forEach((link, index) => {
|
||||
console.log(`\nLink ${index + 1}:`);
|
||||
console.log(`Source: ${link.source}`);
|
||||
console.log(`Master M3U8 URL: ${link.m3u8Url}`);
|
||||
console.log(`Headers: ${JSON.stringify(link.headers, null, 2)}`);
|
||||
|
||||
if (link.qualities && link.qualities.length > 0) {
|
||||
console.log('Available Qualities:');
|
||||
link.qualities.forEach((quality, qIndex) => {
|
||||
console.log(` ${qIndex + 1}. ${quality.quality} (${quality.resolution}) - ${Math.round(quality.bandwidth/1000)}kbps`);
|
||||
console.log(` URL: ${quality.url}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = MyFlixerExtractor;
|
||||
|
|
@ -1,740 +0,0 @@
|
|||
// NetMirror Scraper for Nuvio Local Scrapers
|
||||
// React Native compatible version - No async/await for sandbox compatibility
|
||||
// Fetches streaming links from net2025.cc for Netflix, Prime Video, and Disney+ content
|
||||
|
||||
console.log('[NetMirror] Initializing NetMirror provider');
|
||||
|
||||
// Constants
|
||||
const TMDB_API_KEY = "439c478a771f35c05022f9feabcca01c";
|
||||
const NETMIRROR_BASE = 'https://a.net2025.cc';
|
||||
const BASE_HEADERS = {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
'Accept-Language': 'en-US,en;q=0.5',
|
||||
'Connection': 'keep-alive'
|
||||
};
|
||||
|
||||
// Global cookie storage
|
||||
let globalCookie = '';
|
||||
let cookieTimestamp = 0;
|
||||
const COOKIE_EXPIRY = 54000000; // 15 hours in milliseconds
|
||||
|
||||
// Helper function to make HTTP requests
|
||||
function makeRequest(url, options = {}) {
|
||||
return fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
...BASE_HEADERS,
|
||||
...options.headers
|
||||
},
|
||||
timeout: 10000
|
||||
}).then(function (response) {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
// Get current Unix timestamp
|
||||
function getUnixTime() {
|
||||
return Math.floor(Date.now() / 1000);
|
||||
}
|
||||
|
||||
// Bypass authentication and get valid cookie
|
||||
function bypass() {
|
||||
// Check if we have a valid cached cookie
|
||||
const now = Date.now();
|
||||
if (globalCookie && cookieTimestamp && (now - cookieTimestamp) < COOKIE_EXPIRY) {
|
||||
console.log('[NetMirror] Using cached authentication cookie');
|
||||
return Promise.resolve(globalCookie);
|
||||
}
|
||||
|
||||
console.log('[NetMirror] Bypassing authentication...');
|
||||
|
||||
function attemptBypass(attempts) {
|
||||
if (attempts >= 5) {
|
||||
throw new Error('Max bypass attempts reached');
|
||||
}
|
||||
|
||||
return makeRequest(`${NETMIRROR_BASE}/tv/p.php`, {
|
||||
method: 'POST',
|
||||
headers: BASE_HEADERS
|
||||
}).then(function (response) {
|
||||
// Extract cookie from response headers before reading text
|
||||
const setCookieHeader = response.headers.get('set-cookie');
|
||||
let extractedCookie = null;
|
||||
|
||||
if (setCookieHeader && (typeof setCookieHeader === 'string' || Array.isArray(setCookieHeader))) {
|
||||
const cookieString = Array.isArray(setCookieHeader) ? setCookieHeader.join('; ') : setCookieHeader;
|
||||
const cookieMatch = cookieString.match(/t_hash_t=([^;]+)/);
|
||||
if (cookieMatch) {
|
||||
extractedCookie = cookieMatch[1];
|
||||
}
|
||||
}
|
||||
|
||||
return response.text().then(function (responseText) {
|
||||
// Check if response contains success indicator
|
||||
if (!responseText.includes('"r":"n"')) {
|
||||
console.log(`[NetMirror] Bypass attempt ${attempts + 1} failed, retrying...`);
|
||||
return attemptBypass(attempts + 1);
|
||||
}
|
||||
|
||||
if (extractedCookie) {
|
||||
globalCookie = extractedCookie;
|
||||
cookieTimestamp = Date.now();
|
||||
console.log('[NetMirror] Authentication successful');
|
||||
return globalCookie;
|
||||
}
|
||||
|
||||
throw new Error('Failed to extract authentication cookie');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return attemptBypass(0);
|
||||
}
|
||||
|
||||
// Search for content on specific platform
|
||||
function searchContent(query, platform) {
|
||||
console.log(`[NetMirror] Searching for "${query}" on ${platform}...`);
|
||||
|
||||
const ottMap = {
|
||||
'netflix': 'nf',
|
||||
'primevideo': 'pv',
|
||||
'disney': 'hs'
|
||||
};
|
||||
|
||||
const ott = ottMap[platform.toLowerCase()] || 'nf';
|
||||
|
||||
return bypass().then(function (cookie) {
|
||||
const cookies = {
|
||||
't_hash_t': cookie,
|
||||
'hd': 'on',
|
||||
'ott': ott
|
||||
};
|
||||
|
||||
const cookieString = Object.entries(cookies)
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join('; ');
|
||||
|
||||
// Platform-specific search endpoints
|
||||
const searchEndpoints = {
|
||||
'netflix': `${NETMIRROR_BASE}/search.php`,
|
||||
'primevideo': `${NETMIRROR_BASE}/pv/search.php`,
|
||||
'disney': `${NETMIRROR_BASE}/mobile/hs/search.php`
|
||||
};
|
||||
|
||||
const searchUrl = searchEndpoints[platform.toLowerCase()] || searchEndpoints['netflix'];
|
||||
|
||||
return makeRequest(
|
||||
`${searchUrl}?s=${encodeURIComponent(query)}&t=${getUnixTime()}`,
|
||||
{
|
||||
headers: {
|
||||
...BASE_HEADERS,
|
||||
'Cookie': cookieString,
|
||||
'Referer': `${NETMIRROR_BASE}/tv/home`
|
||||
}
|
||||
}
|
||||
);
|
||||
}).then(function (response) {
|
||||
return response.json();
|
||||
}).then(function (searchData) {
|
||||
if (searchData.searchResult && searchData.searchResult.length > 0) {
|
||||
console.log(`[NetMirror] Found ${searchData.searchResult.length} results`);
|
||||
return searchData.searchResult.map(item => ({
|
||||
id: item.id,
|
||||
title: item.t,
|
||||
posterUrl: `https://imgcdn.media/poster/v/${item.id}.jpg`
|
||||
}));
|
||||
} else {
|
||||
console.log('[NetMirror] No results found');
|
||||
return [];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Get episodes from specific season
|
||||
function getEpisodesFromSeason(seriesId, seasonId, platform, page) {
|
||||
const ottMap = {
|
||||
'netflix': 'nf',
|
||||
'primevideo': 'pv',
|
||||
'disney': 'hs'
|
||||
};
|
||||
|
||||
const ott = ottMap[platform.toLowerCase()] || 'nf';
|
||||
|
||||
return bypass().then(function (cookie) {
|
||||
const cookies = {
|
||||
't_hash_t': cookie,
|
||||
'ott': ott,
|
||||
'hd': 'on'
|
||||
};
|
||||
|
||||
const cookieString = Object.entries(cookies)
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join('; ');
|
||||
|
||||
const episodes = [];
|
||||
let currentPage = page || 1;
|
||||
|
||||
// Platform-specific episodes endpoints
|
||||
const episodesEndpoints = {
|
||||
'netflix': `${NETMIRROR_BASE}/episodes.php`,
|
||||
'primevideo': `${NETMIRROR_BASE}/pv/episodes.php`,
|
||||
'disney': `${NETMIRROR_BASE}/mobile/hs/episodes.php`
|
||||
};
|
||||
|
||||
const episodesUrl = episodesEndpoints[platform.toLowerCase()] || episodesEndpoints['netflix'];
|
||||
|
||||
function fetchPage(pageNum) {
|
||||
return makeRequest(
|
||||
`${episodesUrl}?s=${seasonId}&series=${seriesId}&t=${getUnixTime()}&page=${pageNum}`,
|
||||
{
|
||||
headers: {
|
||||
...BASE_HEADERS,
|
||||
'Cookie': cookieString,
|
||||
'Referer': `${NETMIRROR_BASE}/tv/home`
|
||||
}
|
||||
}
|
||||
).then(function (response) {
|
||||
return response.json();
|
||||
}).then(function (episodeData) {
|
||||
if (episodeData.episodes) {
|
||||
episodes.push(...episodeData.episodes);
|
||||
}
|
||||
|
||||
if (episodeData.nextPageShow === 0) {
|
||||
return episodes;
|
||||
} else {
|
||||
return fetchPage(pageNum + 1);
|
||||
}
|
||||
}).catch(function (error) {
|
||||
console.log(`[NetMirror] Failed to load episodes from season ${seasonId}, page ${pageNum}`);
|
||||
return episodes;
|
||||
});
|
||||
}
|
||||
|
||||
return fetchPage(currentPage);
|
||||
});
|
||||
}
|
||||
|
||||
// Load content details
|
||||
function loadContent(contentId, platform) {
|
||||
console.log(`[NetMirror] Loading content details for ID: ${contentId}`);
|
||||
|
||||
const ottMap = {
|
||||
'netflix': 'nf',
|
||||
'primevideo': 'pv',
|
||||
'disney': 'hs'
|
||||
};
|
||||
|
||||
const ott = ottMap[platform.toLowerCase()] || 'nf';
|
||||
|
||||
return bypass().then(function (cookie) {
|
||||
const cookies = {
|
||||
't_hash_t': cookie,
|
||||
'ott': ott,
|
||||
'hd': 'on'
|
||||
};
|
||||
|
||||
const cookieString = Object.entries(cookies)
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join('; ');
|
||||
|
||||
// Platform-specific post endpoints
|
||||
const postEndpoints = {
|
||||
'netflix': `${NETMIRROR_BASE}/post.php`,
|
||||
'primevideo': `${NETMIRROR_BASE}/pv/post.php`,
|
||||
'disney': `${NETMIRROR_BASE}/mobile/hs/post.php`
|
||||
};
|
||||
|
||||
const postUrl = postEndpoints[platform.toLowerCase()] || postEndpoints['netflix'];
|
||||
|
||||
return makeRequest(
|
||||
`${postUrl}?id=${contentId}&t=${getUnixTime()}`,
|
||||
{
|
||||
headers: {
|
||||
...BASE_HEADERS,
|
||||
'Cookie': cookieString,
|
||||
'Referer': `${NETMIRROR_BASE}/tv/home`
|
||||
}
|
||||
}
|
||||
);
|
||||
}).then(function (response) {
|
||||
return response.json();
|
||||
}).then(function (postData) {
|
||||
console.log(`[NetMirror] Loaded: ${postData.title}`);
|
||||
|
||||
let allEpisodes = postData.episodes || [];
|
||||
|
||||
// If this is a TV series, fetch episodes from all seasons
|
||||
if (postData.episodes && postData.episodes.length > 0 && postData.episodes[0] !== null) {
|
||||
console.log('[NetMirror] Loading episodes from all seasons...');
|
||||
|
||||
// Create a promise chain to load all episodes
|
||||
let episodePromise = Promise.resolve();
|
||||
|
||||
// Add episodes from current season if nextPageShow indicates more pages
|
||||
if (postData.nextPageShow === 1 && postData.nextPageSeason) {
|
||||
episodePromise = episodePromise.then(function () {
|
||||
return getEpisodesFromSeason(contentId, postData.nextPageSeason, platform, 2);
|
||||
}).then(function (additionalEpisodes) {
|
||||
allEpisodes.push(...additionalEpisodes);
|
||||
});
|
||||
}
|
||||
|
||||
// Add episodes from other seasons (excluding the last one which is current)
|
||||
if (postData.season && postData.season.length > 1) {
|
||||
const otherSeasons = postData.season.slice(0, -1); // Remove last season
|
||||
|
||||
otherSeasons.forEach(function (season) {
|
||||
episodePromise = episodePromise.then(function () {
|
||||
return getEpisodesFromSeason(contentId, season.id, platform, 1);
|
||||
}).then(function (seasonEpisodes) {
|
||||
allEpisodes.push(...seasonEpisodes);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return episodePromise.then(function () {
|
||||
console.log(`[NetMirror] Loaded ${allEpisodes.filter(ep => ep !== null).length} total episodes`);
|
||||
|
||||
return {
|
||||
id: contentId,
|
||||
title: postData.title,
|
||||
description: postData.desc,
|
||||
year: postData.year,
|
||||
episodes: allEpisodes,
|
||||
seasons: postData.season || [],
|
||||
isMovie: !postData.episodes || postData.episodes.length === 0 || postData.episodes[0] === null
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
id: contentId,
|
||||
title: postData.title,
|
||||
description: postData.desc,
|
||||
year: postData.year,
|
||||
episodes: allEpisodes,
|
||||
seasons: postData.season || [],
|
||||
isMovie: !postData.episodes || postData.episodes.length === 0 || postData.episodes[0] === null
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Get streaming links
|
||||
function getStreamingLinks(contentId, title, platform) {
|
||||
console.log(`[NetMirror] Getting streaming links for: ${title}`);
|
||||
|
||||
const ottMap = {
|
||||
'netflix': 'nf',
|
||||
'primevideo': 'pv',
|
||||
'disney': 'hs'
|
||||
};
|
||||
|
||||
const ott = ottMap[platform.toLowerCase()] || 'nf';
|
||||
|
||||
return bypass().then(function (cookie) {
|
||||
const cookies = {
|
||||
't_hash_t': cookie,
|
||||
'ott': ott,
|
||||
'hd': 'on'
|
||||
};
|
||||
|
||||
const cookieString = Object.entries(cookies)
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join('; ');
|
||||
|
||||
// Platform-specific playlist endpoints
|
||||
const playlistEndpoints = {
|
||||
'netflix': `${NETMIRROR_BASE}/tv/playlist.php`,
|
||||
'primevideo': `${NETMIRROR_BASE}/mobile/pv/playlist.php`,
|
||||
'disney': `${NETMIRROR_BASE}/mobile/hs/playlist.php`
|
||||
};
|
||||
|
||||
const playlistUrl = playlistEndpoints[platform.toLowerCase()] || playlistEndpoints['netflix'];
|
||||
|
||||
return makeRequest(
|
||||
`${playlistUrl}?id=${contentId}&t=${encodeURIComponent(title)}&tm=${getUnixTime()}`,
|
||||
{
|
||||
headers: {
|
||||
...BASE_HEADERS,
|
||||
'Cookie': cookieString,
|
||||
'Referer': `${NETMIRROR_BASE}/tv/home`
|
||||
}
|
||||
}
|
||||
);
|
||||
}).then(function (response) {
|
||||
return response.json();
|
||||
}).then(function (playlist) {
|
||||
if (!Array.isArray(playlist) || playlist.length === 0) {
|
||||
console.log('[NetMirror] No streaming links found');
|
||||
return { sources: [], subtitles: [] };
|
||||
}
|
||||
|
||||
const sources = [];
|
||||
const subtitles = [];
|
||||
|
||||
playlist.forEach(item => {
|
||||
if (item.sources) {
|
||||
item.sources.forEach(source => {
|
||||
// Convert relative URLs to absolute URLs
|
||||
let fullUrl = source.file;
|
||||
if (source.file.startsWith('/') && !source.file.startsWith('//')) {
|
||||
fullUrl = NETMIRROR_BASE + source.file;
|
||||
} else if (source.file.startsWith('//')) {
|
||||
fullUrl = 'https:' + source.file;
|
||||
}
|
||||
|
||||
sources.push({
|
||||
url: fullUrl,
|
||||
quality: source.label,
|
||||
type: source.type || 'application/x-mpegURL'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (item.tracks) {
|
||||
item.tracks
|
||||
.filter(track => track.kind === 'captions')
|
||||
.forEach(track => {
|
||||
// Convert relative URLs to absolute URLs for subtitles
|
||||
let fullSubUrl = track.file;
|
||||
if (track.file.startsWith('/') && !track.file.startsWith('//')) {
|
||||
fullSubUrl = NETMIRROR_BASE + track.file;
|
||||
} else if (track.file.startsWith('//')) {
|
||||
fullSubUrl = 'https:' + track.file;
|
||||
}
|
||||
|
||||
subtitles.push({
|
||||
url: fullSubUrl,
|
||||
language: track.label
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`[NetMirror] Found ${sources.length} streaming sources and ${subtitles.length} subtitle tracks`);
|
||||
return { sources, subtitles };
|
||||
});
|
||||
}
|
||||
|
||||
// Find episode ID for TV shows
|
||||
function findEpisodeId(episodes, season, episode) {
|
||||
if (!episodes || episodes.length === 0) {
|
||||
console.log('[NetMirror] No episodes found in content data');
|
||||
return null;
|
||||
}
|
||||
|
||||
const validEpisodes = episodes.filter(ep => ep !== null);
|
||||
console.log(`[NetMirror] Found ${validEpisodes.length} valid episodes`);
|
||||
|
||||
if (validEpisodes.length > 0) {
|
||||
console.log(`[NetMirror] Sample episode structure:`, JSON.stringify(validEpisodes[0], null, 2));
|
||||
}
|
||||
|
||||
const targetEpisode = validEpisodes.find(ep => {
|
||||
// Handle different possible episode structure formats
|
||||
let epSeason, epNumber;
|
||||
|
||||
if (ep.s && ep.ep) {
|
||||
epSeason = parseInt(ep.s.replace('S', ''));
|
||||
epNumber = parseInt(ep.ep.replace('E', ''));
|
||||
} else if (ep.season && ep.episode) {
|
||||
epSeason = parseInt(ep.season);
|
||||
epNumber = parseInt(ep.episode);
|
||||
} else if (ep.season_number && ep.episode_number) {
|
||||
epSeason = parseInt(ep.season_number);
|
||||
epNumber = parseInt(ep.episode_number);
|
||||
} else {
|
||||
console.log(`[NetMirror] Unknown episode format:`, ep);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`[NetMirror] Checking episode S${epSeason}E${epNumber} against target S${season}E${episode}`);
|
||||
return epSeason === season && epNumber === episode;
|
||||
});
|
||||
|
||||
if (targetEpisode) {
|
||||
console.log(`[NetMirror] Found target episode:`, targetEpisode);
|
||||
return targetEpisode.id;
|
||||
} else {
|
||||
console.log(`[NetMirror] Target episode S${season}E${episode} not found`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Main function to get streams for TMDB content
|
||||
function getStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) {
|
||||
console.log(`[NetMirror] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}${seasonNum ? `, S${seasonNum}E${episodeNum}` : ''}`);
|
||||
|
||||
// Get TMDB info
|
||||
const tmdbUrl = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY}`;
|
||||
return makeRequest(tmdbUrl).then(function (tmdbResponse) {
|
||||
return tmdbResponse.json();
|
||||
}).then(function (tmdbData) {
|
||||
const title = mediaType === 'tv' ? tmdbData.name : tmdbData.title;
|
||||
const year = mediaType === 'tv' ? tmdbData.first_air_date?.substring(0, 4) : tmdbData.release_date?.substring(0, 4);
|
||||
|
||||
if (!title) {
|
||||
throw new Error('Could not extract title from TMDB response');
|
||||
}
|
||||
|
||||
console.log(`[NetMirror] TMDB Info: "${title}" (${year})`);
|
||||
|
||||
// Try all platforms in sequence, but prioritize Prime Video for certain content
|
||||
let platforms = ['netflix', 'primevideo', 'disney'];
|
||||
|
||||
// Prioritize Prime Video for shows like "The Boys"
|
||||
if (title.toLowerCase().includes('boys') || title.toLowerCase().includes('prime')) {
|
||||
platforms = ['primevideo', 'netflix', 'disney'];
|
||||
}
|
||||
|
||||
console.log(`[NetMirror] Will try search queries: "${title}" and "${title} ${year}"`);
|
||||
|
||||
function calculateSimilarity(str1, str2) {
|
||||
const s1 = str1.toLowerCase().trim();
|
||||
const s2 = str2.toLowerCase().trim();
|
||||
|
||||
if (s1 === s2) return 1.0;
|
||||
|
||||
const words1 = s1.split(/\s+/).filter(w => w.length > 0);
|
||||
const words2 = s2.split(/\s+/).filter(w => w.length > 0);
|
||||
|
||||
// If query is shorter, check if all query words are in title
|
||||
if (words2.length <= words1.length) {
|
||||
let exactMatches = 0;
|
||||
for (const queryWord of words2) {
|
||||
if (words1.includes(queryWord)) {
|
||||
exactMatches++;
|
||||
}
|
||||
}
|
||||
|
||||
// All query words must match for high similarity
|
||||
if (exactMatches === words2.length) {
|
||||
return 0.95 * (exactMatches / words1.length);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if title starts with query
|
||||
if (s1.startsWith(s2)) {
|
||||
return 0.9;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function filterRelevantResults(searchResults, query) {
|
||||
const filtered = searchResults.filter(result => {
|
||||
const similarity = calculateSimilarity(result.title, query);
|
||||
return similarity >= 0.7;
|
||||
});
|
||||
|
||||
// Sort by similarity (highest first)
|
||||
return filtered.sort((a, b) => {
|
||||
const simA = calculateSimilarity(a.title, query);
|
||||
const simB = calculateSimilarity(b.title, query);
|
||||
return simB - simA;
|
||||
});
|
||||
}
|
||||
|
||||
function tryPlatform(platformIndex) {
|
||||
if (platformIndex >= platforms.length) {
|
||||
console.log('[NetMirror] No content found on any platform');
|
||||
return [];
|
||||
}
|
||||
|
||||
const platform = platforms[platformIndex];
|
||||
console.log(`[NetMirror] Trying platform: ${platform}`);
|
||||
|
||||
// Try searching with just the title first
|
||||
function trySearch(withYear) {
|
||||
const searchQuery = withYear ? `${title} ${year}` : title;
|
||||
console.log(`[NetMirror] Searching for: "${searchQuery}"`);
|
||||
|
||||
return searchContent(searchQuery, platform).then(function (searchResults) {
|
||||
if (searchResults.length === 0) {
|
||||
if (!withYear && year) {
|
||||
console.log(`[NetMirror] No results for "${title}", trying with year...`);
|
||||
return trySearch(true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Filter results for relevance
|
||||
const relevantResults = filterRelevantResults(searchResults, title);
|
||||
|
||||
if (relevantResults.length === 0) {
|
||||
console.log(`[NetMirror] Found ${searchResults.length} results but none were relevant enough`);
|
||||
if (!withYear && year) {
|
||||
console.log(`[NetMirror] Trying with year...`);
|
||||
return trySearch(true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Use the most relevant search result
|
||||
const selectedContent = relevantResults[0];
|
||||
console.log(`[NetMirror] Selected: ${selectedContent.title} (ID: ${selectedContent.id}) - filtered from ${searchResults.length} results`);
|
||||
|
||||
return loadContent(selectedContent.id, platform).then(function (contentData) {
|
||||
let targetContentId = selectedContent.id;
|
||||
|
||||
// For TV shows, find the specific episode
|
||||
let episodeData = null;
|
||||
if (mediaType === 'tv' && !contentData.isMovie) {
|
||||
const validEpisodes = contentData.episodes.filter(ep => ep !== null);
|
||||
episodeData = validEpisodes.find(ep => {
|
||||
let epSeason, epNumber;
|
||||
|
||||
if (ep.s && ep.ep) {
|
||||
epSeason = parseInt(ep.s.replace('S', ''));
|
||||
epNumber = parseInt(ep.ep.replace('E', ''));
|
||||
} else if (ep.season && ep.episode) {
|
||||
epSeason = parseInt(ep.season);
|
||||
epNumber = parseInt(ep.episode);
|
||||
} else if (ep.season_number && ep.episode_number) {
|
||||
epSeason = parseInt(ep.season_number);
|
||||
epNumber = parseInt(ep.episode_number);
|
||||
}
|
||||
|
||||
return epSeason === (seasonNum || 1) && epNumber === (episodeNum || 1);
|
||||
});
|
||||
|
||||
if (episodeData) {
|
||||
targetContentId = episodeData.id;
|
||||
console.log(`[NetMirror] Found episode ID: ${episodeData.id}`);
|
||||
} else {
|
||||
console.log(`[NetMirror] Episode S${seasonNum}E${episodeNum} not found`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return getStreamingLinks(targetContentId, title, platform).then(function (streamData) {
|
||||
if (!streamData.sources || streamData.sources.length === 0) {
|
||||
console.log(`[NetMirror] No streaming links found`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Convert to Nuvio stream format
|
||||
const streams = streamData.sources.map(source => {
|
||||
// Extract quality from URL parameters or source label
|
||||
let quality = 'HD';
|
||||
|
||||
// Try to extract quality from URL parameters
|
||||
const urlQualityMatch = source.url.match(/[?&]q=(\d+p)/i);
|
||||
if (urlQualityMatch) {
|
||||
quality = urlQualityMatch[1];
|
||||
} else if (source.quality) {
|
||||
// Try to extract from source label
|
||||
const labelQualityMatch = source.quality.match(/(\d+p)/i);
|
||||
if (labelQualityMatch) {
|
||||
quality = labelQualityMatch[1];
|
||||
} else {
|
||||
// Normalize quality labels
|
||||
const normalizedQuality = source.quality.toLowerCase();
|
||||
if (normalizedQuality.includes('full hd') || normalizedQuality.includes('1080')) {
|
||||
quality = '1080p';
|
||||
} else if (normalizedQuality.includes('hd') || normalizedQuality.includes('720')) {
|
||||
quality = '720p';
|
||||
} else if (normalizedQuality.includes('480')) {
|
||||
quality = '480p';
|
||||
} else {
|
||||
quality = source.quality;
|
||||
}
|
||||
}
|
||||
} else if (source.url.includes('720p')) {
|
||||
quality = '720p';
|
||||
} else if (source.url.includes('480p')) {
|
||||
quality = '480p';
|
||||
} else if (source.url.includes('1080p')) {
|
||||
quality = '1080p';
|
||||
}
|
||||
|
||||
// Build title with episode name if available
|
||||
let streamTitle = `${title} ${year ? `(${year})` : ''} ${quality}`;
|
||||
if (mediaType === 'tv') {
|
||||
const episodeName = episodeData && episodeData.t ? episodeData.t : '';
|
||||
streamTitle += ` S${seasonNum}E${episodeNum}`;
|
||||
if (episodeName) {
|
||||
streamTitle += ` - ${episodeName}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Unified headers for all platforms (Netflix, Prime Video, and Disney)
|
||||
const streamHeaders = {
|
||||
"Accept": "application/vnd.apple.mpegurl, video/mp4, */*",
|
||||
"Origin": "https://net2025.cc",
|
||||
"Referer": "https://net2025.cc/tv/home",
|
||||
"Cookie": "hd=on",
|
||||
"User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 26_0_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/138.0.7204.156 Mobile/15E148 Safari/604.1"
|
||||
};
|
||||
|
||||
return {
|
||||
name: `NetMirror (${platform.charAt(0).toUpperCase() + platform.slice(1)})`,
|
||||
title: streamTitle,
|
||||
url: source.url,
|
||||
quality: quality,
|
||||
type: source.type.includes('mpegURL') ? 'hls' : 'direct',
|
||||
headers: streamHeaders
|
||||
};
|
||||
});
|
||||
|
||||
// Sort streams: Auto quality first, then by quality (highest first)
|
||||
streams.sort((a, b) => {
|
||||
// Auto quality always comes first
|
||||
if (a.quality.toLowerCase() === 'auto' && b.quality.toLowerCase() !== 'auto') {
|
||||
return -1;
|
||||
}
|
||||
if (b.quality.toLowerCase() === 'auto' && a.quality.toLowerCase() !== 'auto') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// If both are Auto or neither is Auto, sort by quality
|
||||
const parseQuality = (quality) => {
|
||||
const match = quality.match(/(\d{3,4})p/i);
|
||||
return match ? parseInt(match[1], 10) : 0;
|
||||
};
|
||||
|
||||
const qualityA = parseQuality(a.quality);
|
||||
const qualityB = parseQuality(b.quality);
|
||||
return qualityB - qualityA; // Highest quality first
|
||||
});
|
||||
|
||||
console.log(`[NetMirror] Successfully processed ${streams.length} streams from ${platform}`);
|
||||
return streams;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return trySearch(false).then(function (result) {
|
||||
if (result) {
|
||||
return result;
|
||||
} else {
|
||||
console.log(`[NetMirror] No content found on ${platform}, trying next platform`);
|
||||
return tryPlatform(platformIndex + 1);
|
||||
}
|
||||
}).catch(function (error) {
|
||||
console.log(`[NetMirror] Error on ${platform}: ${error.message}, trying next platform`);
|
||||
return tryPlatform(platformIndex + 1);
|
||||
});
|
||||
}
|
||||
|
||||
return tryPlatform(0);
|
||||
}).catch(function (error) {
|
||||
console.error(`[NetMirror] Error in getStreams: ${error.message}`);
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
// Export the main function
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { getStreams };
|
||||
} else {
|
||||
// For React Native environment
|
||||
global.getStreams = getStreams;
|
||||
}
|
||||
|
|
@ -1,506 +0,0 @@
|
|||
// ShowBox (OG) Provider for Nuvio Local Scrapers
|
||||
// React Native compatible – no Node core modules, no async/await
|
||||
|
||||
const cheerio = require('cheerio-without-node-native');
|
||||
|
||||
// Constants
|
||||
var TMDB_API_KEY = '439c478a771f35c05022f9feabcca01c';
|
||||
var SHOWBOX_BASE = 'https://www.showbox.media';
|
||||
var PROXY_PREFIX = 'https://timely-taiyaki-81a26d.netlify.app/?destination='; // Proxy all showbox.media
|
||||
var FEBBOX_BASE = 'https://www.febbox.com';
|
||||
var FEBBOX_FILE_SHARE_LIST_URL = FEBBOX_BASE + '/file/file_share_list';
|
||||
var FEBBOX_COOKIE_VALUE = (typeof SCRAPER_SETTINGS !== 'undefined' && SCRAPER_SETTINGS && SCRAPER_SETTINGS.cookie) ? SCRAPER_SETTINGS.cookie : 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3NTA5MzA4NTYsIm5iZiI6MTc1MDkzMDg1NiwiZXhwIjoxNzgyMDM0ODc2LCJkYXRhIjp7InVpZCI6ODQ2NzQ4LCJ0b2tlbiI6ImIzNTllZDk1NjBkMDI5ZmQwY2IyNjdlYTZlMWIwMDlkIn19.WqD3ruYvVx8tyfFuRDMWDaTz1XdvLztW4h_rGt6xt8o';
|
||||
var DEFAULT_REGION = (typeof SCRAPER_SETTINGS !== 'undefined' && SCRAPER_SETTINGS && SCRAPER_SETTINGS.region) ? SCRAPER_SETTINGS.region : null;
|
||||
|
||||
// Headers
|
||||
var DEFAULT_HEADERS = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US,en;q=0.5',
|
||||
'Connection': 'keep-alive'
|
||||
};
|
||||
|
||||
function makeRequest(url, options) {
|
||||
options = options || {};
|
||||
return fetch(url, {
|
||||
method: options.method || 'GET',
|
||||
body: options.body,
|
||||
headers: Object.assign({}, DEFAULT_HEADERS, options.headers || {})
|
||||
}).then(function (response) {
|
||||
if (!response.ok) {
|
||||
throw new Error('HTTP ' + response.status + ': ' + response.statusText);
|
||||
}
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
function proxify(url) {
|
||||
return PROXY_PREFIX + encodeURIComponent(url);
|
||||
}
|
||||
|
||||
// TMDB minimal helper
|
||||
function getTMDBDetails(tmdbId, mediaType) {
|
||||
var u = 'https://api.themoviedb.org/3/' + (mediaType === 'tv' ? 'tv' : 'movie') + '/' + tmdbId + '?api_key=' + TMDB_API_KEY;
|
||||
return makeRequest(u).then(function (r) { return r.json(); }).then(function (data) {
|
||||
if (mediaType === 'movie') {
|
||||
return { title: data.title, original_title: data.original_title, year: data.release_date ? data.release_date.split('-')[0] : null };
|
||||
} else {
|
||||
return { title: data.name, original_title: data.original_name, year: data.first_air_date ? data.first_air_date.split('-')[0] : null };
|
||||
}
|
||||
}).catch(function () { return null; });
|
||||
}
|
||||
|
||||
function normalizeTitle(s) {
|
||||
return (s || '').toLowerCase().replace(/[^a-z0-9\s]/g, ' ').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function calculateSimilarity(a, b) {
|
||||
var s1 = normalizeTitle(a), s2 = normalizeTitle(b);
|
||||
if (s1 === s2) return 1.0;
|
||||
var w1 = s1.split(' '), w2 = s2.split(' ');
|
||||
var matches = 0;
|
||||
for (var i = 0; i < w1.length; i++) {
|
||||
var w = w1[i];
|
||||
if (w.length > 2 && w2.some(function (x) { return x.indexOf(w) !== -1 || w.indexOf(x) !== -1; })) matches++;
|
||||
}
|
||||
return matches / Math.max(w1.length, w2.length || 1);
|
||||
}
|
||||
|
||||
function findBestMatch(results, query, year) {
|
||||
if (!results || results.length === 0) return null;
|
||||
var scored = results.map(function (r) {
|
||||
var score = 0;
|
||||
score += calculateSimilarity(r.title, query) * 100;
|
||||
if (year && r.year && String(r.year) === String(year)) score += 35;
|
||||
if (normalizeTitle(r.title) === normalizeTitle(query)) score += 50;
|
||||
return { item: r, score: score };
|
||||
});
|
||||
scored.sort(function (a, b) { return b.score - a.score; });
|
||||
return scored[0].item;
|
||||
}
|
||||
|
||||
// Search ShowBox via proxy
|
||||
function searchShowbox(query, year, mediaType) {
|
||||
var searchUrl = SHOWBOX_BASE + '/search?keyword=' + encodeURIComponent(query + (year ? (' ' + year) : ''));
|
||||
return makeRequest(proxify(searchUrl)).then(function (r) { return r.text(); }).then(function (html) {
|
||||
var $ = cheerio.load(html);
|
||||
var results = [];
|
||||
$('div.film-poster a.film-poster-ahref').each(function (i, el) {
|
||||
var $el = $(el);
|
||||
var title = ($el.attr('title') || '').trim();
|
||||
var href = $el.attr('href') || '';
|
||||
if (title && href) {
|
||||
var full = SHOWBOX_BASE + (href.indexOf('/') === 0 ? '' : '/') + href;
|
||||
var y = null;
|
||||
var ym = title.match(/\((\d{4})\)$/);
|
||||
if (ym && ym[1]) y = ym[1];
|
||||
// type check from URL
|
||||
var isMovie = href.indexOf('/movie/') !== -1;
|
||||
var isTv = href.indexOf('/tv/') !== -1;
|
||||
if ((mediaType === 'movie' && isMovie) || (mediaType !== 'movie' && isTv)) {
|
||||
results.push({ title: title, url: full, year: y });
|
||||
} else {
|
||||
// still include but penalize later via similarity
|
||||
results.push({ title: title, url: full, year: y });
|
||||
}
|
||||
}
|
||||
});
|
||||
return results;
|
||||
}).catch(function () { return []; });
|
||||
}
|
||||
|
||||
// Extract FebBox share link from ShowBox detail
|
||||
function extractFebboxLink(detailUrl) {
|
||||
return makeRequest(proxify(detailUrl)).then(function (r) { return r.text(); }).then(function (html) {
|
||||
var $ = cheerio.load(html);
|
||||
var link = null;
|
||||
$('a[href*="febbox.com/share/"]').each(function (i, el) {
|
||||
var h = $(el).attr('href');
|
||||
if (h && h.indexOf('febbox.com/share/') !== -1) { link = h; return false; }
|
||||
});
|
||||
if (!link) {
|
||||
var scripts = $('script').map(function (i, el) { return $(el).html() || ''; }).get().join('\n');
|
||||
var m = scripts.match(/['"](https?:\/\/www\.febbox\.com\/share\/[a-zA-Z0-9-]+)['"]/);
|
||||
if (m && m[1]) link = m[1];
|
||||
}
|
||||
if (link) return link;
|
||||
// Fallback: try extracting numeric ID/type then call share_link API via proxy
|
||||
var idAndType = extractShowboxIdAndType(html, detailUrl);
|
||||
if (idAndType && idAndType.id && idAndType.type) {
|
||||
return getShareLinkFromApi(idAndType.id, idAndType.type).then(function (apiLink) { return apiLink || null; });
|
||||
}
|
||||
return null;
|
||||
}).catch(function () { return null; });
|
||||
}
|
||||
|
||||
// Extract numeric content id and type (1 movie, 2 tv) from detail page
|
||||
function extractShowboxIdAndType(html, url) {
|
||||
try {
|
||||
var id = null; var type = null;
|
||||
var um = (url || '').match(/\/(movie|tv)\/detail\/(\d+)/);
|
||||
if (um) {
|
||||
id = um[2];
|
||||
type = um[1] === 'movie' ? '1' : '2';
|
||||
}
|
||||
if (!id) {
|
||||
var $ = cheerio.load(html);
|
||||
var link = $('h2.heading-name a[href*="/detail/"], h1.heading-name a[href*="/detail/"]').first();
|
||||
if (link && link.length) {
|
||||
var href = link.attr('href') || '';
|
||||
var hm = href.match(/\/(movie|tv)\/detail\/(\d+)/);
|
||||
if (hm) { id = hm[2]; type = hm[1] === 'movie' ? '1' : '2'; }
|
||||
}
|
||||
if (!id) {
|
||||
var shareDiv = $('div.sharethis-inline-share-buttons');
|
||||
var href2 = null;
|
||||
shareDiv.find('a[href*="/detail/"]').each(function (i, el) { if (!href2) href2 = $(el).attr('href'); });
|
||||
if (!href2) {
|
||||
var durl = shareDiv.attr('data-url') || '';
|
||||
if (durl) href2 = durl;
|
||||
}
|
||||
if (href2) {
|
||||
var hm2 = href2.match(/\/(movie|tv)\/detail\/(\d+)/);
|
||||
if (hm2) { id = hm2[2]; type = hm2[1] === 'movie' ? '1' : '2'; }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (id && type) return { id: id, type: type };
|
||||
return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Call ShowBox API to get FebBox link (proxied)
|
||||
function getShareLinkFromApi(id, type) {
|
||||
var apiUrl = SHOWBOX_BASE + '/index/share_link?id=' + encodeURIComponent(id) + '&type=' + encodeURIComponent(type);
|
||||
return makeRequest(proxify(apiUrl), { headers: { 'Accept': 'application/json, text/javascript, */*; q=0.01', 'X-Requested-With': 'XMLHttpRequest' } })
|
||||
.then(function (r) { return r.text(); })
|
||||
.then(function (txt) {
|
||||
try {
|
||||
var json = JSON.parse(txt);
|
||||
if (json && json.code === 1 && json.data && json.data.link) return json.data.link;
|
||||
} catch (e) { /* ignore */ }
|
||||
return null;
|
||||
}).catch(function () { return null; });
|
||||
}
|
||||
|
||||
// Parse direct jwplayer sources from FebBox share page (if present)
|
||||
function parseDirectSourcesFromShare(html) {
|
||||
try {
|
||||
var m = html.match(/var\s+sources\s*=\s*(\[.*?\]);/s);
|
||||
if (!m) return [];
|
||||
var arr = JSON.parse(m[1]);
|
||||
return (arr || []).filter(function (x) { return x && x.file; }).map(function (x) {
|
||||
return { label: String(x.label || 'ORG'), url: String(x.file) };
|
||||
});
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function getQualityFromLabel(label) {
|
||||
var l = String(label || '').toLowerCase();
|
||||
if (l.indexOf('2160') !== -1 || l.indexOf('4k') !== -1 || l.indexOf('uhd') !== -1) return '2160p';
|
||||
if (l.indexOf('1080') !== -1) return '1080p';
|
||||
if (l.indexOf('720') !== -1) return '720p';
|
||||
if (l.indexOf('480') !== -1) return '480p';
|
||||
if (l.indexOf('360') !== -1) return '360p';
|
||||
return 'ORG';
|
||||
}
|
||||
|
||||
// Size helpers via HEAD request
|
||||
function formatSize(bytes) {
|
||||
var b = parseInt(bytes, 10);
|
||||
if (isNaN(b) || b <= 0) return null;
|
||||
if (b < 1024) return b + ' B';
|
||||
if (b < 1024 * 1024) return (b / 1024).toFixed(2) + ' KB';
|
||||
if (b < 1024 * 1024 * 1024) return (b / (1024 * 1024)).toFixed(2) + ' MB';
|
||||
return (b / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
|
||||
}
|
||||
|
||||
function fetchSizeForUrl(url) {
|
||||
try {
|
||||
if ((url || '').toLowerCase().indexOf('.m3u8') !== -1) return Promise.resolve('Playlist (size N/A)');
|
||||
// Try HEAD without Range to get full content-length
|
||||
return fetch(url, { method: 'HEAD', headers: { 'User-Agent': DEFAULT_HEADERS['User-Agent'] } })
|
||||
.then(function (res) {
|
||||
if (!res) return null;
|
||||
var len = res.headers && res.headers.get ? res.headers.get('content-length') : null;
|
||||
if (len) return formatSize(len) || null;
|
||||
var cr = res.headers && res.headers.get ? res.headers.get('content-range') : null; // bytes 0-0/12345
|
||||
if (cr) {
|
||||
var m = cr.match(/\/(\d+)$/);
|
||||
if (m && m[1]) return formatSize(m[1]) || null;
|
||||
}
|
||||
// Fallback: do a tiny ranged GET to read Content-Range total size
|
||||
return fetch(url, { method: 'GET', headers: { 'Range': 'bytes=0-0', 'User-Agent': DEFAULT_HEADERS['User-Agent'] } })
|
||||
.then(function (r2) {
|
||||
var cr2 = r2.headers && r2.headers.get ? r2.headers.get('content-range') : null;
|
||||
if (cr2) {
|
||||
var m2 = cr2.match(/\/(\d+)$/);
|
||||
if (m2 && m2[1]) return formatSize(m2[1]) || null;
|
||||
}
|
||||
var len2 = r2.headers && r2.headers.get ? r2.headers.get('content-length') : null;
|
||||
return len2 ? (formatSize(len2) || null) : null;
|
||||
}).catch(function () { return null; });
|
||||
}).catch(function () { return null; });
|
||||
} catch (e) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
function attachSizes(streams) {
|
||||
var tasks = (streams || []).map(function (s) {
|
||||
return fetchSizeForUrl(s.url).then(function (sz) { s.size = sz; return s; }).catch(function () { return s; });
|
||||
});
|
||||
return Promise.all(tasks).then(function (arr) { return arr; });
|
||||
}
|
||||
|
||||
// Build display title per README: descriptive text, not the URL
|
||||
function buildStreamTitle(label, fileName) {
|
||||
if (fileName && fileName.trim()) return fileName;
|
||||
return (label && String(label).trim()) ? String(label).trim() : 'ORG';
|
||||
}
|
||||
|
||||
// Extract detailed filename (prefer KEY5 param if present)
|
||||
function extractDetailedFilename(url) {
|
||||
try {
|
||||
var u = new URL(url);
|
||||
var key5 = u.searchParams.get('KEY5');
|
||||
if (key5) {
|
||||
try { return decodeURIComponent(key5); } catch (e) { return key5; }
|
||||
}
|
||||
var base = u.pathname.split('/').pop() || '';
|
||||
try { return decodeURIComponent(base); } catch (e2) { return base; }
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function sortPreferMkvOrg(streams) {
|
||||
if (!streams || streams.length === 0) return streams;
|
||||
function baseOrder(q) {
|
||||
if (q === 'ORG') return 6;
|
||||
if (q === '2160p') return 5;
|
||||
if (q === '1080p') return 4;
|
||||
if (q === '720p') return 3;
|
||||
if (q === '480p') return 2;
|
||||
if (q === '360p') return 1;
|
||||
return 0;
|
||||
}
|
||||
function mkvBonus(s) {
|
||||
var fname = (s.fileName || s.title || '').toLowerCase();
|
||||
var url = (s.url || '').toLowerCase();
|
||||
return (fname.indexOf('.mkv') !== -1 || /\.mkv(\?|$)/i.test(url)) ? 1 : 0;
|
||||
}
|
||||
function sizeBytes(sz) {
|
||||
if (!sz) return 0;
|
||||
var m = String(sz).match(/([\d.]+)\s*(gb|mb|kb|b)/i);
|
||||
if (!m) return 0;
|
||||
var v = parseFloat(m[1]);
|
||||
var u = m[2].toLowerCase();
|
||||
var mult = (u==='gb')?1024*1024*1024:(u==='mb')?1024*1024:(u==='kb')?1024:1;
|
||||
return Math.floor(v*mult);
|
||||
}
|
||||
return [].concat(streams).sort(function(a,b){
|
||||
var aBase = baseOrder(a.quality||'');
|
||||
var bBase = baseOrder(b.quality||'');
|
||||
if (aBase !== bBase) return bBase - aBase;
|
||||
var aM = mkvBonus(a), bM = mkvBonus(b);
|
||||
if (aM !== bM) return bM - aM;
|
||||
var aS = sizeBytes(a.size), bS = sizeBytes(b.size);
|
||||
if (aS !== bS) return bS - aS;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
// Extract (share_key, fids[]) from FebBox share page
|
||||
function extractShareMeta(html, url) {
|
||||
var $ = cheerio.load(html);
|
||||
var shareKey = null;
|
||||
var mk = (url || '').match(/\/share\/([a-zA-Z0-9-]+)/);
|
||||
if (mk && mk[1]) shareKey = mk[1];
|
||||
if (!shareKey) {
|
||||
var km = html.match(/(?:var\s+share_key\s*=|share_key:\s*|shareid=)"?([a-zA-Z0-9-]+)"?/);
|
||||
if (km && km[1]) shareKey = km[1];
|
||||
}
|
||||
var fids = [];
|
||||
$('div.file').each(function (i, el) {
|
||||
var $el = $(el);
|
||||
var id = $el.attr('data-id');
|
||||
if (id && /^\d+$/.test(id) && !$el.hasClass('open_dir')) fids.push(id);
|
||||
});
|
||||
fids = Array.from(new Set(fids));
|
||||
return { shareKey: shareKey, fids: fids };
|
||||
}
|
||||
|
||||
// POST to FebBox player to resolve a fid -> sources
|
||||
function resolveFid(fid, shareKey, region) {
|
||||
var url = FEBBOX_BASE + '/file/player';
|
||||
var body = new URLSearchParams();
|
||||
body.append('fid', fid);
|
||||
body.append('share_key', shareKey);
|
||||
return makeRequest(url, {
|
||||
method: 'POST',
|
||||
body: body.toString(),
|
||||
headers: {
|
||||
'Cookie': 'ui=' + FEBBOX_COOKIE_VALUE + (region ? ('; oss_group=' + region) : ''),
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
}).then(function (r) { return r.text(); }).then(function (txt) {
|
||||
// Either direct URL or JS with var sources = [...]
|
||||
if (/^https?:\/\//i.test(txt) && (txt.indexOf('.mp4') !== -1 || txt.indexOf('.m3u8') !== -1)) {
|
||||
return [{ label: 'DirectLink', url: txt.trim() }];
|
||||
}
|
||||
var m = txt.match(/var\s+sources\s*=\s*(\[.*?\]);/s);
|
||||
if (m && m[1]) {
|
||||
try { return JSON.parse(m[1]); } catch (e) { return []; }
|
||||
}
|
||||
try {
|
||||
var json = JSON.parse(txt);
|
||||
if (json && json.msg) return [];
|
||||
} catch (e) { /* ignore */ }
|
||||
return [];
|
||||
}).catch(function () { return []; });
|
||||
}
|
||||
|
||||
// Fetch FebBox share page and return streams
|
||||
function getStreamsFromFebboxShare(shareUrl, type, season, episode, region) {
|
||||
return makeRequest(shareUrl, { headers: { 'Cookie': 'ui=' + FEBBOX_COOKIE_VALUE + (region ? ('; oss_group=' + region) : '') } })
|
||||
.then(function (r) { return r.text(); })
|
||||
.then(function (html) {
|
||||
var direct = parseDirectSourcesFromShare(html);
|
||||
if (direct.length > 0) {
|
||||
return attachSizes(direct.map(function (s) {
|
||||
return {
|
||||
name: 'ShowBox',
|
||||
title: buildStreamTitle(s.label, extractDetailedFilename(s.url)),
|
||||
url: s.url,
|
||||
quality: getQualityFromLabel(s.label),
|
||||
size: null,
|
||||
fileName: extractDetailedFilename(s.url),
|
||||
type: 'direct'
|
||||
};
|
||||
})).then(function (arr) { return sortPreferMkvOrg(arr); });
|
||||
}
|
||||
var meta = extractShareMeta(html, shareUrl);
|
||||
if (!meta.shareKey) return [];
|
||||
|
||||
// If movie or there are direct file fids on root, resolve them directly
|
||||
if (type === 'movie' || (meta.fids && meta.fids.length > 0)) {
|
||||
var fidsDirect = meta.fids || [];
|
||||
var tasksDirect = fidsDirect.map(function (fid) { return resolveFid(fid, meta.shareKey, region).then(function (arr) { return arr || []; }).catch(function () { return []; }); });
|
||||
return Promise.all(tasksDirect).then(function (arrs) {
|
||||
var flatD = [].concat.apply([], arrs).filter(function (x) { return x && x.file; });
|
||||
return attachSizes(flatD.map(function (s) { return { name: 'ShowBox', title: buildStreamTitle(s.label, extractDetailedFilename(s.file)), url: s.file, quality: getQualityFromLabel(s.label), size: null, fileName: extractDetailedFilename(s.file), type: 'direct' }; })).then(function (arr) { return sortPreferMkvOrg(arr); });
|
||||
});
|
||||
}
|
||||
|
||||
// TV folder traversal: find season folder and fetch file list via FebBox API
|
||||
if (type !== 'movie' && season) {
|
||||
var $root = cheerio.load(html);
|
||||
var shareKey = meta.shareKey;
|
||||
var seasonFolderId = null;
|
||||
// locate season folders
|
||||
$root('div.file.open_dir').each(function (i, el) {
|
||||
var $el = $root(el);
|
||||
var id = $el.attr('data-id');
|
||||
var fname = ($el.find('p.file_name').text() || $el.attr('data-path') || '').toLowerCase();
|
||||
var sNum = null;
|
||||
var m1 = fname.match(/season\s+(\d+)/i);
|
||||
var m2 = fname.match(/\bs(\d+)\b/i);
|
||||
var m3 = fname.match(/season(\d+)/i);
|
||||
if (m1 && m1[1]) sNum = parseInt(m1[1], 10);
|
||||
else if (m2 && m2[1]) sNum = parseInt(m2[1], 10);
|
||||
else if (m3 && m3[1]) sNum = parseInt(m3[1], 10);
|
||||
if (!sNum) {
|
||||
var onlyNum = fname.match(/\b(\d+)\b/);
|
||||
if (onlyNum && onlyNum[1]) sNum = parseInt(onlyNum[1], 10);
|
||||
}
|
||||
if (id && sNum === parseInt(season, 10)) seasonFolderId = id;
|
||||
});
|
||||
if (!seasonFolderId) return [];
|
||||
|
||||
// fetch folder content via API
|
||||
var listUrl = FEBBOX_FILE_SHARE_LIST_URL + '?share_key=' + encodeURIComponent(shareKey) + '&parent_id=' + encodeURIComponent(seasonFolderId) + '&is_html=1&pwd=';
|
||||
return makeRequest(listUrl, { headers: { 'Cookie': 'ui=' + FEBBOX_COOKIE_VALUE + (region ? ('; oss_group=' + region) : ''), 'X-Requested-With': 'XMLHttpRequest' } })
|
||||
.then(function (res) { return res.text(); })
|
||||
.then(function (txt) {
|
||||
var folderHtml = txt;
|
||||
try { var j = JSON.parse(txt); if (j && j.html) folderHtml = j.html; } catch (e) { /* keep txt */ }
|
||||
var $folder = cheerio.load(folderHtml || '');
|
||||
var targets = [];
|
||||
$folder('div.file').each(function (i, el) {
|
||||
var $el = $folder(el);
|
||||
var id = $el.attr('data-id');
|
||||
if (!id || /open_dir/.test($el.attr('class') || '')) return;
|
||||
var name = ($el.find('p.file_name').text() || '').toLowerCase();
|
||||
var ep = null;
|
||||
var m = name.match(/(?:e|ep|episode)[\s._-]*0*(\d{1,3})/i);
|
||||
if (m && m[1]) ep = parseInt(m[1], 10);
|
||||
if (!ep) {
|
||||
var only = name.match(/\b(\d{1,3})\b/);
|
||||
if (only && only[1]) ep = parseInt(only[1], 10);
|
||||
}
|
||||
if (!episode || (ep && ep === parseInt(episode, 10))) targets.push(id);
|
||||
});
|
||||
if (targets.length === 0) return [];
|
||||
var tasks = targets.map(function (fid) { return resolveFid(fid, shareKey, region).then(function (arr) { return arr || []; }).catch(function () { return []; }); });
|
||||
return Promise.all(tasks).then(function (arrs) {
|
||||
var flat = [].concat.apply([], arrs).filter(function (x) { return x && x.file; });
|
||||
return attachSizes(flat.map(function (s) { return { name: 'ShowBox', title: buildStreamTitle(s.label, extractDetailedFilename(s.file)), url: s.file, quality: getQualityFromLabel(s.label), size: null, fileName: extractDetailedFilename(s.file), type: 'direct' }; })).then(function (arr) { return sortPreferMkvOrg(arr); });
|
||||
});
|
||||
}).catch(function () { return []; });
|
||||
}
|
||||
|
||||
return [];
|
||||
}).catch(function () { return []; });
|
||||
}
|
||||
|
||||
// Region-aware entry: force a specific oss_group region via cookie
|
||||
function getStreamsByRegion(tmdbId, type, season, episode, region) {
|
||||
type = type || 'movie';
|
||||
var tmdbType = (type === 'series' ? 'tv' : type);
|
||||
return getTMDBDetails(tmdbId, tmdbType).then(function (tmdb) {
|
||||
if (!tmdb || !tmdb.title) return [];
|
||||
return searchShowbox(tmdb.title, tmdb.year, tmdbType === 'movie' ? 'movie' : 'tv').then(function (results) {
|
||||
if (!results || results.length === 0) return [];
|
||||
var best = findBestMatch(results, tmdb.title, tmdb.year) || results[0];
|
||||
return extractFebboxLink(best.url).then(function (shareUrl) {
|
||||
if (!shareUrl) return [];
|
||||
return getStreamsFromFebboxShare(shareUrl, tmdbType === 'movie' ? 'movie' : 'tv', season, episode, region)
|
||||
.then(function (streams) { return sortPreferMkvOrg(streams || []); })
|
||||
.then(function (streams) { return streams.map(function (s) { s.name = 'ShowBox - ' + region; return s; }); });
|
||||
});
|
||||
});
|
||||
}).catch(function () { return []; });
|
||||
}
|
||||
|
||||
// Main entry – Promise-based
|
||||
function getStreams(tmdbId, type, season, episode) {
|
||||
type = type || 'movie';
|
||||
var tmdbType = (type === 'series' ? 'tv' : type);
|
||||
return getTMDBDetails(tmdbId, tmdbType).then(function (tmdb) {
|
||||
if (!tmdb || !tmdb.title) return [];
|
||||
return searchShowbox(tmdb.title, tmdb.year, tmdbType === 'movie' ? 'movie' : 'tv').then(function (results) {
|
||||
if (!results || results.length === 0) return [];
|
||||
var best = findBestMatch(results, tmdb.title, tmdb.year) || results[0];
|
||||
return extractFebboxLink(best.url).then(function (shareUrl) {
|
||||
if (!shareUrl) return [];
|
||||
return getStreamsFromFebboxShare(shareUrl, tmdbType === 'movie' ? 'movie' : 'tv', season, episode, DEFAULT_REGION)
|
||||
.then(function (streams) { return sortPreferMkvOrg(streams || []); });
|
||||
});
|
||||
});
|
||||
}).catch(function () { return []; });
|
||||
}
|
||||
|
||||
// Export
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { getStreams, getStreamsByRegion };
|
||||
} else {
|
||||
// eslint-disable-next-line no-undef
|
||||
global.getStreams = getStreams;
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,547 +0,0 @@
|
|||
// Watch32 Scraper for Nuvio Local Scrapers
|
||||
// React Native compatible version - Standalone (no external dependencies)
|
||||
|
||||
// Import cheerio-without-node-native for React Native
|
||||
const cheerio = require('cheerio-without-node-native');
|
||||
console.log('[Watch32] Using cheerio-without-node-native for DOM parsing');
|
||||
|
||||
// Constants
|
||||
const TMDB_API_KEY = "439c478a771f35c05022f9feabcca01c";
|
||||
const MAIN_URL = 'https://watch32.sx';
|
||||
const VIDEOSTR_URL = 'https://videostr.net';
|
||||
|
||||
// Helper function to make HTTP requests
|
||||
function makeRequest(url, options = {}) {
|
||||
const defaultHeaders = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US,en;q=0.5',
|
||||
'Connection': 'keep-alive'
|
||||
};
|
||||
|
||||
return fetch(url, {
|
||||
method: options.method || 'GET',
|
||||
headers: { ...defaultHeaders, ...options.headers },
|
||||
...options
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`[Watch32] Request failed for ${url}: ${error.message}`);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
// Search for content
|
||||
function searchContent(query) {
|
||||
const searchUrl = `${MAIN_URL}/search/${query.replace(/\s+/g, '-')}`;
|
||||
console.log(`[Watch32] Searching: ${searchUrl}`);
|
||||
|
||||
return makeRequest(searchUrl)
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
const $ = cheerio.load(html);
|
||||
const results = [];
|
||||
|
||||
$('.flw-item').each((i, element) => {
|
||||
const title = $(element).find('h2.film-name > a').attr('title');
|
||||
const link = $(element).find('h2.film-name > a').attr('href');
|
||||
const poster = $(element).find('img.film-poster-img').attr('data-src');
|
||||
|
||||
if (title && link) {
|
||||
results.push({
|
||||
title,
|
||||
url: link.startsWith('http') ? link : `${MAIN_URL}${link}`,
|
||||
poster
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`[Watch32] Found ${results.length} search results`);
|
||||
return results;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`[Watch32] Search error: ${error.message}`);
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
// Get content details (movie or TV series)
|
||||
function getContentDetails(url) {
|
||||
console.log(`[Watch32] Getting content details: ${url}`);
|
||||
|
||||
return makeRequest(url)
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
const $ = cheerio.load(html);
|
||||
const contentId = $('.detail_page-watch').attr('data-id');
|
||||
const name = $('.detail_page-infor h2.heading-name > a').text();
|
||||
const isMovie = url.includes('movie');
|
||||
|
||||
if (isMovie) {
|
||||
return {
|
||||
type: 'movie',
|
||||
name,
|
||||
data: `list/${contentId}`
|
||||
};
|
||||
} else {
|
||||
// Get TV series episodes
|
||||
return makeRequest(`${MAIN_URL}/ajax/season/list/${contentId}`)
|
||||
.then(response => response.text())
|
||||
.then(seasonsHtml => {
|
||||
const $seasons = cheerio.load(seasonsHtml);
|
||||
const episodes = [];
|
||||
const seasonPromises = [];
|
||||
|
||||
$seasons('a.ss-item').each((i, season) => {
|
||||
const seasonId = $(season).attr('data-id');
|
||||
const seasonNum = $(season).text().replace('Season ', '');
|
||||
|
||||
const episodePromise = makeRequest(`${MAIN_URL}/ajax/season/episodes/${seasonId}`)
|
||||
.then(response => response.text())
|
||||
.then(episodesHtml => {
|
||||
const $episodes = cheerio.load(episodesHtml);
|
||||
|
||||
$episodes('a.eps-item').each((i, episode) => {
|
||||
const epId = $(episode).attr('data-id');
|
||||
const title = $(episode).attr('title');
|
||||
const match = title.match(/Eps (\d+): (.+)/);
|
||||
|
||||
if (match) {
|
||||
episodes.push({
|
||||
id: epId,
|
||||
episode: parseInt(match[1]),
|
||||
name: match[2],
|
||||
season: parseInt(seasonNum.replace('Series', '').trim()),
|
||||
data: `servers/${epId}`
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
seasonPromises.push(episodePromise);
|
||||
});
|
||||
|
||||
return Promise.all(seasonPromises)
|
||||
.then(() => ({
|
||||
type: 'series',
|
||||
name,
|
||||
episodes
|
||||
}));
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`[Watch32] Content details error: ${error.message}`);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
// Get server links for content
|
||||
function getServerLinks(data) {
|
||||
console.log(`[Watch32] Getting server links: ${data}`);
|
||||
|
||||
return makeRequest(`${MAIN_URL}/ajax/episode/${data}`)
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
const $ = cheerio.load(html);
|
||||
const servers = [];
|
||||
|
||||
$('a.link-item').each((i, element) => {
|
||||
const linkId = $(element).attr('data-linkid') || $(element).attr('data-id');
|
||||
if (linkId) {
|
||||
servers.push(linkId);
|
||||
}
|
||||
});
|
||||
|
||||
return servers;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`[Watch32] Server links error: ${error.message}`);
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
// Get source URL from link ID
|
||||
function getSourceUrl(linkId) {
|
||||
console.log(`[Watch32] Getting source URL for linkId: ${linkId}`);
|
||||
|
||||
return makeRequest(`${MAIN_URL}/ajax/episode/sources/${linkId}`)
|
||||
.then(response => response.json())
|
||||
.then(data => data.link)
|
||||
.catch(error => {
|
||||
console.error(`[Watch32] Source URL error: ${error.message}`);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
// Extract M3U8 from Videostr
|
||||
function extractVideostrM3u8(url) {
|
||||
console.log(`[Watch32] Extracting from Videostr: ${url}`);
|
||||
|
||||
const headers = {
|
||||
'Accept': '*/*',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Referer': VIDEOSTR_URL,
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; Win64; x64) AppleWebKit/537.36'
|
||||
};
|
||||
|
||||
// Extract ID from URL
|
||||
const id = url.split('/').pop().split('?')[0];
|
||||
|
||||
// Get nonce from embed page
|
||||
return makeRequest(url, { headers })
|
||||
.then(response => response.text())
|
||||
.then(embedHtml => {
|
||||
// Try to find 48-character nonce
|
||||
let nonce = embedHtml.match(/\b[a-zA-Z0-9]{48}\b/);
|
||||
if (nonce) {
|
||||
nonce = nonce[0];
|
||||
} else {
|
||||
// Try to find three 16-character segments
|
||||
const matches = embedHtml.match(/\b([a-zA-Z0-9]{16})\b.*?\b([a-zA-Z0-9]{16})\b.*?\b([a-zA-Z0-9]{16})\b/);
|
||||
if (matches) {
|
||||
nonce = matches[1] + matches[2] + matches[3];
|
||||
}
|
||||
}
|
||||
|
||||
if (!nonce) {
|
||||
throw new Error('Could not extract nonce');
|
||||
}
|
||||
|
||||
console.log(`[Watch32] Extracted nonce: ${nonce}`);
|
||||
|
||||
// Get sources from API
|
||||
const apiUrl = `${VIDEOSTR_URL}/embed-1/v3/e-1/getSources?id=${id}&_k=${nonce}`;
|
||||
console.log(`[Watch32] API URL: ${apiUrl}`);
|
||||
|
||||
return makeRequest(apiUrl, { headers })
|
||||
.then(response => response.json())
|
||||
.then(sourcesData => {
|
||||
console.log('[Watch32] Sources data:', JSON.stringify(sourcesData, null, 2));
|
||||
|
||||
if (!sourcesData.sources || sourcesData.sources.length === 0) {
|
||||
throw new Error('No sources found in response');
|
||||
}
|
||||
|
||||
// Get the first source file (matching Kotlin logic)
|
||||
const encoded = sourcesData.sources[0].file;
|
||||
console.log('[Watch32] Encoded source:', encoded);
|
||||
|
||||
// Check if sources is already an M3U8 URL
|
||||
if (encoded.includes('.m3u8')) {
|
||||
console.log('[Watch32] Source is already M3U8 URL');
|
||||
return encoded;
|
||||
}
|
||||
|
||||
console.log('[Watch32] Sources are encrypted, attempting to decrypt...');
|
||||
|
||||
// Get decryption key - use 'mega' key like Kotlin version
|
||||
return makeRequest('https://raw.githubusercontent.com/yogesh-hacker/MegacloudKeys/refs/heads/main/keys.json')
|
||||
.then(response => response.json())
|
||||
.then(keyData => {
|
||||
console.log('[Watch32] Key data:', JSON.stringify(keyData, null, 2));
|
||||
|
||||
const key = keyData.mega; // Use 'mega' key like Kotlin
|
||||
|
||||
if (!key) {
|
||||
throw new Error('Could not get decryption key (mega)');
|
||||
}
|
||||
|
||||
console.log('[Watch32] Using mega key for decryption');
|
||||
|
||||
// Decrypt using Google Apps Script - exact same logic as Kotlin
|
||||
const decodeUrl = 'https://script.google.com/macros/s/AKfycbxHbYHbrGMXYD2-bC-C43D3njIbU-wGiYQuJL61H4vyy6YVXkybMNNEPJNPPuZrD1gRVA/exec';
|
||||
const fullUrl = `${decodeUrl}?encrypted_data=${encodeURIComponent(encoded)}&nonce=${encodeURIComponent(nonce)}&secret=${encodeURIComponent(key)}`;
|
||||
|
||||
console.log('[Watch32] Decryption URL:', fullUrl);
|
||||
|
||||
return makeRequest(fullUrl)
|
||||
.then(response => response.text())
|
||||
.then(decryptedData => {
|
||||
console.log('[Watch32] Decrypted response:', decryptedData);
|
||||
|
||||
// Extract file URL from decrypted response - exact same regex as Kotlin
|
||||
const fileMatch = decryptedData.match(/"file":"(.*?)"/);
|
||||
if (fileMatch) {
|
||||
const m3u8Url = fileMatch[1];
|
||||
console.log('[Watch32] Extracted M3U8 URL:', m3u8Url);
|
||||
return m3u8Url;
|
||||
} else {
|
||||
throw new Error('Video URL not found in decrypted response');
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(finalM3u8Url => {
|
||||
console.log(`[Watch32] Final M3U8 URL: ${finalM3u8Url}`);
|
||||
|
||||
// Accept both megacdn and other reliable CDN links
|
||||
if (!finalM3u8Url.includes('megacdn.co') && !finalM3u8Url.includes('akmzed.cloud') && !finalM3u8Url.includes('sunnybreeze')) {
|
||||
console.log('[Watch32] Skipping unreliable CDN link');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parse master playlist to extract quality streams
|
||||
return parseM3U8Qualities(finalM3u8Url)
|
||||
.then(qualities => ({
|
||||
m3u8Url: finalM3u8Url,
|
||||
qualities,
|
||||
headers: {
|
||||
'Referer': 'https://videostr.net/',
|
||||
'Origin': 'https://videostr.net/',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
||||
}
|
||||
}));
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`[Watch32] Videostr extraction error: ${error.message}`);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
// Parse M3U8 master playlist to extract qualities
|
||||
function parseM3U8Qualities(masterUrl) {
|
||||
return makeRequest(masterUrl, {
|
||||
headers: {
|
||||
'Referer': 'https://videostr.net/',
|
||||
'Origin': 'https://videostr.net/'
|
||||
}
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(playlist => {
|
||||
const qualities = [];
|
||||
|
||||
// Parse M3U8 master playlist
|
||||
const lines = playlist.split('\n');
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (line.startsWith('#EXT-X-STREAM-INF:')) {
|
||||
const nextLine = lines[i + 1]?.trim();
|
||||
if (nextLine && !nextLine.startsWith('#')) {
|
||||
// Extract resolution and bandwidth
|
||||
const resolutionMatch = line.match(/RESOLUTION=(\d+x\d+)/);
|
||||
const bandwidthMatch = line.match(/BANDWIDTH=(\d+)/);
|
||||
|
||||
const resolution = resolutionMatch ? resolutionMatch[1] : 'Unknown';
|
||||
const bandwidth = bandwidthMatch ? parseInt(bandwidthMatch[1]) : 0;
|
||||
|
||||
// Determine quality label
|
||||
let quality = 'Unknown';
|
||||
if (resolution.includes('1920x1080')) quality = '1080p';
|
||||
else if (resolution.includes('1280x720')) quality = '720p';
|
||||
else if (resolution.includes('640x360')) quality = '360p';
|
||||
else if (resolution.includes('854x480')) quality = '480p';
|
||||
|
||||
qualities.push({
|
||||
quality,
|
||||
resolution,
|
||||
bandwidth,
|
||||
url: nextLine.startsWith('http') ? nextLine : new URL(nextLine, masterUrl).href
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by bandwidth (highest first)
|
||||
qualities.sort((a, b) => b.bandwidth - a.bandwidth);
|
||||
|
||||
return qualities;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`[Watch32] Error parsing M3U8 qualities: ${error.message}`);
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
// Main scraping function
|
||||
function getStreams(tmdbId, mediaType, season, episode) {
|
||||
console.log(`[Watch32] Searching for: ${tmdbId} (${mediaType})`);
|
||||
|
||||
// First, get movie/TV show details from TMDB
|
||||
const tmdbUrl = `https://api.themoviedb.org/3/${mediaType}/${tmdbId}?api_key=${TMDB_API_KEY}`;
|
||||
|
||||
return makeRequest(tmdbUrl)
|
||||
.then(response => response.json())
|
||||
.then(tmdbData => {
|
||||
const title = mediaType === 'tv' ? tmdbData.name : tmdbData.title;
|
||||
const year = mediaType === 'tv' ? tmdbData.first_air_date?.substring(0, 4) : tmdbData.release_date?.substring(0, 4);
|
||||
|
||||
if (!title) {
|
||||
throw new Error('Could not extract title from TMDB response');
|
||||
}
|
||||
|
||||
console.log(`[Watch32] TMDB Info: "${title}" (${year || 'N/A'})`);
|
||||
|
||||
// Build search query - use title instead of TMDB ID
|
||||
const query = year ? `${title} ${year}` : title;
|
||||
|
||||
return searchContent(query).then(searchResults => ({ searchResults, query, tmdbData }));
|
||||
})
|
||||
.then(({ searchResults, query, tmdbData }) => {
|
||||
if (searchResults.length === 0) {
|
||||
console.log('[Watch32] No search results found');
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log(`[Watch32] Found ${searchResults.length} results`);
|
||||
|
||||
// Try to find exact match first, then partial match
|
||||
let selectedResult = searchResults.find(result =>
|
||||
result.title.toLowerCase() === query.toLowerCase()
|
||||
);
|
||||
|
||||
if (!selectedResult) {
|
||||
// Look for best partial match (contains all words from query)
|
||||
const queryWords = query.toLowerCase().split(' ');
|
||||
selectedResult = searchResults.find(result => {
|
||||
const titleLower = result.title.toLowerCase();
|
||||
return queryWords.every(word => titleLower.includes(word));
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback to first result if no good match found
|
||||
if (!selectedResult) {
|
||||
selectedResult = searchResults[0];
|
||||
}
|
||||
|
||||
console.log(`[Watch32] Selected: ${selectedResult.title}`);
|
||||
|
||||
// Get content details
|
||||
return getContentDetails(selectedResult.url).then(contentDetails => ({ contentDetails, tmdbData }));
|
||||
})
|
||||
.then(({ contentDetails, tmdbData }) => {
|
||||
if (!contentDetails) {
|
||||
console.log('[Watch32] Could not get content details');
|
||||
return [];
|
||||
}
|
||||
|
||||
let itemsToProcess = [];
|
||||
|
||||
if (contentDetails.type === 'movie') {
|
||||
itemsToProcess.push({ data: contentDetails.data, episodeMeta: null });
|
||||
} else {
|
||||
// For TV series, filter by episode/season if specified
|
||||
let episodes = contentDetails.episodes;
|
||||
|
||||
if (season) {
|
||||
episodes = episodes.filter(ep => ep.season === season);
|
||||
}
|
||||
|
||||
if (episode) {
|
||||
episodes = episodes.filter(ep => ep.episode === episode);
|
||||
}
|
||||
|
||||
if (episodes.length === 0) {
|
||||
console.log('[Watch32] No matching episodes found');
|
||||
return [];
|
||||
}
|
||||
|
||||
// Process all matching episodes in parallel
|
||||
episodes.forEach(ep => {
|
||||
console.log(`[Watch32] Queue episode: S${ep.season}E${ep.episode} - ${ep.name}`);
|
||||
itemsToProcess.push({ data: ep.data, episodeMeta: ep });
|
||||
});
|
||||
}
|
||||
|
||||
// Process all data
|
||||
const allPromises = itemsToProcess.map(item => {
|
||||
return getServerLinks(item.data)
|
||||
.then(serverLinks => {
|
||||
console.log(`[Watch32] Found ${serverLinks.length} servers`);
|
||||
|
||||
// Process all server links
|
||||
const linkPromises = serverLinks.map(linkId => {
|
||||
return getSourceUrl(linkId)
|
||||
.then(sourceUrl => {
|
||||
if (!sourceUrl) return null;
|
||||
|
||||
console.log(`[Watch32] Source URL: ${sourceUrl}`);
|
||||
|
||||
// Check if it's a videostr URL
|
||||
if (sourceUrl.includes('videostr.net')) {
|
||||
return extractVideostrM3u8(sourceUrl);
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`[Watch32] Error processing link ${linkId}: ${error.message}`);
|
||||
return null;
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.all(linkPromises);
|
||||
})
|
||||
.then(results => ({ results, episodeMeta: item.episodeMeta }));
|
||||
});
|
||||
|
||||
return Promise.all(allPromises).then(resultsWithMeta => ({ resultsWithMeta, tmdbData, contentDetails }));
|
||||
})
|
||||
.then(({ resultsWithMeta, tmdbData, contentDetails }) => {
|
||||
// Flatten and filter results
|
||||
const allM3u8Links = [];
|
||||
for (const item of resultsWithMeta) {
|
||||
const serverResults = item.results;
|
||||
for (const result of serverResults) {
|
||||
if (result) {
|
||||
allM3u8Links.push({ link: result, episodeMeta: item.episodeMeta });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build title with year and episode info
|
||||
const title = mediaType === 'tv' ? tmdbData.name : tmdbData.title;
|
||||
const year = mediaType === 'tv' ? tmdbData.first_air_date?.substring(0, 4) : tmdbData.release_date?.substring(0, 4);
|
||||
|
||||
// Convert to Nuvio format
|
||||
const formattedLinks = [];
|
||||
|
||||
allM3u8Links.forEach(item => {
|
||||
const link = item.link;
|
||||
const episodeMeta = item.episodeMeta;
|
||||
let perItemTitle = `${title} (${year || 'N/A'})`;
|
||||
if (mediaType === 'tv' && episodeMeta) {
|
||||
perItemTitle += ` - S${episodeMeta.season}E${episodeMeta.episode}`;
|
||||
}
|
||||
if (link.qualities && link.qualities.length > 0) {
|
||||
link.qualities.forEach(quality => {
|
||||
formattedLinks.push({
|
||||
name: `Watch32 - ${quality.quality}`,
|
||||
title: perItemTitle,
|
||||
url: quality.url,
|
||||
quality: quality.quality,
|
||||
headers: link.headers || {},
|
||||
subtitles: []
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Skip unknown quality links
|
||||
console.log('[Watch32] Skipping unknown quality link');
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`[Watch32] Total found: ${formattedLinks.length} streams`);
|
||||
return formattedLinks;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`[Watch32] Scraping error: ${error.message}`);
|
||||
return [];
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`[Watch32] TMDB API error: ${error.message}`);
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
// Export the main function
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { getStreams };
|
||||
} else {
|
||||
// For React Native environment
|
||||
global.Watch32ScraperModule = { getStreams };
|
||||
}
|
||||
|
|
@ -1,800 +0,0 @@
|
|||
// Xprime Scraper for Nuvio Local Scrapers
|
||||
// React Native compatible version - Standalone (no external dependencies)
|
||||
|
||||
// TMDB API Configuration
|
||||
const TMDB_API_KEY = '439c478a771f35c05022f9feabcca01c';
|
||||
const TMDB_BASE_URL = 'https://api.themoviedb.org/3';
|
||||
|
||||
// Working headers for Cloudflare Workers URLs
|
||||
const WORKING_HEADERS = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Accept': 'video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
'Accept-Encoding': 'identity',
|
||||
'Origin': 'https://xprime.tv',
|
||||
'Referer': 'https://xprime.tv/',
|
||||
'Sec-Fetch-Dest': 'video',
|
||||
'Sec-Fetch-Mode': 'no-cors',
|
||||
'Sec-Fetch-Site': 'cross-site',
|
||||
'DNT': '1'
|
||||
};
|
||||
|
||||
// M3U8 Resolver Functions (inlined to remove external dependency)
|
||||
|
||||
// Parse M3U8 content and extract quality streams
|
||||
function parseM3U8(content, baseUrl) {
|
||||
const lines = content.split('\n').map(line => line.trim()).filter(line => line);
|
||||
const streams = [];
|
||||
|
||||
let currentStream = null;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
if (line.startsWith('#EXT-X-STREAM-INF:')) {
|
||||
// Parse stream info
|
||||
currentStream = {
|
||||
bandwidth: null,
|
||||
resolution: null,
|
||||
codecs: null,
|
||||
url: null
|
||||
};
|
||||
|
||||
// Extract bandwidth
|
||||
const bandwidthMatch = line.match(/BANDWIDTH=(\d+)/);
|
||||
if (bandwidthMatch) {
|
||||
currentStream.bandwidth = parseInt(bandwidthMatch[1]);
|
||||
}
|
||||
|
||||
// Extract resolution
|
||||
const resolutionMatch = line.match(/RESOLUTION=(\d+x\d+)/);
|
||||
if (resolutionMatch) {
|
||||
currentStream.resolution = resolutionMatch[1];
|
||||
}
|
||||
|
||||
// Extract codecs
|
||||
const codecsMatch = line.match(/CODECS="([^"]+)"/);
|
||||
if (codecsMatch) {
|
||||
currentStream.codecs = codecsMatch[1];
|
||||
}
|
||||
|
||||
} else if (currentStream && !line.startsWith('#')) {
|
||||
// This is the URL for the current stream
|
||||
currentStream.url = resolveUrl(line, baseUrl);
|
||||
streams.push(currentStream);
|
||||
currentStream = null;
|
||||
}
|
||||
}
|
||||
|
||||
return streams;
|
||||
}
|
||||
|
||||
// Resolve relative URLs against base URL
|
||||
function resolveUrl(url, baseUrl) {
|
||||
if (url.startsWith('http')) {
|
||||
return url;
|
||||
}
|
||||
|
||||
try {
|
||||
return new URL(url, baseUrl).toString();
|
||||
} catch (error) {
|
||||
console.log(`⚠️ Could not resolve URL: ${url} against ${baseUrl}`);
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine quality from resolution or bandwidth
|
||||
function getQualityFromStream(stream) {
|
||||
if (stream.resolution) {
|
||||
const [width, height] = stream.resolution.split('x').map(Number);
|
||||
|
||||
if (height >= 2160) return '4K';
|
||||
if (height >= 1440) return '1440p';
|
||||
if (height >= 1080) return '1080p';
|
||||
if (height >= 720) return '720p';
|
||||
if (height >= 480) return '480p';
|
||||
if (height >= 360) return '360p';
|
||||
return '240p';
|
||||
}
|
||||
|
||||
if (stream.bandwidth) {
|
||||
const mbps = stream.bandwidth / 1000000;
|
||||
|
||||
if (mbps >= 15) return '4K';
|
||||
if (mbps >= 8) return '1440p';
|
||||
if (mbps >= 5) return '1080p';
|
||||
if (mbps >= 3) return '720p';
|
||||
if (mbps >= 1.5) return '480p';
|
||||
if (mbps >= 0.8) return '360p';
|
||||
return '240p';
|
||||
}
|
||||
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
// Fetch and resolve M3U8 playlist
|
||||
function resolveM3U8(url, sourceName = 'Unknown') {
|
||||
console.log(`🔍 Resolving M3U8 playlist for ${sourceName}...`);
|
||||
console.log(`📡 URL: ${url.substring(0, 80)}...`);
|
||||
|
||||
return fetch(url, {
|
||||
method: 'GET',
|
||||
headers: WORKING_HEADERS,
|
||||
timeout: 15000
|
||||
}).then(function(response) {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.text().then(function(content) {
|
||||
console.log(`✅ Fetched M3U8 content (${content.length} bytes)`);
|
||||
|
||||
// Check if it's a master playlist (contains #EXT-X-STREAM-INF)
|
||||
if (content.includes('#EXT-X-STREAM-INF:')) {
|
||||
console.log(`📋 Master playlist detected - parsing quality streams...`);
|
||||
|
||||
const streams = parseM3U8(content, url);
|
||||
console.log(`🎬 Found ${streams.length} quality streams`);
|
||||
|
||||
const resolvedStreams = [];
|
||||
|
||||
for (const stream of streams) {
|
||||
const quality = getQualityFromStream(stream);
|
||||
|
||||
// Extract clean server name from sourceName
|
||||
const cleanServerName = sourceName.replace(/^XPRIME\s+/i, '').replace(/\s+-\s+.*$/, '');
|
||||
const formattedName = `XPRIME ${cleanServerName.charAt(0).toUpperCase() + cleanServerName.slice(1)} - ${quality}`;
|
||||
|
||||
resolvedStreams.push({
|
||||
source: sourceName,
|
||||
name: formattedName,
|
||||
url: stream.url,
|
||||
quality: quality,
|
||||
resolution: stream.resolution,
|
||||
bandwidth: stream.bandwidth,
|
||||
codecs: stream.codecs,
|
||||
type: 'M3U8',
|
||||
headers: WORKING_HEADERS,
|
||||
referer: 'https://xprime.tv'
|
||||
});
|
||||
|
||||
console.log(` 📊 ${quality} (${stream.resolution || 'Unknown resolution'}) - ${Math.round((stream.bandwidth || 0) / 1000000 * 10) / 10} Mbps`);
|
||||
}
|
||||
|
||||
// Sort by quality (highest first)
|
||||
resolvedStreams.sort((a, b) => {
|
||||
const qualityOrder = { '4K': 4, '1440p': 3, '1080p': 2, '720p': 1, '480p': 0, '360p': -1, '240p': -2, 'Unknown': -3 };
|
||||
return (qualityOrder[b.quality] || -3) - (qualityOrder[a.quality] || -3);
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
type: 'master',
|
||||
streams: resolvedStreams,
|
||||
originalUrl: url
|
||||
};
|
||||
|
||||
} else if (content.includes('#EXTINF:')) {
|
||||
console.log(`📺 Media playlist detected - single quality stream`);
|
||||
|
||||
// Extract clean server name from sourceName
|
||||
const cleanServerName = sourceName.replace(/^XPRIME\s+/i, '').replace(/\s+-\s+.*$/, '');
|
||||
const formattedName = `XPRIME ${cleanServerName.charAt(0).toUpperCase() + cleanServerName.slice(1)} - Unknown`;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
type: 'media',
|
||||
streams: [{
|
||||
source: sourceName,
|
||||
name: formattedName,
|
||||
url: url,
|
||||
quality: 'Unknown',
|
||||
type: 'M3U8',
|
||||
headers: WORKING_HEADERS,
|
||||
referer: 'https://xprime.tv'
|
||||
}],
|
||||
originalUrl: url
|
||||
};
|
||||
|
||||
} else {
|
||||
throw new Error('Invalid M3U8 content - no playlist markers found');
|
||||
}
|
||||
});
|
||||
}).catch(function(error) {
|
||||
console.log(`❌ Failed to resolve M3U8: ${error.message}`);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
streams: [],
|
||||
originalUrl: url
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Resolve multiple M3U8 URLs
|
||||
function resolveMultipleM3U8(links) {
|
||||
console.log(`🚀 Resolving ${links.length} M3U8 playlists in parallel...`);
|
||||
|
||||
const resolvePromises = links.map(function(link) {
|
||||
return resolveM3U8(link.url, link.name).then(function(result) {
|
||||
return {
|
||||
originalLink: link,
|
||||
resolution: result
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.allSettled(resolvePromises).then(function(results) {
|
||||
const allResolvedStreams = [];
|
||||
const failedResolutions = [];
|
||||
|
||||
for (const result of results) {
|
||||
if (result.status === 'fulfilled') {
|
||||
const { originalLink, resolution } = result.value;
|
||||
|
||||
if (resolution.success) {
|
||||
allResolvedStreams.push(...resolution.streams);
|
||||
} else {
|
||||
failedResolutions.push({
|
||||
link: originalLink,
|
||||
error: resolution.error
|
||||
});
|
||||
}
|
||||
} else {
|
||||
failedResolutions.push({
|
||||
link: 'Unknown',
|
||||
error: result.reason.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n📊 Resolution Summary:`);
|
||||
console.log(`✅ Successfully resolved: ${allResolvedStreams.length} streams`);
|
||||
console.log(`❌ Failed resolutions: ${failedResolutions.length}`);
|
||||
|
||||
if (failedResolutions.length > 0) {
|
||||
console.log(`\n❌ Failed resolutions:`);
|
||||
failedResolutions.forEach((failure, index) => {
|
||||
console.log(` ${index + 1}. ${failure.link.name || 'Unknown'}: ${failure.error}`);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: allResolvedStreams.length > 0,
|
||||
streams: allResolvedStreams,
|
||||
failed: failedResolutions,
|
||||
summary: {
|
||||
total: links.length,
|
||||
resolved: allResolvedStreams.length,
|
||||
failed: failedResolutions.length
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Constants
|
||||
const FALLBACK_DOMAIN = 'https://xprime.tv';
|
||||
const DOMAIN_CACHE_TTL = 4 * 60 * 60 * 1000; // 4 hours
|
||||
|
||||
// Global variables for domain caching
|
||||
let xprimeDomain = FALLBACK_DOMAIN;
|
||||
let domainCacheTimestamp = 0;
|
||||
|
||||
// Utility Functions
|
||||
function getQualityFromName(qualityStr) {
|
||||
if (!qualityStr) return 'Unknown';
|
||||
|
||||
const quality = qualityStr.toLowerCase();
|
||||
const qualityMap = {
|
||||
'2160p': '4K', '4k': '4K',
|
||||
'1440p': '1440p', '2k': '1440p',
|
||||
'1080p': '1080p', 'fhd': '1080p', 'full hd': '1080p',
|
||||
'720p': '720p', 'hd': '720p',
|
||||
'480p': '480p', 'sd': '480p',
|
||||
'360p': '360p',
|
||||
'240p': '240p'
|
||||
};
|
||||
|
||||
for (const [key, value] of Object.entries(qualityMap)) {
|
||||
if (quality.includes(key)) return value;
|
||||
}
|
||||
|
||||
// Try to extract number from string and format consistently
|
||||
const match = qualityStr.match(/(\d{3,4})[pP]?/);
|
||||
if (match) {
|
||||
const resolution = parseInt(match[1]);
|
||||
if (resolution >= 2160) return '4K';
|
||||
if (resolution >= 1440) return '1440p';
|
||||
if (resolution >= 1080) return '1080p';
|
||||
if (resolution >= 720) return '720p';
|
||||
if (resolution >= 480) return '480p';
|
||||
if (resolution >= 360) return '360p';
|
||||
return '240p';
|
||||
}
|
||||
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
// Fetch latest domain from GitHub
|
||||
function getXprimeDomain() {
|
||||
const now = Date.now();
|
||||
if (now - domainCacheTimestamp < DOMAIN_CACHE_TTL) {
|
||||
return Promise.resolve(xprimeDomain);
|
||||
}
|
||||
|
||||
console.log('[Xprime] Fetching latest domain...');
|
||||
return fetch('https://raw.githubusercontent.com/phisher98/TVVVV/refs/heads/main/domains.json', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||
}
|
||||
}).then(function(response) {
|
||||
if (response.ok) {
|
||||
return response.json().then(function(data) {
|
||||
if (data && data.xprime) {
|
||||
xprimeDomain = data.xprime;
|
||||
domainCacheTimestamp = now;
|
||||
console.log(`[Xprime] Updated domain to: ${xprimeDomain}`);
|
||||
}
|
||||
return xprimeDomain;
|
||||
});
|
||||
}
|
||||
return xprimeDomain;
|
||||
}).catch(function(error) {
|
||||
console.error(`[Xprime] Failed to fetch latest domain: ${error.message}`);
|
||||
return xprimeDomain;
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to make HTTP requests
|
||||
function makeRequest(url, options = {}) {
|
||||
const defaultHeaders = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
'Accept-Encoding': 'gzip, deflate, br'
|
||||
};
|
||||
|
||||
return fetch(url, {
|
||||
method: options.method || 'GET',
|
||||
headers: { ...defaultHeaders, ...options.headers },
|
||||
...options
|
||||
}).then(function(response) {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
return response;
|
||||
}).catch(function(error) {
|
||||
console.error(`[Xprime] Request failed for ${url}: ${error.message}`);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
// Hardcoded Server List
|
||||
function getXprimeServers(api) {
|
||||
console.log('[Xprime] Using hardcoded servers...');
|
||||
const hardcodedServers = [
|
||||
{ name: 'primebox', status: 'ok' },
|
||||
{ name: 'rage', status: 'ok' },
|
||||
// Temporarily disabled Phoenix server
|
||||
// { name: 'phoenix', status: 'ok' },
|
||||
// Temporarily disabled Fox server
|
||||
// { name: 'fox', status: 'ok' }
|
||||
];
|
||||
console.log(`[Xprime] Using ${hardcodedServers.length} hardcoded servers: ${hardcodedServers.map(s => s.name).join(', ')}`);
|
||||
return Promise.resolve(hardcodedServers);
|
||||
}
|
||||
|
||||
// Build Query Parameters
|
||||
function buildQueryParams(serverName, title, year, id, season, episode) {
|
||||
const params = new URLSearchParams();
|
||||
params.append('name', title || '');
|
||||
|
||||
if (serverName === 'primebox') {
|
||||
if (year) params.append('fallback_year', year.toString());
|
||||
if (season && episode) {
|
||||
params.append('season', season.toString());
|
||||
params.append('episode', episode.toString());
|
||||
}
|
||||
} else {
|
||||
if (year) params.append('year', year.toString());
|
||||
if (id) {
|
||||
params.append('id', id);
|
||||
params.append('imdb', id);
|
||||
}
|
||||
if (season && episode) {
|
||||
params.append('season', season.toString());
|
||||
params.append('episode', episode.toString());
|
||||
}
|
||||
}
|
||||
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
// Process PrimeBox Response
|
||||
function processPrimeBoxResponse(data, serverLabel, serverName) {
|
||||
const links = [];
|
||||
const subtitles = [];
|
||||
|
||||
try {
|
||||
if (data.streams) {
|
||||
// Process quality streams - fix: use available_qualities instead of qualities
|
||||
if (data.available_qualities && Array.isArray(data.available_qualities)) {
|
||||
data.available_qualities.forEach(quality => {
|
||||
const url = data.streams[quality];
|
||||
if (url) {
|
||||
const normalizedQuality = getQualityFromName(quality);
|
||||
links.push({
|
||||
source: serverLabel,
|
||||
name: `XPRIME ${serverName.charAt(0).toUpperCase() + serverName.slice(1)} - ${normalizedQuality}`,
|
||||
url: url.trim(), // Remove any whitespace
|
||||
quality: normalizedQuality,
|
||||
type: 'VIDEO',
|
||||
headers: WORKING_HEADERS,
|
||||
referer: 'https://xprime.tv'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Process subtitles
|
||||
if (data.has_subtitles && data.subtitles && Array.isArray(data.subtitles)) {
|
||||
data.subtitles.forEach(sub => {
|
||||
if (sub.file) {
|
||||
subtitles.push({
|
||||
language: sub.label || 'Unknown',
|
||||
url: sub.file.trim() // Remove any whitespace
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[Xprime] Error parsing PrimeBox response: ${error.message}`);
|
||||
}
|
||||
|
||||
return { links, subtitles };
|
||||
}
|
||||
|
||||
// Process Other Server Response
|
||||
function processOtherServerResponse(data, serverLabel, serverName) {
|
||||
const links = [];
|
||||
|
||||
try {
|
||||
// Special handling for Rage server response
|
||||
if (serverName === 'rage' && data && data.success && Array.isArray(data.qualities)) {
|
||||
data.qualities.forEach(function(q) {
|
||||
if (q && q.url) {
|
||||
const normalizedQuality = getQualityFromName(q.quality);
|
||||
// Normalize size to a human-readable string
|
||||
let sizeStr = 'Unknown';
|
||||
if (typeof q.size === 'number' && isFinite(q.size)) {
|
||||
const gb = q.size / (1024 * 1024 * 1024);
|
||||
const mb = q.size / (1024 * 1024);
|
||||
sizeStr = gb >= 1 ? `${gb.toFixed(2)} GB` : `${mb.toFixed(0)} MB`;
|
||||
} else if (typeof q.size === 'string' && q.size.trim()) {
|
||||
sizeStr = q.size.trim();
|
||||
}
|
||||
links.push({
|
||||
source: serverLabel,
|
||||
name: `XPRIME ${serverName.charAt(0).toUpperCase() + serverName.slice(1)} - ${normalizedQuality}`,
|
||||
url: q.url,
|
||||
quality: normalizedQuality,
|
||||
size: sizeStr,
|
||||
type: 'VIDEO',
|
||||
headers: WORKING_HEADERS,
|
||||
referer: 'https://xprime.tv'
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (data.url) {
|
||||
// Try to extract quality from the URL or response data
|
||||
let quality = 'Unknown';
|
||||
|
||||
// Check if there's quality information in the response
|
||||
if (data.quality) {
|
||||
quality = getQualityFromName(data.quality);
|
||||
} else {
|
||||
// Try to extract quality from URL patterns
|
||||
const urlQualityMatch = data.url.match(/(\d{3,4})p/i);
|
||||
if (urlQualityMatch) {
|
||||
quality = getQualityFromName(urlQualityMatch[1] + 'p');
|
||||
}
|
||||
}
|
||||
|
||||
links.push({
|
||||
source: serverLabel,
|
||||
name: `XPRIME ${serverName.charAt(0).toUpperCase() + serverName.slice(1)} - ${quality}`,
|
||||
url: data.url,
|
||||
quality: quality,
|
||||
type: 'M3U8',
|
||||
headers: WORKING_HEADERS,
|
||||
referer: 'https://xprime.tv'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[Xprime] Error parsing server response: ${error.message}`);
|
||||
}
|
||||
|
||||
return { links, subtitles: [] };
|
||||
}
|
||||
|
||||
// Group streams by quality for better organization
|
||||
function groupStreamsByQuality(streams, subtitles, mediaInfo = {}) {
|
||||
// Create media title with details
|
||||
let mediaTitle = '';
|
||||
if (mediaInfo.title) {
|
||||
if (mediaInfo.mediaType === 'tv' && mediaInfo.season && mediaInfo.episode) {
|
||||
mediaTitle = `${mediaInfo.title} S${String(mediaInfo.season).padStart(2, '0')}E${String(mediaInfo.episode).padStart(2, '0')}`;
|
||||
} else if (mediaInfo.year) {
|
||||
mediaTitle = `${mediaInfo.title} (${mediaInfo.year})`;
|
||||
} else {
|
||||
mediaTitle = mediaInfo.title;
|
||||
}
|
||||
}
|
||||
|
||||
// Group streams by quality
|
||||
const qualityGroups = {};
|
||||
|
||||
streams.forEach(stream => {
|
||||
const quality = stream.quality || 'Unknown';
|
||||
if (!qualityGroups[quality]) {
|
||||
qualityGroups[quality] = [];
|
||||
}
|
||||
|
||||
qualityGroups[quality].push({
|
||||
name: stream.name,
|
||||
title: mediaTitle || '',
|
||||
url: stream.url,
|
||||
quality: quality,
|
||||
size: stream.size || 'Unknown',
|
||||
headers: stream.headers || WORKING_HEADERS,
|
||||
subtitles: subtitles
|
||||
});
|
||||
});
|
||||
|
||||
// Define quality order (highest to lowest)
|
||||
const qualityOrder = ['4K', '1440p', '1080p', '720p', '480p', '360p', '240p', 'Unknown'];
|
||||
|
||||
// Sort and flatten the grouped streams
|
||||
const sortedStreams = [];
|
||||
qualityOrder.forEach(quality => {
|
||||
if (qualityGroups[quality]) {
|
||||
// Sort streams within the same quality by server name
|
||||
qualityGroups[quality].sort((a, b) => a.name.localeCompare(b.name));
|
||||
sortedStreams.push(...qualityGroups[quality]);
|
||||
}
|
||||
});
|
||||
|
||||
// Add any qualities not in the predefined order
|
||||
Object.keys(qualityGroups).forEach(quality => {
|
||||
if (!qualityOrder.includes(quality)) {
|
||||
qualityGroups[quality].sort((a, b) => a.name.localeCompare(b.name));
|
||||
sortedStreams.push(...qualityGroups[quality]);
|
||||
}
|
||||
});
|
||||
|
||||
return sortedStreams;
|
||||
}
|
||||
|
||||
// Get movie/TV show details from TMDB
|
||||
function getTMDBDetails(tmdbId, mediaType) {
|
||||
const endpoint = mediaType === 'tv' ? 'tv' : 'movie';
|
||||
const url = `${TMDB_BASE_URL}/${endpoint}/${tmdbId}?api_key=${TMDB_API_KEY}&append_to_response=external_ids`;
|
||||
|
||||
return makeRequest(url)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`TMDB API error: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
const title = mediaType === 'tv' ? data.name : data.title;
|
||||
const releaseDate = mediaType === 'tv' ? data.first_air_date : data.release_date;
|
||||
const year = releaseDate ? parseInt(releaseDate.split('-')[0]) : null;
|
||||
|
||||
return {
|
||||
title: title,
|
||||
year: year,
|
||||
imdbId: data.external_ids?.imdb_id || null
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Main scraping function - Updated to match Nuvio interface
|
||||
function getStreams(tmdbId, mediaType = 'movie', season = null, episode = null) {
|
||||
console.log(`[Xprime] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}${mediaType === 'tv' ? `, S:${season}E:${episode}` : ''}`);
|
||||
|
||||
// First, get movie/TV show details from TMDB
|
||||
return getTMDBDetails(tmdbId, mediaType)
|
||||
.then(mediaInfo => {
|
||||
if (!mediaInfo.title) {
|
||||
throw new Error('Could not extract title from TMDB response');
|
||||
}
|
||||
|
||||
console.log(`[Xprime] TMDB Info: "${mediaInfo.title}" (${mediaInfo.year || 'N/A'})`);
|
||||
console.log(`[Xprime] Searching for: ${mediaInfo.title} (${mediaInfo.year})`);
|
||||
|
||||
const { title, year, imdbId } = mediaInfo;
|
||||
const type = mediaType; // Keep the original mediaType
|
||||
|
||||
return getXprimeDomain().then(function(api) {
|
||||
return getXprimeServers(api).then(function(servers) {
|
||||
if (servers.length === 0) {
|
||||
console.log('[Xprime] No active servers found');
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log(`[Xprime] Processing ${servers.length} servers in parallel`);
|
||||
|
||||
const allLinks = [];
|
||||
const allSubtitles = [];
|
||||
|
||||
// Process servers in parallel for better performance
|
||||
const serverPromises = servers.map(function(server) {
|
||||
console.log(`[Xprime] Processing server: ${server.name}`);
|
||||
|
||||
// Rage server requires a different endpoint (backend.xprime.tv) and TMDB id param
|
||||
let serverUrl;
|
||||
if (server.name === 'rage') {
|
||||
if (type === 'tv' && season && episode) {
|
||||
serverUrl = `https://backend.xprime.tv/rage?id=${encodeURIComponent(tmdbId)}&season=${encodeURIComponent(season)}&episode=${encodeURIComponent(episode)}`;
|
||||
} else {
|
||||
serverUrl = `https://backend.xprime.tv/rage?id=${encodeURIComponent(tmdbId)}`;
|
||||
}
|
||||
} else {
|
||||
const queryParams = buildQueryParams(server.name, title, year, imdbId, season, episode);
|
||||
serverUrl = `${api}/${server.name}?${queryParams}`;
|
||||
}
|
||||
|
||||
console.log(`[Xprime] Request URL: ${serverUrl}`);
|
||||
|
||||
return makeRequest(serverUrl, {
|
||||
headers: {
|
||||
'Origin': server.name === 'rage' ? 'https://xprime.tv' : api,
|
||||
'Referer': server.name === 'rage' ? 'https://xprime.tv/' : api
|
||||
}
|
||||
}).then(function(response) {
|
||||
return response.json().then(function(data) {
|
||||
const serverLabel = `Xprime ${server.name.charAt(0).toUpperCase() + server.name.slice(1)}`;
|
||||
let result;
|
||||
|
||||
if (server.name === 'primebox') {
|
||||
result = processPrimeBoxResponse(data, serverLabel, server.name);
|
||||
} else {
|
||||
result = processOtherServerResponse(data, serverLabel, server.name);
|
||||
}
|
||||
|
||||
console.log(`[Xprime] Server ${server.name}: Found ${result.links.length} links, ${result.subtitles.length} subtitles`);
|
||||
return result;
|
||||
});
|
||||
}).catch(function(error) {
|
||||
console.error(`[Xprime] Error on server ${server.name}: ${error.message}`);
|
||||
return { links: [], subtitles: [] };
|
||||
});
|
||||
});
|
||||
|
||||
// Wait for all server requests to complete
|
||||
return Promise.allSettled(serverPromises).then(function(results) {
|
||||
// Process results
|
||||
for (const result of results) {
|
||||
if (result.status === 'fulfilled') {
|
||||
const { links, subtitles } = result.value;
|
||||
allLinks.push(...links);
|
||||
allSubtitles.push(...subtitles);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[Xprime] Total found: ${allLinks.length} links, ${allSubtitles.length} subtitles`);
|
||||
|
||||
// Separate M3U8 links from direct video links
|
||||
const m3u8Links = allLinks.filter(link => link.type === 'M3U8');
|
||||
const directLinks = allLinks.filter(link => link.type !== 'M3U8');
|
||||
|
||||
let resolvedStreams = [];
|
||||
|
||||
// Resolve M3U8 playlists to extract individual quality streams
|
||||
if (m3u8Links.length > 0) {
|
||||
console.log(`[Xprime] Resolving ${m3u8Links.length} M3U8 playlists...`);
|
||||
|
||||
return resolveMultipleM3U8(m3u8Links).then(function(resolutionResult) {
|
||||
if (resolutionResult.success && resolutionResult.streams.length > 0) {
|
||||
console.log(`[Xprime] Successfully resolved ${resolutionResult.streams.length} quality streams`);
|
||||
resolvedStreams = resolutionResult.streams;
|
||||
} else {
|
||||
console.log(`[Xprime] M3U8 resolution failed, using master playlist URLs`);
|
||||
resolvedStreams = m3u8Links;
|
||||
}
|
||||
|
||||
// Combine resolved streams with direct links
|
||||
const finalLinks = [...directLinks, ...resolvedStreams];
|
||||
|
||||
console.log(`[Xprime] Final result: ${finalLinks.length} total streams (${resolvedStreams.length} from M3U8, ${directLinks.length} direct)`);
|
||||
|
||||
// Group streams by quality and format for Nuvio
|
||||
const mediaInfoForGrouping = {
|
||||
title: title,
|
||||
year: year,
|
||||
mediaType: mediaType,
|
||||
season: season,
|
||||
episode: episode
|
||||
};
|
||||
const formattedLinks = groupStreamsByQuality(finalLinks, allSubtitles, mediaInfoForGrouping);
|
||||
|
||||
// Add provider identifier for header detection
|
||||
formattedLinks.forEach(link => {
|
||||
link.provider = 'xprime';
|
||||
});
|
||||
|
||||
return formattedLinks;
|
||||
}).catch(function(error) {
|
||||
console.error(`[Xprime] M3U8 resolution error: ${error.message}`);
|
||||
resolvedStreams = m3u8Links;
|
||||
|
||||
// Combine resolved streams with direct links
|
||||
const finalLinks = [...directLinks, ...resolvedStreams];
|
||||
|
||||
console.log(`[Xprime] Final result: ${finalLinks.length} total streams (${resolvedStreams.length} from M3U8, ${directLinks.length} direct)`);
|
||||
|
||||
// Group streams by quality and format for Nuvio
|
||||
const mediaInfoForGrouping = {
|
||||
title: title,
|
||||
year: year,
|
||||
mediaType: mediaType,
|
||||
season: season,
|
||||
episode: episode
|
||||
};
|
||||
const formattedLinks = groupStreamsByQuality(finalLinks, allSubtitles, mediaInfoForGrouping);
|
||||
|
||||
// Add provider identifier for header detection
|
||||
formattedLinks.forEach(link => {
|
||||
link.provider = 'xprime';
|
||||
});
|
||||
|
||||
return formattedLinks;
|
||||
});
|
||||
} else {
|
||||
// No M3U8 links, just return direct links
|
||||
const finalLinks = [...directLinks, ...resolvedStreams];
|
||||
|
||||
console.log(`[Xprime] Final result: ${finalLinks.length} total streams (${resolvedStreams.length} from M3U8, ${directLinks.length} direct)`);
|
||||
|
||||
// Group streams by quality and format for Nuvio
|
||||
const mediaInfoForGrouping = {
|
||||
title: title,
|
||||
year: year,
|
||||
mediaType: mediaType,
|
||||
season: season,
|
||||
episode: episode
|
||||
};
|
||||
const formattedLinks = groupStreamsByQuality(finalLinks, allSubtitles, mediaInfoForGrouping);
|
||||
|
||||
// Add provider identifier for header detection
|
||||
formattedLinks.forEach(link => {
|
||||
link.provider = 'xprime';
|
||||
});
|
||||
|
||||
return formattedLinks;
|
||||
}
|
||||
});
|
||||
});
|
||||
}).catch(function(error) {
|
||||
console.error(`[Xprime] Scraping error: ${error.message}`);
|
||||
return [];
|
||||
});
|
||||
})
|
||||
.catch(function(error) {
|
||||
console.error(`[Xprime] TMDB or scraping error: ${error.message}`);
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
// Export the main function
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { getStreams };
|
||||
} else {
|
||||
// For React Native environment
|
||||
global.XprimeScraperModule = { getStreams };
|
||||
}
|
||||
Loading…
Reference in a new issue