mirror of
https://gitlab.com/vyla-entertainment/sdk.git
synced 2026-11-09 19:57:23 -05:00
sdk initial commit
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,3 +1,3 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
.env
|
.env
|
||||||
package-lock.json
|
package-lock.json
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# npmignore for vyla-sdk in js
|
# npmignore for vyla-sdk in js
|
||||||
.env
|
.env
|
||||||
test/
|
test/
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|||||||
812
LICENSE
812
LICENSE
@@ -1,407 +1,407 @@
|
|||||||
Attribution-NonCommercial 4.0 International
|
Attribution-NonCommercial 4.0 International
|
||||||
|
|
||||||
=======================================================================
|
=======================================================================
|
||||||
|
|
||||||
Creative Commons Corporation ("Creative Commons") is not a law firm and
|
Creative Commons Corporation ("Creative Commons") is not a law firm and
|
||||||
does not provide legal services or legal advice. Distribution of
|
does not provide legal services or legal advice. Distribution of
|
||||||
Creative Commons public licenses does not create a lawyer-client or
|
Creative Commons public licenses does not create a lawyer-client or
|
||||||
other relationship. Creative Commons makes its licenses and related
|
other relationship. Creative Commons makes its licenses and related
|
||||||
information available on an "as-is" basis. Creative Commons gives no
|
information available on an "as-is" basis. Creative Commons gives no
|
||||||
warranties regarding its licenses, any material licensed under their
|
warranties regarding its licenses, any material licensed under their
|
||||||
terms and conditions, or any related information. Creative Commons
|
terms and conditions, or any related information. Creative Commons
|
||||||
disclaims all liability for damages resulting from their use to the
|
disclaims all liability for damages resulting from their use to the
|
||||||
fullest extent possible.
|
fullest extent possible.
|
||||||
|
|
||||||
Using Creative Commons Public Licenses
|
Using Creative Commons Public Licenses
|
||||||
|
|
||||||
Creative Commons public licenses provide a standard set of terms and
|
Creative Commons public licenses provide a standard set of terms and
|
||||||
conditions that creators and other rights holders may use to share
|
conditions that creators and other rights holders may use to share
|
||||||
original works of authorship and other material subject to copyright
|
original works of authorship and other material subject to copyright
|
||||||
and certain other rights specified in the public license below. The
|
and certain other rights specified in the public license below. The
|
||||||
following considerations are for informational purposes only, are not
|
following considerations are for informational purposes only, are not
|
||||||
exhaustive, and do not form part of our licenses.
|
exhaustive, and do not form part of our licenses.
|
||||||
|
|
||||||
Considerations for licensors: Our public licenses are
|
Considerations for licensors: Our public licenses are
|
||||||
intended for use by those authorized to give the public
|
intended for use by those authorized to give the public
|
||||||
permission to use material in ways otherwise restricted by
|
permission to use material in ways otherwise restricted by
|
||||||
copyright and certain other rights. Our licenses are
|
copyright and certain other rights. Our licenses are
|
||||||
irrevocable. Licensors should read and understand the terms
|
irrevocable. Licensors should read and understand the terms
|
||||||
and conditions of the license they choose before applying it.
|
and conditions of the license they choose before applying it.
|
||||||
Licensors should also secure all rights necessary before
|
Licensors should also secure all rights necessary before
|
||||||
applying our licenses so that the public can reuse the
|
applying our licenses so that the public can reuse the
|
||||||
material as expected. Licensors should clearly mark any
|
material as expected. Licensors should clearly mark any
|
||||||
material not subject to the license. This includes other CC-
|
material not subject to the license. This includes other CC-
|
||||||
licensed material, or material used under an exception or
|
licensed material, or material used under an exception or
|
||||||
limitation to copyright. More considerations for licensors:
|
limitation to copyright. More considerations for licensors:
|
||||||
wiki.creativecommons.org/Considerations_for_licensors
|
wiki.creativecommons.org/Considerations_for_licensors
|
||||||
|
|
||||||
Considerations for the public: By using one of our public
|
Considerations for the public: By using one of our public
|
||||||
licenses, a licensor grants the public permission to use the
|
licenses, a licensor grants the public permission to use the
|
||||||
licensed material under specified terms and conditions. If
|
licensed material under specified terms and conditions. If
|
||||||
the licensor's permission is not necessary for any reason--for
|
the licensor's permission is not necessary for any reason--for
|
||||||
example, because of any applicable exception or limitation to
|
example, because of any applicable exception or limitation to
|
||||||
copyright--then that use is not regulated by the license. Our
|
copyright--then that use is not regulated by the license. Our
|
||||||
licenses grant only permissions under copyright and certain
|
licenses grant only permissions under copyright and certain
|
||||||
other rights that a licensor has authority to grant. Use of
|
other rights that a licensor has authority to grant. Use of
|
||||||
the licensed material may still be restricted for other
|
the licensed material may still be restricted for other
|
||||||
reasons, including because others have copyright or other
|
reasons, including because others have copyright or other
|
||||||
rights in the material. A licensor may make special requests,
|
rights in the material. A licensor may make special requests,
|
||||||
such as asking that all changes be marked or described.
|
such as asking that all changes be marked or described.
|
||||||
Although not required by our licenses, you are encouraged to
|
Although not required by our licenses, you are encouraged to
|
||||||
respect those requests where reasonable. More considerations
|
respect those requests where reasonable. More considerations
|
||||||
for the public:
|
for the public:
|
||||||
wiki.creativecommons.org/Considerations_for_licensees
|
wiki.creativecommons.org/Considerations_for_licensees
|
||||||
|
|
||||||
=======================================================================
|
=======================================================================
|
||||||
|
|
||||||
Creative Commons Attribution-NonCommercial 4.0 International Public
|
Creative Commons Attribution-NonCommercial 4.0 International Public
|
||||||
License
|
License
|
||||||
|
|
||||||
By exercising the Licensed Rights (defined below), You accept and agree
|
By exercising the Licensed Rights (defined below), You accept and agree
|
||||||
to be bound by the terms and conditions of this Creative Commons
|
to be bound by the terms and conditions of this Creative Commons
|
||||||
Attribution-NonCommercial 4.0 International Public License ("Public
|
Attribution-NonCommercial 4.0 International Public License ("Public
|
||||||
License"). To the extent this Public License may be interpreted as a
|
License"). To the extent this Public License may be interpreted as a
|
||||||
contract, You are granted the Licensed Rights in consideration of Your
|
contract, You are granted the Licensed Rights in consideration of Your
|
||||||
acceptance of these terms and conditions, and the Licensor grants You
|
acceptance of these terms and conditions, and the Licensor grants You
|
||||||
such rights in consideration of benefits the Licensor receives from
|
such rights in consideration of benefits the Licensor receives from
|
||||||
making the Licensed Material available under these terms and
|
making the Licensed Material available under these terms and
|
||||||
conditions.
|
conditions.
|
||||||
|
|
||||||
|
|
||||||
Section 1 -- Definitions.
|
Section 1 -- Definitions.
|
||||||
|
|
||||||
a. Adapted Material means material subject to Copyright and Similar
|
a. Adapted Material means material subject to Copyright and Similar
|
||||||
Rights that is derived from or based upon the Licensed Material
|
Rights that is derived from or based upon the Licensed Material
|
||||||
and in which the Licensed Material is translated, altered,
|
and in which the Licensed Material is translated, altered,
|
||||||
arranged, transformed, or otherwise modified in a manner requiring
|
arranged, transformed, or otherwise modified in a manner requiring
|
||||||
permission under the Copyright and Similar Rights held by the
|
permission under the Copyright and Similar Rights held by the
|
||||||
Licensor. For purposes of this Public License, where the Licensed
|
Licensor. For purposes of this Public License, where the Licensed
|
||||||
Material is a musical work, performance, or sound recording,
|
Material is a musical work, performance, or sound recording,
|
||||||
Adapted Material is always produced where the Licensed Material is
|
Adapted Material is always produced where the Licensed Material is
|
||||||
synched in timed relation with a moving image.
|
synched in timed relation with a moving image.
|
||||||
|
|
||||||
b. Adapter's License means the license You apply to Your Copyright
|
b. Adapter's License means the license You apply to Your Copyright
|
||||||
and Similar Rights in Your contributions to Adapted Material in
|
and Similar Rights in Your contributions to Adapted Material in
|
||||||
accordance with the terms and conditions of this Public License.
|
accordance with the terms and conditions of this Public License.
|
||||||
|
|
||||||
c. Copyright and Similar Rights means copyright and/or similar rights
|
c. Copyright and Similar Rights means copyright and/or similar rights
|
||||||
closely related to copyright including, without limitation,
|
closely related to copyright including, without limitation,
|
||||||
performance, broadcast, sound recording, and Sui Generis Database
|
performance, broadcast, sound recording, and Sui Generis Database
|
||||||
Rights, without regard to how the rights are labeled or
|
Rights, without regard to how the rights are labeled or
|
||||||
categorized. For purposes of this Public License, the rights
|
categorized. For purposes of this Public License, the rights
|
||||||
specified in Section 2(b)(1)-(2) are not Copyright and Similar
|
specified in Section 2(b)(1)-(2) are not Copyright and Similar
|
||||||
Rights.
|
Rights.
|
||||||
d. Effective Technological Measures means those measures that, in the
|
d. Effective Technological Measures means those measures that, in the
|
||||||
absence of proper authority, may not be circumvented under laws
|
absence of proper authority, may not be circumvented under laws
|
||||||
fulfilling obligations under Article 11 of the WIPO Copyright
|
fulfilling obligations under Article 11 of the WIPO Copyright
|
||||||
Treaty adopted on December 20, 1996, and/or similar international
|
Treaty adopted on December 20, 1996, and/or similar international
|
||||||
agreements.
|
agreements.
|
||||||
|
|
||||||
e. Exceptions and Limitations means fair use, fair dealing, and/or
|
e. Exceptions and Limitations means fair use, fair dealing, and/or
|
||||||
any other exception or limitation to Copyright and Similar Rights
|
any other exception or limitation to Copyright and Similar Rights
|
||||||
that applies to Your use of the Licensed Material.
|
that applies to Your use of the Licensed Material.
|
||||||
|
|
||||||
f. Licensed Material means the artistic or literary work, database,
|
f. Licensed Material means the artistic or literary work, database,
|
||||||
or other material to which the Licensor applied this Public
|
or other material to which the Licensor applied this Public
|
||||||
License.
|
License.
|
||||||
|
|
||||||
g. Licensed Rights means the rights granted to You subject to the
|
g. Licensed Rights means the rights granted to You subject to the
|
||||||
terms and conditions of this Public License, which are limited to
|
terms and conditions of this Public License, which are limited to
|
||||||
all Copyright and Similar Rights that apply to Your use of the
|
all Copyright and Similar Rights that apply to Your use of the
|
||||||
Licensed Material and that the Licensor has authority to license.
|
Licensed Material and that the Licensor has authority to license.
|
||||||
|
|
||||||
h. Licensor means the individual(s) or entity(ies) granting rights
|
h. Licensor means the individual(s) or entity(ies) granting rights
|
||||||
under this Public License.
|
under this Public License.
|
||||||
|
|
||||||
i. NonCommercial means not primarily intended for or directed towards
|
i. NonCommercial means not primarily intended for or directed towards
|
||||||
commercial advantage or monetary compensation. For purposes of
|
commercial advantage or monetary compensation. For purposes of
|
||||||
this Public License, the exchange of the Licensed Material for
|
this Public License, the exchange of the Licensed Material for
|
||||||
other material subject to Copyright and Similar Rights by digital
|
other material subject to Copyright and Similar Rights by digital
|
||||||
file-sharing or similar means is NonCommercial provided there is
|
file-sharing or similar means is NonCommercial provided there is
|
||||||
no payment of monetary compensation in connection with the
|
no payment of monetary compensation in connection with the
|
||||||
exchange.
|
exchange.
|
||||||
|
|
||||||
j. Share means to provide material to the public by any means or
|
j. Share means to provide material to the public by any means or
|
||||||
process that requires permission under the Licensed Rights, such
|
process that requires permission under the Licensed Rights, such
|
||||||
as reproduction, public display, public performance, distribution,
|
as reproduction, public display, public performance, distribution,
|
||||||
dissemination, communication, or importation, and to make material
|
dissemination, communication, or importation, and to make material
|
||||||
available to the public including in ways that members of the
|
available to the public including in ways that members of the
|
||||||
public may access the material from a place and at a time
|
public may access the material from a place and at a time
|
||||||
individually chosen by them.
|
individually chosen by them.
|
||||||
|
|
||||||
k. Sui Generis Database Rights means rights other than copyright
|
k. Sui Generis Database Rights means rights other than copyright
|
||||||
resulting from Directive 96/9/EC of the European Parliament and of
|
resulting from Directive 96/9/EC of the European Parliament and of
|
||||||
the Council of 11 March 1996 on the legal protection of databases,
|
the Council of 11 March 1996 on the legal protection of databases,
|
||||||
as amended and/or succeeded, as well as other essentially
|
as amended and/or succeeded, as well as other essentially
|
||||||
equivalent rights anywhere in the world.
|
equivalent rights anywhere in the world.
|
||||||
|
|
||||||
l. You means the individual or entity exercising the Licensed Rights
|
l. You means the individual or entity exercising the Licensed Rights
|
||||||
under this Public License. Your has a corresponding meaning.
|
under this Public License. Your has a corresponding meaning.
|
||||||
|
|
||||||
|
|
||||||
Section 2 -- Scope.
|
Section 2 -- Scope.
|
||||||
|
|
||||||
a. License grant.
|
a. License grant.
|
||||||
|
|
||||||
1. Subject to the terms and conditions of this Public License,
|
1. Subject to the terms and conditions of this Public License,
|
||||||
the Licensor hereby grants You a worldwide, royalty-free,
|
the Licensor hereby grants You a worldwide, royalty-free,
|
||||||
non-sublicensable, non-exclusive, irrevocable license to
|
non-sublicensable, non-exclusive, irrevocable license to
|
||||||
exercise the Licensed Rights in the Licensed Material to:
|
exercise the Licensed Rights in the Licensed Material to:
|
||||||
|
|
||||||
a. reproduce and Share the Licensed Material, in whole or
|
a. reproduce and Share the Licensed Material, in whole or
|
||||||
in part, for NonCommercial purposes only; and
|
in part, for NonCommercial purposes only; and
|
||||||
|
|
||||||
b. produce, reproduce, and Share Adapted Material for
|
b. produce, reproduce, and Share Adapted Material for
|
||||||
NonCommercial purposes only.
|
NonCommercial purposes only.
|
||||||
|
|
||||||
2. Exceptions and Limitations. For the avoidance of doubt, where
|
2. Exceptions and Limitations. For the avoidance of doubt, where
|
||||||
Exceptions and Limitations apply to Your use, this Public
|
Exceptions and Limitations apply to Your use, this Public
|
||||||
License does not apply, and You do not need to comply with
|
License does not apply, and You do not need to comply with
|
||||||
its terms and conditions.
|
its terms and conditions.
|
||||||
|
|
||||||
3. Term. The term of this Public License is specified in Section
|
3. Term. The term of this Public License is specified in Section
|
||||||
6(a).
|
6(a).
|
||||||
|
|
||||||
4. Media and formats; technical modifications allowed. The
|
4. Media and formats; technical modifications allowed. The
|
||||||
Licensor authorizes You to exercise the Licensed Rights in
|
Licensor authorizes You to exercise the Licensed Rights in
|
||||||
all media and formats whether now known or hereafter created,
|
all media and formats whether now known or hereafter created,
|
||||||
and to make technical modifications necessary to do so. The
|
and to make technical modifications necessary to do so. The
|
||||||
Licensor waives and/or agrees not to assert any right or
|
Licensor waives and/or agrees not to assert any right or
|
||||||
authority to forbid You from making technical modifications
|
authority to forbid You from making technical modifications
|
||||||
necessary to exercise the Licensed Rights, including
|
necessary to exercise the Licensed Rights, including
|
||||||
technical modifications necessary to circumvent Effective
|
technical modifications necessary to circumvent Effective
|
||||||
Technological Measures. For purposes of this Public License,
|
Technological Measures. For purposes of this Public License,
|
||||||
simply making modifications authorized by this Section 2(a)
|
simply making modifications authorized by this Section 2(a)
|
||||||
(4) never produces Adapted Material.
|
(4) never produces Adapted Material.
|
||||||
|
|
||||||
5. Downstream recipients.
|
5. Downstream recipients.
|
||||||
|
|
||||||
a. Offer from the Licensor -- Licensed Material. Every
|
a. Offer from the Licensor -- Licensed Material. Every
|
||||||
recipient of the Licensed Material automatically
|
recipient of the Licensed Material automatically
|
||||||
receives an offer from the Licensor to exercise the
|
receives an offer from the Licensor to exercise the
|
||||||
Licensed Rights under the terms and conditions of this
|
Licensed Rights under the terms and conditions of this
|
||||||
Public License.
|
Public License.
|
||||||
|
|
||||||
b. No downstream restrictions. You may not offer or impose
|
b. No downstream restrictions. You may not offer or impose
|
||||||
any additional or different terms or conditions on, or
|
any additional or different terms or conditions on, or
|
||||||
apply any Effective Technological Measures to, the
|
apply any Effective Technological Measures to, the
|
||||||
Licensed Material if doing so restricts exercise of the
|
Licensed Material if doing so restricts exercise of the
|
||||||
Licensed Rights by any recipient of the Licensed
|
Licensed Rights by any recipient of the Licensed
|
||||||
Material.
|
Material.
|
||||||
|
|
||||||
6. No endorsement. Nothing in this Public License constitutes or
|
6. No endorsement. Nothing in this Public License constitutes or
|
||||||
may be construed as permission to assert or imply that You
|
may be construed as permission to assert or imply that You
|
||||||
are, or that Your use of the Licensed Material is, connected
|
are, or that Your use of the Licensed Material is, connected
|
||||||
with, or sponsored, endorsed, or granted official status by,
|
with, or sponsored, endorsed, or granted official status by,
|
||||||
the Licensor or others designated to receive attribution as
|
the Licensor or others designated to receive attribution as
|
||||||
provided in Section 3(a)(1)(A)(i).
|
provided in Section 3(a)(1)(A)(i).
|
||||||
|
|
||||||
b. Other rights.
|
b. Other rights.
|
||||||
|
|
||||||
1. Moral rights, such as the right of integrity, are not
|
1. Moral rights, such as the right of integrity, are not
|
||||||
licensed under this Public License, nor are publicity,
|
licensed under this Public License, nor are publicity,
|
||||||
privacy, and/or other similar personality rights; however, to
|
privacy, and/or other similar personality rights; however, to
|
||||||
the extent possible, the Licensor waives and/or agrees not to
|
the extent possible, the Licensor waives and/or agrees not to
|
||||||
assert any such rights held by the Licensor to the limited
|
assert any such rights held by the Licensor to the limited
|
||||||
extent necessary to allow You to exercise the Licensed
|
extent necessary to allow You to exercise the Licensed
|
||||||
Rights, but not otherwise.
|
Rights, but not otherwise.
|
||||||
|
|
||||||
2. Patent and trademark rights are not licensed under this
|
2. Patent and trademark rights are not licensed under this
|
||||||
Public License.
|
Public License.
|
||||||
|
|
||||||
3. To the extent possible, the Licensor waives any right to
|
3. To the extent possible, the Licensor waives any right to
|
||||||
collect royalties from You for the exercise of the Licensed
|
collect royalties from You for the exercise of the Licensed
|
||||||
Rights, whether directly or through a collecting society
|
Rights, whether directly or through a collecting society
|
||||||
under any voluntary or waivable statutory or compulsory
|
under any voluntary or waivable statutory or compulsory
|
||||||
licensing scheme. In all other cases the Licensor expressly
|
licensing scheme. In all other cases the Licensor expressly
|
||||||
reserves any right to collect such royalties, including when
|
reserves any right to collect such royalties, including when
|
||||||
the Licensed Material is used other than for NonCommercial
|
the Licensed Material is used other than for NonCommercial
|
||||||
purposes.
|
purposes.
|
||||||
|
|
||||||
|
|
||||||
Section 3 -- License Conditions.
|
Section 3 -- License Conditions.
|
||||||
|
|
||||||
Your exercise of the Licensed Rights is expressly made subject to the
|
Your exercise of the Licensed Rights is expressly made subject to the
|
||||||
following conditions.
|
following conditions.
|
||||||
|
|
||||||
a. Attribution.
|
a. Attribution.
|
||||||
|
|
||||||
1. If You Share the Licensed Material (including in modified
|
1. If You Share the Licensed Material (including in modified
|
||||||
form), You must:
|
form), You must:
|
||||||
|
|
||||||
a. retain the following if it is supplied by the Licensor
|
a. retain the following if it is supplied by the Licensor
|
||||||
with the Licensed Material:
|
with the Licensed Material:
|
||||||
|
|
||||||
i. identification of the creator(s) of the Licensed
|
i. identification of the creator(s) of the Licensed
|
||||||
Material and any others designated to receive
|
Material and any others designated to receive
|
||||||
attribution, in any reasonable manner requested by
|
attribution, in any reasonable manner requested by
|
||||||
the Licensor (including by pseudonym if
|
the Licensor (including by pseudonym if
|
||||||
designated);
|
designated);
|
||||||
|
|
||||||
ii. a copyright notice;
|
ii. a copyright notice;
|
||||||
|
|
||||||
iii. a notice that refers to this Public License;
|
iii. a notice that refers to this Public License;
|
||||||
|
|
||||||
iv. a notice that refers to the disclaimer of
|
iv. a notice that refers to the disclaimer of
|
||||||
warranties;
|
warranties;
|
||||||
|
|
||||||
v. a URI or hyperlink to the Licensed Material to the
|
v. a URI or hyperlink to the Licensed Material to the
|
||||||
extent reasonably practicable;
|
extent reasonably practicable;
|
||||||
|
|
||||||
b. indicate if You modified the Licensed Material and
|
b. indicate if You modified the Licensed Material and
|
||||||
retain an indication of any previous modifications; and
|
retain an indication of any previous modifications; and
|
||||||
|
|
||||||
c. indicate the Licensed Material is licensed under this
|
c. indicate the Licensed Material is licensed under this
|
||||||
Public License, and include the text of, or the URI or
|
Public License, and include the text of, or the URI or
|
||||||
hyperlink to, this Public License.
|
hyperlink to, this Public License.
|
||||||
|
|
||||||
2. You may satisfy the conditions in Section 3(a)(1) in any
|
2. You may satisfy the conditions in Section 3(a)(1) in any
|
||||||
reasonable manner based on the medium, means, and context in
|
reasonable manner based on the medium, means, and context in
|
||||||
which You Share the Licensed Material. For example, it may be
|
which You Share the Licensed Material. For example, it may be
|
||||||
reasonable to satisfy the conditions by providing a URI or
|
reasonable to satisfy the conditions by providing a URI or
|
||||||
hyperlink to a resource that includes the required
|
hyperlink to a resource that includes the required
|
||||||
information.
|
information.
|
||||||
|
|
||||||
3. If requested by the Licensor, You must remove any of the
|
3. If requested by the Licensor, You must remove any of the
|
||||||
information required by Section 3(a)(1)(A) to the extent
|
information required by Section 3(a)(1)(A) to the extent
|
||||||
reasonably practicable.
|
reasonably practicable.
|
||||||
|
|
||||||
4. If You Share Adapted Material You produce, the Adapter's
|
4. If You Share Adapted Material You produce, the Adapter's
|
||||||
License You apply must not prevent recipients of the Adapted
|
License You apply must not prevent recipients of the Adapted
|
||||||
Material from complying with this Public License.
|
Material from complying with this Public License.
|
||||||
|
|
||||||
|
|
||||||
Section 4 -- Sui Generis Database Rights.
|
Section 4 -- Sui Generis Database Rights.
|
||||||
|
|
||||||
Where the Licensed Rights include Sui Generis Database Rights that
|
Where the Licensed Rights include Sui Generis Database Rights that
|
||||||
apply to Your use of the Licensed Material:
|
apply to Your use of the Licensed Material:
|
||||||
|
|
||||||
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
|
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
|
||||||
to extract, reuse, reproduce, and Share all or a substantial
|
to extract, reuse, reproduce, and Share all or a substantial
|
||||||
portion of the contents of the database for NonCommercial purposes
|
portion of the contents of the database for NonCommercial purposes
|
||||||
only;
|
only;
|
||||||
|
|
||||||
b. if You include all or a substantial portion of the database
|
b. if You include all or a substantial portion of the database
|
||||||
contents in a database in which You have Sui Generis Database
|
contents in a database in which You have Sui Generis Database
|
||||||
Rights, then the database in which You have Sui Generis Database
|
Rights, then the database in which You have Sui Generis Database
|
||||||
Rights (but not its individual contents) is Adapted Material; and
|
Rights (but not its individual contents) is Adapted Material; and
|
||||||
|
|
||||||
c. You must comply with the conditions in Section 3(a) if You Share
|
c. You must comply with the conditions in Section 3(a) if You Share
|
||||||
all or a substantial portion of the contents of the database.
|
all or a substantial portion of the contents of the database.
|
||||||
|
|
||||||
For the avoidance of doubt, this Section 4 supplements and does not
|
For the avoidance of doubt, this Section 4 supplements and does not
|
||||||
replace Your obligations under this Public License where the Licensed
|
replace Your obligations under this Public License where the Licensed
|
||||||
Rights include other Copyright and Similar Rights.
|
Rights include other Copyright and Similar Rights.
|
||||||
|
|
||||||
|
|
||||||
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
|
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
|
||||||
|
|
||||||
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
|
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
|
||||||
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
|
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
|
||||||
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
|
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
|
||||||
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
|
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
|
||||||
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
|
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
|
||||||
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||||
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
|
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
|
||||||
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
|
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
|
||||||
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
|
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
|
||||||
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
|
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
|
||||||
|
|
||||||
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
|
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
|
||||||
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
|
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
|
||||||
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
|
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
|
||||||
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
|
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
|
||||||
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
|
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
|
||||||
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
|
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
|
||||||
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
|
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
|
||||||
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
|
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
|
||||||
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
|
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
|
||||||
|
|
||||||
c. The disclaimer of warranties and limitation of liability provided
|
c. The disclaimer of warranties and limitation of liability provided
|
||||||
above shall be interpreted in a manner that, to the extent
|
above shall be interpreted in a manner that, to the extent
|
||||||
possible, most closely approximates an absolute disclaimer and
|
possible, most closely approximates an absolute disclaimer and
|
||||||
waiver of all liability.
|
waiver of all liability.
|
||||||
|
|
||||||
|
|
||||||
Section 6 -- Term and Termination.
|
Section 6 -- Term and Termination.
|
||||||
|
|
||||||
a. This Public License applies for the term of the Copyright and
|
a. This Public License applies for the term of the Copyright and
|
||||||
Similar Rights licensed here. However, if You fail to comply with
|
Similar Rights licensed here. However, if You fail to comply with
|
||||||
this Public License, then Your rights under this Public License
|
this Public License, then Your rights under this Public License
|
||||||
terminate automatically.
|
terminate automatically.
|
||||||
|
|
||||||
b. Where Your right to use the Licensed Material has terminated under
|
b. Where Your right to use the Licensed Material has terminated under
|
||||||
Section 6(a), it reinstates:
|
Section 6(a), it reinstates:
|
||||||
|
|
||||||
1. automatically as of the date the violation is cured, provided
|
1. automatically as of the date the violation is cured, provided
|
||||||
it is cured within 30 days of Your discovery of the
|
it is cured within 30 days of Your discovery of the
|
||||||
violation; or
|
violation; or
|
||||||
|
|
||||||
2. upon express reinstatement by the Licensor.
|
2. upon express reinstatement by the Licensor.
|
||||||
|
|
||||||
For the avoidance of doubt, this Section 6(b) does not affect any
|
For the avoidance of doubt, this Section 6(b) does not affect any
|
||||||
right the Licensor may have to seek remedies for Your violations
|
right the Licensor may have to seek remedies for Your violations
|
||||||
of this Public License.
|
of this Public License.
|
||||||
|
|
||||||
c. For the avoidance of doubt, the Licensor may also offer the
|
c. For the avoidance of doubt, the Licensor may also offer the
|
||||||
Licensed Material under separate terms or conditions or stop
|
Licensed Material under separate terms or conditions or stop
|
||||||
distributing the Licensed Material at any time; however, doing so
|
distributing the Licensed Material at any time; however, doing so
|
||||||
will not terminate this Public License.
|
will not terminate this Public License.
|
||||||
|
|
||||||
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
|
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
|
||||||
License.
|
License.
|
||||||
|
|
||||||
|
|
||||||
Section 7 -- Other Terms and Conditions.
|
Section 7 -- Other Terms and Conditions.
|
||||||
|
|
||||||
a. The Licensor shall not be bound by any additional or different
|
a. The Licensor shall not be bound by any additional or different
|
||||||
terms or conditions communicated by You unless expressly agreed.
|
terms or conditions communicated by You unless expressly agreed.
|
||||||
|
|
||||||
b. Any arrangements, understandings, or agreements regarding the
|
b. Any arrangements, understandings, or agreements regarding the
|
||||||
Licensed Material not stated herein are separate from and
|
Licensed Material not stated herein are separate from and
|
||||||
independent of the terms and conditions of this Public License.
|
independent of the terms and conditions of this Public License.
|
||||||
|
|
||||||
|
|
||||||
Section 8 -- Interpretation.
|
Section 8 -- Interpretation.
|
||||||
|
|
||||||
a. For the avoidance of doubt, this Public License does not, and
|
a. For the avoidance of doubt, this Public License does not, and
|
||||||
shall not be interpreted to, reduce, limit, restrict, or impose
|
shall not be interpreted to, reduce, limit, restrict, or impose
|
||||||
conditions on any use of the Licensed Material that could lawfully
|
conditions on any use of the Licensed Material that could lawfully
|
||||||
be made without permission under this Public License.
|
be made without permission under this Public License.
|
||||||
|
|
||||||
b. To the extent possible, if any provision of this Public License is
|
b. To the extent possible, if any provision of this Public License is
|
||||||
deemed unenforceable, it shall be automatically reformed to the
|
deemed unenforceable, it shall be automatically reformed to the
|
||||||
minimum extent necessary to make it enforceable. If the provision
|
minimum extent necessary to make it enforceable. If the provision
|
||||||
cannot be reformed, it shall be severed from this Public License
|
cannot be reformed, it shall be severed from this Public License
|
||||||
without affecting the enforceability of the remaining terms and
|
without affecting the enforceability of the remaining terms and
|
||||||
conditions.
|
conditions.
|
||||||
|
|
||||||
c. No term or condition of this Public License will be waived and no
|
c. No term or condition of this Public License will be waived and no
|
||||||
failure to comply consented to unless expressly agreed to by the
|
failure to comply consented to unless expressly agreed to by the
|
||||||
Licensor.
|
Licensor.
|
||||||
|
|
||||||
d. Nothing in this Public License constitutes or may be interpreted
|
d. Nothing in this Public License constitutes or may be interpreted
|
||||||
as a limitation upon, or waiver of, any privileges and immunities
|
as a limitation upon, or waiver of, any privileges and immunities
|
||||||
that apply to the Licensor or You, including from the legal
|
that apply to the Licensor or You, including from the legal
|
||||||
processes of any jurisdiction or authority.
|
processes of any jurisdiction or authority.
|
||||||
|
|
||||||
=======================================================================
|
=======================================================================
|
||||||
|
|
||||||
Creative Commons is not a party to its public
|
Creative Commons is not a party to its public
|
||||||
licenses. Notwithstanding, Creative Commons may elect to apply one of
|
licenses. Notwithstanding, Creative Commons may elect to apply one of
|
||||||
its public licenses to material it publishes and in those instances
|
its public licenses to material it publishes and in those instances
|
||||||
will be considered the “Licensor.” The text of the Creative Commons
|
will be considered the “Licensor.” The text of the Creative Commons
|
||||||
public licenses is dedicated to the public domain under the CC0 Public
|
public licenses is dedicated to the public domain under the CC0 Public
|
||||||
Domain Dedication. Except for the limited purpose of indicating that
|
Domain Dedication. Except for the limited purpose of indicating that
|
||||||
material is shared under a Creative Commons public license or as
|
material is shared under a Creative Commons public license or as
|
||||||
otherwise permitted by the Creative Commons policies published at
|
otherwise permitted by the Creative Commons policies published at
|
||||||
creativecommons.org/policies, Creative Commons does not authorize the
|
creativecommons.org/policies, Creative Commons does not authorize the
|
||||||
use of the trademark "Creative Commons" or any other trademark or logo
|
use of the trademark "Creative Commons" or any other trademark or logo
|
||||||
of Creative Commons without its prior written consent including,
|
of Creative Commons without its prior written consent including,
|
||||||
without limitation, in connection with any unauthorized modifications
|
without limitation, in connection with any unauthorized modifications
|
||||||
to any of its public licenses or any other arrangements,
|
to any of its public licenses or any other arrangements,
|
||||||
understandings, or agreements concerning use of licensed material. For
|
understandings, or agreements concerning use of licensed material. For
|
||||||
the avoidance of doubt, this paragraph does not form part of the
|
the avoidance of doubt, this paragraph does not form part of the
|
||||||
public licenses.
|
public licenses.
|
||||||
|
|
||||||
Creative Commons may be contacted at creativecommons.org.
|
Creative Commons may be contacted at creativecommons.org.
|
||||||
78
README.md
78
README.md
@@ -1,40 +1,40 @@
|
|||||||
# vyla/sdk
|
# vyla/sdk
|
||||||
VylaSDK is a javascript base version of [Vyla](https://vyla.mintlify.app/)'s stream & scrape api, allowing developers to access content providers directly without using a REST api. This gives you the power & updates of Vyla without the headaches of api keys & server reliability.
|
VylaSDK is a javascript base version of [Vyla](https://vyla.mintlify.app/)'s stream & scrape api, allowing developers to access content providers directly without using a REST api. This gives you the power & updates of Vyla without the headaches of api keys & server reliability.
|
||||||
|
|
||||||
## Installing
|
## Installing
|
||||||
To start, install VylaSDK through npm or pnpm:
|
To start, install VylaSDK through npm or pnpm:
|
||||||
```bash
|
```bash
|
||||||
# npm
|
# npm
|
||||||
npm install @vyla-entertainment/sdk
|
npm install @vyla-entertainment/sdk
|
||||||
|
|
||||||
# pnpm
|
# pnpm
|
||||||
pnpm install @vyla-entertainment/sdk
|
pnpm install @vyla-entertainment/sdk
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
Start by creating a VylaSDK element with your TMDB api key.
|
Start by creating a VylaSDK element with your TMDB api key.
|
||||||
```js
|
```js
|
||||||
import VylaSDK from "@vyla-entertainment/sdk";
|
import VylaSDK from "@vyla-entertainment/sdk";
|
||||||
|
|
||||||
const sdk = new VylaSDK({
|
const sdk = new VylaSDK({
|
||||||
tmdbApiKey: ...
|
tmdbApiKey: ...
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
You can now use this to access Vyla's content.
|
You can now use this to access Vyla's content.
|
||||||
```js
|
```js
|
||||||
// Get sources
|
// Get sources
|
||||||
// Args: excludeDisabled
|
// Args: excludeDisabled
|
||||||
sdk.getSources(false);
|
sdk.getSources(false);
|
||||||
|
|
||||||
// Get subtitles
|
// Get subtitles
|
||||||
// Args: tmdb id, season?, episode?
|
// Args: tmdb id, season?, episode?
|
||||||
await sdk.getSubtitles('0000000', null, null); -> {label, file, type, source}[]
|
await sdk.getSubtitles('0000000', null, null); -> {label, file, type, source}[]
|
||||||
|
|
||||||
// Probe & test a source
|
// Probe & test a source
|
||||||
// Args: source key
|
// Args: source key
|
||||||
await sdk.probeSource('nebula'); -> {ok, ms}
|
await sdk.probeSource('nebula'); -> {ok, ms}
|
||||||
|
|
||||||
// Probe & test all sources
|
// Probe & test all sources
|
||||||
await sdk.probeAllSources(); -> {'key': {ok, ms}, ...}
|
await sdk.probeAllSources(); -> {'key': {ok, ms}, ...}
|
||||||
```
|
```
|
||||||
1036
src/config.js
1036
src/config.js
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
|||||||
import { getDownloads, getDownloadsTv } from "./sources/trendimovies";
|
import { getDownloads, getDownloadsTv } from "./sources/trendimovies";
|
||||||
|
|
||||||
export async function fetchDownloads(id, s = null, e = null) {
|
export async function fetchDownloads(id, s = null, e = null) {
|
||||||
const res = s != null && e != null
|
const res = s != null && e != null
|
||||||
? await getDownloadsTv(id, s, e)
|
? await getDownloadsTv(id, s, e)
|
||||||
: await getDownloads(id);
|
: await getDownloads(id);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
@@ -1,63 +1,63 @@
|
|||||||
const BASE = "https://trendimovies.com";
|
const BASE = "https://trendimovies.com";
|
||||||
|
|
||||||
const HEADERS = {
|
const HEADERS = {
|
||||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
||||||
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||||
"Accept-Language": "en-US,en;q=0.9",
|
"Accept-Language": "en-US,en;q=0.9",
|
||||||
"Referer": "https://trendimovies.com/",
|
"Referer": "https://trendimovies.com/",
|
||||||
};
|
};
|
||||||
|
|
||||||
async function fetchPage(path) {
|
async function fetchPage(path) {
|
||||||
const res = await fetch(`${BASE}${path}`, { headers: HEADERS });
|
const res = await fetch(`${BASE}${path}`, { headers: HEADERS });
|
||||||
if (!res.ok) throw new Error(`trendimovies ${res.status}`);
|
if (!res.ok) throw new Error(`trendimovies ${res.status}`);
|
||||||
return res.text();
|
return res.text();
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseLinks(html) {
|
function parseLinks(html) {
|
||||||
const m = html.match(/DownloadSection[^>]*props="([^"]+)"/s) ||
|
const m = html.match(/DownloadSection[^>]*props="([^"]+)"/s) ||
|
||||||
html.match(/props="([^"]+)"[^>]*ssr[^>]*client="load"[^>]*opts="[^"]*DownloadSection/s);
|
html.match(/props="([^"]+)"[^>]*ssr[^>]*client="load"[^>]*opts="[^"]*DownloadSection/s);
|
||||||
if (!m) {
|
if (!m) {
|
||||||
const alt = html.match(/"DownloadSection"/);
|
const alt = html.match(/"DownloadSection"/);
|
||||||
if (!alt) return [];
|
if (!alt) return [];
|
||||||
const propsMatch = html.match(/props="({[^"]*DownloadSection[^"]*})"/s);
|
const propsMatch = html.match(/props="({[^"]*DownloadSection[^"]*})"/s);
|
||||||
if (!propsMatch) return [];
|
if (!propsMatch) return [];
|
||||||
return extractLinks(propsMatch[1].replace(/"/g, '"').replace(/'/g, "'"));
|
return extractLinks(propsMatch[1].replace(/"/g, '"').replace(/'/g, "'"));
|
||||||
}
|
}
|
||||||
return extractLinks(m[1].replace(/"/g, '"').replace(/'/g, "'"));
|
return extractLinks(m[1].replace(/"/g, '"').replace(/'/g, "'"));
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractLinks(propsJson) {
|
function extractLinks(propsJson) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(propsJson);
|
const parsed = JSON.parse(propsJson);
|
||||||
const rawLinks = parsed?.links?.[1];
|
const rawLinks = parsed?.links?.[1];
|
||||||
if (!Array.isArray(rawLinks)) return [];
|
if (!Array.isArray(rawLinks)) return [];
|
||||||
return rawLinks
|
return rawLinks
|
||||||
.map(item => item?.[1])
|
.map(item => item?.[1])
|
||||||
.filter(l => l && typeof l === "object")
|
.filter(l => l && typeof l === "object")
|
||||||
.map(l => ({
|
.map(l => ({
|
||||||
url: unwrap(l.url),
|
url: unwrap(l.url),
|
||||||
quality: unwrap(l.quality) || "HD",
|
quality: unwrap(l.quality) || "HD",
|
||||||
size: unwrap(l.file_size) || null,
|
size: unwrap(l.file_size) || null,
|
||||||
type: "mkv",
|
type: "mkv",
|
||||||
active: unwrap(l.is_active) !== false,
|
active: unwrap(l.is_active) !== false,
|
||||||
}))
|
}))
|
||||||
.filter(l => l.url && l.active);
|
.filter(l => l.url && l.active);
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function unwrap(v) {
|
function unwrap(v) {
|
||||||
if (Array.isArray(v) && v.length === 2 && (v[0] === 0 || v[0] === 1)) return v[1];
|
if (Array.isArray(v) && v.length === 2 && (v[0] === 0 || v[0] === 1)) return v[1];
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getDownloads(tmdbId) {
|
export async function getDownloads(tmdbId) {
|
||||||
const html = await fetchPage(`/movie/${tmdbId}`);
|
const html = await fetchPage(`/movie/${tmdbId}`);
|
||||||
return parseLinks(html);
|
return parseLinks(html);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getDownloadsTv(tmdbId, season, episode) {
|
export async function getDownloadsTv(tmdbId, season, episode) {
|
||||||
const html = await fetchPage(`/tv/${tmdbId}/season/${season}/episode/${episode}`);
|
const html = await fetchPage(`/tv/${tmdbId}/season/${season}/episode/${episode}`);
|
||||||
return parseLinks(html);
|
return parseLinks(html);
|
||||||
}
|
}
|
||||||
@@ -1,38 +1,38 @@
|
|||||||
import { HEALTH_PROBE_ID } from "./config.js";
|
import { HEALTH_PROBE_ID } from "./config.js";
|
||||||
|
|
||||||
const withTimeout = (promise, ms) => Promise.race([promise, new Promise(r => setTimeout(() => r(null), ms))]);
|
const withTimeout = (promise, ms) => Promise.race([promise, new Promise(r => setTimeout(() => r(null), ms))]);
|
||||||
async function withRetry(fn, attempts = 2, delay = 300) {
|
async function withRetry(fn, attempts = 2, delay = 300) {
|
||||||
for (let i = 0; i < attempts; i++) {
|
for (let i = 0; i < attempts; i++) {
|
||||||
try {
|
try {
|
||||||
const result = await fn();
|
const result = await fn();
|
||||||
if (result != null) return result;
|
if (result != null) return result;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (i === attempts - 1) throw err;
|
if (i === attempts - 1) throw err;
|
||||||
await new Promise(r => setTimeout(r, delay + Math.random() * delay * 0.5));
|
await new Promise(r => setTimeout(r, delay + Math.random() * delay * 0.5));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function probeSource(tmdbApiKey, cfg, mod) {
|
export async function probeSource(tmdbApiKey, cfg, mod) {
|
||||||
const audio = /dub$/.test(cfg.key) ? 'dub' : 'sub';
|
const audio = /dub$/.test(cfg.key) ? 'dub' : 'sub';
|
||||||
const streamArgs = {
|
const streamArgs = {
|
||||||
id: HEALTH_PROBE_ID,
|
id: HEALTH_PROBE_ID,
|
||||||
s: null,
|
s: null,
|
||||||
e: null,
|
e: null,
|
||||||
clientIP: null,
|
clientIP: null,
|
||||||
absoluteBase: null,
|
absoluteBase: null,
|
||||||
audio,
|
audio,
|
||||||
tmdbApiKey,
|
tmdbApiKey,
|
||||||
config: cfg,
|
config: cfg,
|
||||||
};
|
};
|
||||||
|
|
||||||
const t = Date.now();
|
const t = Date.now();
|
||||||
const result = await withTimeout(
|
const result = await withTimeout(
|
||||||
withRetry(() => mod.getStream(streamArgs), cfg.retries, 300),
|
withRetry(() => mod.getStream(streamArgs), cfg.retries, 300),
|
||||||
cfg.timeout
|
cfg.timeout
|
||||||
);
|
);
|
||||||
|
|
||||||
const firstUrl = result?.url ?? result?.allUrls?.[0]?.url;
|
const firstUrl = result?.url ?? result?.allUrls?.[0]?.url;
|
||||||
return { ok: !!(typeof firstUrl === 'string' && firstUrl.startsWith('http')), ms: Date.now() - t };
|
return { ok: !!(typeof firstUrl === 'string' && firstUrl.startsWith('http')), ms: Date.now() - t };
|
||||||
}
|
}
|
||||||
128
src/sdk.js
128
src/sdk.js
@@ -1,65 +1,65 @@
|
|||||||
import SOURCES, { SOURCE_MAP } from "./config.js";
|
import SOURCES, { SOURCE_MAP } from "./config.js";
|
||||||
import { fetchDownloads } from "./downloads/downloads.js";
|
import { fetchDownloads } from "./downloads/downloads.js";
|
||||||
import { probeSource } from "./health.js";
|
import { probeSource } from "./health.js";
|
||||||
import { fetchSubtitles, getSubPathsMovie, getSubPathsTv } from "./subtitles.js";
|
import { fetchSubtitles, getSubPathsMovie, getSubPathsTv } from "./subtitles.js";
|
||||||
import { createStreamArgs } from "./utils/helpers.js";
|
import { createStreamArgs } from "./utils/helpers.js";
|
||||||
|
|
||||||
const ALL_SOURCE_MODULES = Object.fromEntries(
|
const ALL_SOURCE_MODULES = Object.fromEntries(
|
||||||
await Promise.all(SOURCES.map(async cfg => [cfg.key, await import(`./sources/${cfg.sourceFile}.js`)]))
|
await Promise.all(SOURCES.map(async cfg => [cfg.key, await import(`./sources/${cfg.sourceFile}.js`)]))
|
||||||
);
|
);
|
||||||
|
|
||||||
const SOURCE_MODULES = Object.fromEntries(
|
const SOURCE_MODULES = Object.fromEntries(
|
||||||
Object.entries(ALL_SOURCE_MODULES).filter(([key]) => !SOURCE_MAP[key]?.disabled)
|
Object.entries(ALL_SOURCE_MODULES).filter(([key]) => !SOURCE_MAP[key]?.disabled)
|
||||||
);
|
);
|
||||||
|
|
||||||
export default class VylaSDK {
|
export default class VylaSDK {
|
||||||
constructor({ tmdbApiKey } = {}) {
|
constructor({ tmdbApiKey } = {}) {
|
||||||
this.tmdbApiKey = tmdbApiKey;
|
this.tmdbApiKey = tmdbApiKey;
|
||||||
}
|
}
|
||||||
getSources(excludeDisabled = false) {
|
getSources(excludeDisabled = false) {
|
||||||
if (excludeDisabled) {
|
if (excludeDisabled) {
|
||||||
return SOURCES.filter(cfg => !cfg.disabled);
|
return SOURCES.filter(cfg => !cfg.disabled);
|
||||||
}
|
}
|
||||||
return SOURCES;
|
return SOURCES;
|
||||||
}
|
}
|
||||||
async probeAllSources() {
|
async probeAllSources() {
|
||||||
const active = SOURCES.filter(cfg => !cfg.disabled);
|
const active = SOURCES.filter(cfg => !cfg.disabled);
|
||||||
|
|
||||||
const results = await Promise.allSettled(
|
const results = await Promise.allSettled(
|
||||||
active.map(async cfg => await probeSource(cfg, SOURCE_MODULES[cfg.key]))
|
active.map(async cfg => await probeSource(cfg, SOURCE_MODULES[cfg.key]))
|
||||||
);
|
);
|
||||||
|
|
||||||
const sources = Object.fromEntries(
|
const sources = Object.fromEntries(
|
||||||
active.map((cfg, i) => [
|
active.map((cfg, i) => [
|
||||||
cfg.key,
|
cfg.key,
|
||||||
results[i].status === 'fulfilled' ? results[i].value : { ok: false, ms: null },
|
results[i].status === 'fulfilled' ? results[i].value : { ok: false, ms: null },
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
return sources;
|
return sources;
|
||||||
}
|
}
|
||||||
async probeSource(key) {
|
async probeSource(key) {
|
||||||
const cfg = SOURCES.find(cfg => cfg.key === key);
|
const cfg = SOURCES.find(cfg => cfg.key === key);
|
||||||
if (!cfg) throw new Error(`Source with key "${key}" not found`);
|
if (!cfg) throw new Error(`Source with key "${key}" not found`);
|
||||||
const mod = SOURCE_MODULES[key];
|
const mod = SOURCE_MODULES[key];
|
||||||
if (!mod) throw new Error(`Source module for key "${key}" not found`);
|
if (!mod) throw new Error(`Source module for key "${key}" not found`);
|
||||||
return await probeSource(this.tmdbApiKey, cfg, mod);
|
return await probeSource(this.tmdbApiKey, cfg, mod);
|
||||||
}
|
}
|
||||||
async getStream(key, id, s = null, e = null, clientIP = null) {
|
async getStream(key, id, s = null, e = null, clientIP = null) {
|
||||||
const cfg = SOURCES.find(cfg => cfg.key === key);
|
const cfg = SOURCES.find(cfg => cfg.key === key);
|
||||||
if (!cfg) throw new Error(`Source with key "${key}" not found`);
|
if (!cfg) throw new Error(`Source with key "${key}" not found`);
|
||||||
const mod = SOURCE_MODULES[key];
|
const mod = SOURCE_MODULES[key];
|
||||||
if (!mod) throw new Error(`Source module for key "${key}" not found`);
|
if (!mod) throw new Error(`Source module for key "${key}" not found`);
|
||||||
const streamArgs = await createStreamArgs(cfg, this.tmdbApiKey, id, s, e, clientIP);
|
const streamArgs = await createStreamArgs(cfg, this.tmdbApiKey, id, s, e, clientIP);
|
||||||
return await mod.getStream(streamArgs);
|
return await mod.getStream(streamArgs);
|
||||||
}
|
}
|
||||||
async getSubtitles(id, s = null, e = null) {
|
async getSubtitles(id, s = null, e = null) {
|
||||||
const paths = s != null && e != null
|
const paths = s != null && e != null
|
||||||
? await getSubPathsTv(id, s, e)
|
? await getSubPathsTv(id, s, e)
|
||||||
: await getSubPathsMovie(id);
|
: await getSubPathsMovie(id);
|
||||||
return await fetchSubtitles(paths);
|
return await fetchSubtitles(paths);
|
||||||
}
|
}
|
||||||
async getDownloads(id, s = null, e = null) {
|
async getDownloads(id, s = null, e = null) {
|
||||||
return await fetchDownloads(id, s, e);
|
return await fetchDownloads(id, s, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,57 +1,57 @@
|
|||||||
import { getTmdbInfo, fetchJson, USER_AGENT } from '../utils/helpers.js';
|
import { getTmdbInfo, fetchJson, USER_AGENT } from '../utils/helpers.js';
|
||||||
|
|
||||||
async function resolveSlug(id, s, e, info) {
|
async function resolveSlug(id, s, e, info) {
|
||||||
const titles = info?.titles || [];
|
const titles = info?.titles || [];
|
||||||
const keywords = [];
|
const keywords = [];
|
||||||
for (const title of titles) {
|
for (const title of titles) {
|
||||||
const t = title.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').replace(/\s+/g, ' ').trim();
|
const t = title.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').replace(/\s+/g, ' ').trim();
|
||||||
if (!keywords.includes(t)) keywords.push(t);
|
if (!keywords.includes(t)) keywords.push(t);
|
||||||
const words = title.split(/[\s:]+/).filter(w => w.length > 2);
|
const words = title.split(/[\s:]+/).filter(w => w.length > 2);
|
||||||
if (words.length >= 2) {
|
if (words.length >= 2) {
|
||||||
const short = words.slice(0, 3).join(' ').toLowerCase();
|
const short = words.slice(0, 3).join(' ').toLowerCase();
|
||||||
if (!keywords.includes(short)) keywords.push(short);
|
if (!keywords.includes(short)) keywords.push(short);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const slugCandidates = [];
|
const slugCandidates = [];
|
||||||
for (const keyword of keywords) {
|
for (const keyword of keywords) {
|
||||||
try {
|
try {
|
||||||
const data = await fetchJson(`https://123animehub.cc/ajax/film/search?keyword=${encodeURIComponent(keyword)}&_=${Date.now()}`, { headers: { 'X-Requested-With': 'XMLHttpRequest', 'Referer': 'https://123animehub.cc', 'Accept': 'application/json', 'User-Agent': USER_AGENT }, signal: AbortSignal.timeout(8000) });
|
const data = await fetchJson(`https://123animehub.cc/ajax/film/search?keyword=${encodeURIComponent(keyword)}&_=${Date.now()}`, { headers: { 'X-Requested-With': 'XMLHttpRequest', 'Referer': 'https://123animehub.cc', 'Accept': 'application/json', 'User-Agent': USER_AGENT }, signal: AbortSignal.timeout(8000) });
|
||||||
let html = data.html || '', idx = 0;
|
let html = data.html || '', idx = 0;
|
||||||
while ((idx = html.indexOf('href="/anime/', idx)) !== -1) {
|
while ((idx = html.indexOf('href="/anime/', idx)) !== -1) {
|
||||||
const start = idx + 13;
|
const start = idx + 13;
|
||||||
const end = html.indexOf('"', start);
|
const end = html.indexOf('"', start);
|
||||||
const slug = html.slice(start, end);
|
const slug = html.slice(start, end);
|
||||||
if (!slugCandidates.includes(slug)) slugCandidates.push(slug);
|
if (!slugCandidates.includes(slug)) slugCandidates.push(slug);
|
||||||
idx = end;
|
idx = end;
|
||||||
}
|
}
|
||||||
} catch { }
|
} catch { }
|
||||||
if (slugCandidates.length) break;
|
if (slugCandidates.length) break;
|
||||||
}
|
}
|
||||||
for (const slug of slugCandidates) {
|
for (const slug of slugCandidates) {
|
||||||
try {
|
try {
|
||||||
const data = await fetchJson(`https://123animehub.cc/ajax/episode/info?epr=${encodeURIComponent(s ? `${slug}/${s}/${e}` : `${slug}/1`)}&ts=1&_=${Date.now()}`, { headers: { 'Referer': `https://123animehub.cc/anime/${slug}`, 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json, text/javascript, */*; q=0.01', 'User-Agent': USER_AGENT }, signal: AbortSignal.timeout(10000) });
|
const data = await fetchJson(`https://123animehub.cc/ajax/episode/info?epr=${encodeURIComponent(s ? `${slug}/${s}/${e}` : `${slug}/1`)}&ts=1&_=${Date.now()}`, { headers: { 'Referer': `https://123animehub.cc/anime/${slug}`, 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json, text/javascript, */*; q=0.01', 'User-Agent': USER_AGENT }, signal: AbortSignal.timeout(10000) });
|
||||||
if (data?.target) return data.target;
|
if (data?.target) return data.target;
|
||||||
} catch { }
|
} catch { }
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStream({ id, s, e }) {
|
export async function getStream({ id, s, e }) {
|
||||||
if (!s) return null;
|
if (!s) return null;
|
||||||
const info = await getTmdbInfo(id, 'tv', s);
|
const info = await getTmdbInfo(id, 'tv', s);
|
||||||
if (!info.isAnime) return null;
|
if (!info.isAnime) return null;
|
||||||
const target = await resolveSlug(id, s, e, info);
|
const target = await resolveSlug(id, s, e, info);
|
||||||
if (!target) return null;
|
if (!target) return null;
|
||||||
const embedMatch = target.match(/\/embed-[^/]+\/([A-Za-z0-9+/=]+)$/);
|
const embedMatch = target.match(/\/embed-[^/]+\/([A-Za-z0-9+/=]+)$/);
|
||||||
if (!embedMatch) return null;
|
if (!embedMatch) return null;
|
||||||
const origin = new URL(target).origin;
|
const origin = new URL(target).origin;
|
||||||
try {
|
try {
|
||||||
const embedRes = await fetch(target, { headers: { 'User-Agent': USER_AGENT }, signal: AbortSignal.timeout(10000) });
|
const embedRes = await fetch(target, { headers: { 'User-Agent': USER_AGENT }, signal: AbortSignal.timeout(10000) });
|
||||||
if (!embedRes.ok) return null;
|
if (!embedRes.ok) return null;
|
||||||
const setCookie = embedRes.headers.get('set-cookie');
|
const setCookie = embedRes.headers.get('set-cookie');
|
||||||
const data = await fetchJson(`${origin}/hs/getSources?id=${embedMatch[1]}`, { headers: { 'Referer': target, 'Accept': '*/*', 'User-Agent': USER_AGENT, ...(setCookie ? { 'Cookie': setCookie.split(';')[0].trim() } : {}) }, signal: AbortSignal.timeout(10000) });
|
const data = await fetchJson(`${origin}/hs/getSources?id=${embedMatch[1]}`, { headers: { 'Referer': target, 'Accept': '*/*', 'User-Agent': USER_AGENT, ...(setCookie ? { 'Cookie': setCookie.split(';')[0].trim() } : {}) }, signal: AbortSignal.timeout(10000) });
|
||||||
const src = typeof data.sources === 'string' && data.sources.length ? data.sources : (Array.isArray(data.sources) && data.sources.length ? (data.sources[0].file || data.sources[0].src || data.sources[0].url) : null);
|
const src = typeof data.sources === 'string' && data.sources.length ? data.sources : (Array.isArray(data.sources) && data.sources.length ? (data.sources[0].file || data.sources[0].src || data.sources[0].url) : null);
|
||||||
if (src) return { url: src, headers: { 'Referer': 'https://play2.echovideo.ru/', 'Origin': 'https://play2.echovideo.ru' } };
|
if (src) return { url: src, headers: { 'Referer': 'https://play2.echovideo.ru/', 'Origin': 'https://play2.echovideo.ru' } };
|
||||||
} catch { }
|
} catch { }
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -1,92 +1,92 @@
|
|||||||
import { USER_AGENT } from '../utils/helpers.js';
|
import { USER_AGENT } from '../utils/helpers.js';
|
||||||
|
|
||||||
const BASE_URL = "https://1embed.cc";
|
const BASE_URL = "https://1embed.cc";
|
||||||
|
|
||||||
const PROVIDERS = [
|
const PROVIDERS = [
|
||||||
{ id: "sdev", name: "1Embed - SDev" },
|
{ id: "sdev", name: "1Embed - SDev" },
|
||||||
{ id: "xpa", name: "1Embed - XPass" },
|
{ id: "xpa", name: "1Embed - XPass" },
|
||||||
{ id: "vnes", name: "1Embed - VidNest" },
|
{ id: "vnes", name: "1Embed - VidNest" },
|
||||||
{ id: "pro", name: "1Embed - Pro" },
|
{ id: "pro", name: "1Embed - Pro" },
|
||||||
{ id: "pur", name: "1Embed - Purstream" },
|
{ id: "pur", name: "1Embed - Purstream" },
|
||||||
{ id: "czo", name: "1Embed - Cinezo" },
|
{ id: "czo", name: "1Embed - Cinezo" },
|
||||||
{ id: "fas", name: "1Embed - VidFast" }
|
{ id: "fas", name: "1Embed - VidFast" }
|
||||||
];
|
];
|
||||||
|
|
||||||
let cachedToken = null;
|
let cachedToken = null;
|
||||||
let tokenExpiresAt = 0;
|
let tokenExpiresAt = 0;
|
||||||
|
|
||||||
async function getToken() {
|
async function getToken() {
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
if (cachedToken && tokenExpiresAt > now + 10) return cachedToken;
|
if (cachedToken && tokenExpiresAt > now + 10) return cachedToken;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${BASE_URL}/api/token`, { headers: { "Referer": `${BASE_URL}/`, "User-Agent": USER_AGENT, "Accept": "application/json" }, signal: AbortSignal.timeout(10000) });
|
const res = await fetch(`${BASE_URL}/api/token`, { headers: { "Referer": `${BASE_URL}/`, "User-Agent": USER_AGENT, "Accept": "application/json" }, signal: AbortSignal.timeout(10000) });
|
||||||
if (!res.ok) return null;
|
if (!res.ok) return null;
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
cachedToken = data.token;
|
cachedToken = data.token;
|
||||||
tokenExpiresAt = data.expiresAt;
|
tokenExpiresAt = data.expiresAt;
|
||||||
return cachedToken;
|
return cachedToken;
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchProviderStream(provider, type, id, s, e, token) {
|
async function fetchProviderStream(provider, type, id, s, e, token) {
|
||||||
try {
|
try {
|
||||||
const referer = type === "movie" ? `${BASE_URL}/embed/movie/${id}` : `${BASE_URL}/embed/tv/${id}/${s}/${e}`;
|
const referer = type === "movie" ? `${BASE_URL}/embed/movie/${id}` : `${BASE_URL}/embed/tv/${id}/${s}/${e}`;
|
||||||
const payload = { type, id: String(id), provider: provider.id };
|
const payload = { type, id: String(id), provider: provider.id };
|
||||||
if (type === "tv") { payload.season = Number(s); payload.episode = Number(e); }
|
if (type === "tv") { payload.season = Number(s); payload.episode = Number(e); }
|
||||||
const res = await fetch(`${BASE_URL}/api/sources`, {
|
const res = await fetch(`${BASE_URL}/api/sources`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "User-Agent": USER_AGENT, "Referer": referer, "Origin": BASE_URL, "Accept": "application/json", "Content-Type": "application/json", "x-bcine-key": token, "X-Requested-With": "XMLHttpRequest" },
|
headers: { "User-Agent": USER_AGENT, "Referer": referer, "Origin": BASE_URL, "Accept": "application/json", "Content-Type": "application/json", "x-bcine-key": token, "X-Requested-With": "XMLHttpRequest" },
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
signal: AbortSignal.timeout(15000)
|
signal: AbortSignal.timeout(15000)
|
||||||
});
|
});
|
||||||
if (!res.ok) return null;
|
if (!res.ok) return null;
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
let rawSources = [];
|
let rawSources = [];
|
||||||
if (Array.isArray(data.servers)) rawSources.push(...data.servers);
|
if (Array.isArray(data.servers)) rawSources.push(...data.servers);
|
||||||
if (data.providerResults?.[provider.id]?.sources) rawSources.push(...data.providerResults[provider.id].sources);
|
if (data.providerResults?.[provider.id]?.sources) rawSources.push(...data.providerResults[provider.id].sources);
|
||||||
if (data.providerResults?.[provider.id] && Array.isArray(data.providerResults[provider.id])) rawSources.push(...data.providerResults[provider.id]);
|
if (data.providerResults?.[provider.id] && Array.isArray(data.providerResults[provider.id])) rawSources.push(...data.providerResults[provider.id]);
|
||||||
|
|
||||||
if (!rawSources.length) return null;
|
if (!rawSources.length) return null;
|
||||||
const results = [];
|
const results = [];
|
||||||
for (const src of rawSources) {
|
for (const src of rawSources) {
|
||||||
if (typeof src !== 'object' || !src) continue;
|
if (typeof src !== 'object' || !src) continue;
|
||||||
let streamUrl = src.url || src.file || src.link;
|
let streamUrl = src.url || src.file || src.link;
|
||||||
if (typeof streamUrl !== 'string') continue;
|
if (typeof streamUrl !== 'string') continue;
|
||||||
if (streamUrl.startsWith('/')) streamUrl = `${BASE_URL}${streamUrl}`;
|
if (streamUrl.startsWith('/')) streamUrl = `${BASE_URL}${streamUrl}`;
|
||||||
const isWorker = streamUrl.includes('omena-puu') || streamUrl.includes('nocach') || streamUrl.includes('?p=');
|
const isWorker = streamUrl.includes('omena-puu') || streamUrl.includes('nocach') || streamUrl.includes('?p=');
|
||||||
const isHls = src.type === 'hls' || streamUrl.includes('m3u8') || isWorker;
|
const isHls = src.type === 'hls' || streamUrl.includes('m3u8') || isWorker;
|
||||||
if (isHls && !streamUrl.includes('m3u8')) streamUrl += streamUrl.includes('?') ? '&format=.m3u8' : '?format=.m3u8';
|
if (isHls && !streamUrl.includes('m3u8')) streamUrl += streamUrl.includes('?') ? '&format=.m3u8' : '?format=.m3u8';
|
||||||
const srcHeaders = (src.headers && typeof src.headers === 'object') ? src.headers : {};
|
const srcHeaders = (src.headers && typeof src.headers === 'object') ? src.headers : {};
|
||||||
results.push({
|
results.push({
|
||||||
url: streamUrl,
|
url: streamUrl,
|
||||||
server: provider.name,
|
server: provider.name,
|
||||||
quality: src.quality || src.label || src.title || "Auto",
|
quality: src.quality || src.label || src.title || "Auto",
|
||||||
type: isHls ? "hls" : "mp4",
|
type: isHls ? "hls" : "mp4",
|
||||||
headers: { "User-Agent": USER_AGENT, "Referer": referer, "Origin": BASE_URL, ...srcHeaders },
|
headers: { "User-Agent": USER_AGENT, "Referer": referer, "Origin": BASE_URL, ...srcHeaders },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStream(args) {
|
export async function getStream(args) {
|
||||||
const { id, s, e, server } = args;
|
const { id, s, e, server } = args;
|
||||||
const type = s != null && e != null ? "tv" : "movie";
|
const type = s != null && e != null ? "tv" : "movie";
|
||||||
const token = await getToken();
|
const token = await getToken();
|
||||||
if (!token) return null;
|
if (!token) return null;
|
||||||
let targets = PROVIDERS;
|
let targets = PROVIDERS;
|
||||||
if (server && server !== 'all') {
|
if (server && server !== 'all') {
|
||||||
const cleanName = server.replace('1Embed - ', '').toLowerCase();
|
const cleanName = server.replace('1Embed - ', '').toLowerCase();
|
||||||
targets = PROVIDERS.filter(p => p.id === cleanName || p.name.toLowerCase().includes(cleanName));
|
targets = PROVIDERS.filter(p => p.id === cleanName || p.name.toLowerCase().includes(cleanName));
|
||||||
if (!targets.length) targets = PROVIDERS;
|
if (!targets.length) targets = PROVIDERS;
|
||||||
}
|
}
|
||||||
const settled = await Promise.allSettled(targets.map(p => fetchProviderStream(p, type, id, s, e, token)));
|
const settled = await Promise.allSettled(targets.map(p => fetchProviderStream(p, type, id, s, e, token)));
|
||||||
const allUrls = [];
|
const allUrls = [];
|
||||||
for (const r of settled) if (r.status === 'fulfilled' && r.value) allUrls.push(...r.value);
|
for (const r of settled) if (r.status === 'fulfilled' && r.value) allUrls.push(...r.value);
|
||||||
return allUrls.length ? { allUrls } : null;
|
return allUrls.length ? { allUrls } : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSources() { return PROVIDERS.map(p => p.name); }
|
export async function getSources() { return PROVIDERS.map(p => p.name); }
|
||||||
@@ -1,112 +1,112 @@
|
|||||||
import { getTmdbInfo, USER_AGENT } from '../utils/helpers.js';
|
import { getTmdbInfo, USER_AGENT } from '../utils/helpers.js';
|
||||||
|
|
||||||
async function extractVoe(voeUrl) {
|
async function extractVoe(voeUrl) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(voeUrl, {
|
const res = await fetch(voeUrl, {
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': USER_AGENT,
|
'User-Agent': USER_AGENT,
|
||||||
'Referer': 'https://anihq.cc/'
|
'Referer': 'https://anihq.cc/'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const mirrorUrl = res.url;
|
const mirrorUrl = res.url;
|
||||||
const html = await res.text();
|
const html = await res.text();
|
||||||
|
|
||||||
const scriptMatch = html.match(/<script type="application\/json">\["(.*?)"\]<\/script>/);
|
const scriptMatch = html.match(/<script type="application\/json">\["(.*?)"\]<\/script>/);
|
||||||
if (!scriptMatch) return null;
|
if (!scriptMatch) return null;
|
||||||
|
|
||||||
let str = scriptMatch[1];
|
let str = scriptMatch[1];
|
||||||
|
|
||||||
str = str.replace(/[a-zA-Z]/g, (c) => {
|
str = str.replace(/[a-zA-Z]/g, (c) => {
|
||||||
return String.fromCharCode((c <= "Z" ? 90 : 122) >= (c = c.charCodeAt(0) + 13) ? c : c - 26);
|
return String.fromCharCode((c <= "Z" ? 90 : 122) >= (c = c.charCodeAt(0) + 13) ? c : c - 26);
|
||||||
});
|
});
|
||||||
|
|
||||||
const junk = ['@$', '^^', '~@', '%?', '*~', '!!', '#&'];
|
const junk = ['@$', '^^', '~@', '%?', '*~', '!!', '#&'];
|
||||||
junk.forEach(j => { str = str.split(j).join(''); });
|
junk.forEach(j => { str = str.split(j).join(''); });
|
||||||
|
|
||||||
str = Buffer.from(str, 'base64').toString('utf-8');
|
str = Buffer.from(str, 'base64').toString('utf-8');
|
||||||
|
|
||||||
let shifted = '';
|
let shifted = '';
|
||||||
for (let i = 0; i < str.length; i++) {
|
for (let i = 0; i < str.length; i++) {
|
||||||
shifted += String.fromCharCode(str.charCodeAt(i) - 3);
|
shifted += String.fromCharCode(str.charCodeAt(i) - 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
let reversed = shifted.split('').reverse().join('');
|
let reversed = shifted.split('').reverse().join('');
|
||||||
|
|
||||||
let finalJsonStr = Buffer.from(reversed, 'base64').toString('utf-8');
|
let finalJsonStr = Buffer.from(reversed, 'base64').toString('utf-8');
|
||||||
|
|
||||||
const finalData = JSON.parse(finalJsonStr);
|
const finalData = JSON.parse(finalJsonStr);
|
||||||
|
|
||||||
if (finalData.file || finalData.source) {
|
if (finalData.file || finalData.source) {
|
||||||
return {
|
return {
|
||||||
url: finalData.file || finalData.source,
|
url: finalData.file || finalData.source,
|
||||||
mirror: mirrorUrl
|
mirror: mirrorUrl
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanSlug(title) {
|
function cleanSlug(title) {
|
||||||
return title
|
return title
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/[^a-z0-9\s]/g, '')
|
.replace(/[^a-z0-9\s]/g, '')
|
||||||
.replace(/\s+/g, '-')
|
.replace(/\s+/g, '-')
|
||||||
.replace(/^-+|-+$/g, '');
|
.replace(/^-+|-+$/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStream({ id, s, e, audio }) {
|
export async function getStream({ id, s, e, audio }) {
|
||||||
if (!s) return null;
|
if (!s) return null;
|
||||||
|
|
||||||
const info = await getTmdbInfo(id, 'tv', s);
|
const info = await getTmdbInfo(id, 'tv', s);
|
||||||
if (!info.isAnime) return null;
|
if (!info.isAnime) return null;
|
||||||
|
|
||||||
const seriesTitle = info.titles.find(t =>
|
const seriesTitle = info.titles.find(t =>
|
||||||
t.toLowerCase() !== 'east blue' &&
|
t.toLowerCase() !== 'east blue' &&
|
||||||
t.length > 3
|
t.length > 3
|
||||||
) || info.titles[0];
|
) || info.titles[0];
|
||||||
|
|
||||||
const cleanTitle = cleanSlug(seriesTitle);
|
const cleanTitle = cleanSlug(seriesTitle);
|
||||||
const typeSuffix = audio === 'dub' ? 'english-dubbed' : 'english-subbed';
|
const typeSuffix = audio === 'dub' ? 'english-dubbed' : 'english-subbed';
|
||||||
const url = `https://anihq.cc/watch/${cleanTitle}-episode-${e}-${typeSuffix}/`;
|
const url = `https://anihq.cc/watch/${cleanTitle}-episode-${e}-${typeSuffix}/`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let pageRes = await fetch(url, { headers: { 'User-Agent': USER_AGENT } });
|
let pageRes = await fetch(url, { headers: { 'User-Agent': USER_AGENT } });
|
||||||
let html = await pageRes.text();
|
let html = await pageRes.text();
|
||||||
|
|
||||||
if (!pageRes.ok) {
|
if (!pageRes.ok) {
|
||||||
const searchUrl = `https://anihq.cc/search?keyword=${encodeURIComponent(seriesTitle)}`;
|
const searchUrl = `https://anihq.cc/search?keyword=${encodeURIComponent(seriesTitle)}`;
|
||||||
const searchRes = await fetch(searchUrl, { headers: { 'User-Agent': USER_AGENT } });
|
const searchRes = await fetch(searchUrl, { headers: { 'User-Agent': USER_AGENT } });
|
||||||
const searchHtml = await searchRes.text();
|
const searchHtml = await searchRes.text();
|
||||||
const searchRegex = new RegExp(`href="(https://anihq.cc/watch/[^"]+-episode-${e}-${typeSuffix}/)"`, 'i');
|
const searchRegex = new RegExp(`href="(https://anihq.cc/watch/[^"]+-episode-${e}-${typeSuffix}/)"`, 'i');
|
||||||
const match = searchHtml.match(searchRegex);
|
const match = searchHtml.match(searchRegex);
|
||||||
if (match) {
|
if (match) {
|
||||||
const retryRes = await fetch(match[1], { headers: { 'User-Agent': USER_AGENT } });
|
const retryRes = await fetch(match[1], { headers: { 'User-Agent': USER_AGENT } });
|
||||||
html = await retryRes.text();
|
html = await retryRes.text();
|
||||||
} else return null;
|
} else return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const iframeMatch = html.match(/iframe[^>]+src=["'](https:\/\/(?:[a-z0-9.]+|ellenpoliticalfollow\.com)\/e\/[a-zA-Z0-9]+)["']/i);
|
const iframeMatch = html.match(/iframe[^>]+src=["'](https:\/\/(?:[a-z0-9.]+|ellenpoliticalfollow\.com)\/e\/[a-zA-Z0-9]+)["']/i);
|
||||||
if (!iframeMatch) return null;
|
if (!iframeMatch) return null;
|
||||||
|
|
||||||
const result = await extractVoe(iframeMatch[1]);
|
const result = await extractVoe(iframeMatch[1]);
|
||||||
|
|
||||||
if (result && result.url) {
|
if (result && result.url) {
|
||||||
const mirrorOrigin = new URL(result.mirror).origin;
|
const mirrorOrigin = new URL(result.mirror).origin;
|
||||||
return {
|
return {
|
||||||
url: result.url,
|
url: result.url,
|
||||||
headers: {
|
headers: {
|
||||||
'Referer': mirrorOrigin + '/',
|
'Referer': mirrorOrigin + '/',
|
||||||
'User-Agent': USER_AGENT,
|
'User-Agent': USER_AGENT,
|
||||||
'Origin': mirrorOrigin
|
'Origin': mirrorOrigin
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -1,81 +1,81 @@
|
|||||||
import { getTmdbInfo } from '../utils/helpers.js';
|
import { getTmdbInfo } from '../utils/helpers.js';
|
||||||
|
|
||||||
const BASE_URL = "https://anineko.to";
|
const BASE_URL = "https://anineko.to";
|
||||||
|
|
||||||
function cleanTitle(t) { return t ? t.toLowerCase().replace(/[^a-z0-9]/g, '') : ''; }
|
function cleanTitle(t) { return t ? t.toLowerCase().replace(/[^a-z0-9]/g, '') : ''; }
|
||||||
|
|
||||||
function decodeEntities(str) {
|
function decodeEntities(str) {
|
||||||
return str.replace(/&(nbsp|amp|quot|lt|gt);/g, (m, e) => ({ "nbsp": " ", "amp": "&", "quot": "\"", "lt": "<", "gt": ">" })[e]).replace(/&#(\d+);/gi, (m, n) => String.fromCharCode(parseInt(n, 10)));
|
return str.replace(/&(nbsp|amp|quot|lt|gt);/g, (m, e) => ({ "nbsp": " ", "amp": "&", "quot": "\"", "lt": "<", "gt": ">" })[e]).replace(/&#(\d+);/gi, (m, n) => String.fromCharCode(parseInt(n, 10)));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function search(query) {
|
async function search(query) {
|
||||||
try {
|
try {
|
||||||
const html = await fetchText(`${BASE_URL}/browser?keyword=${encodeURIComponent(query)}`, { signal: AbortSignal.timeout(6000) });
|
const html = await fetchText(`${BASE_URL}/browser?keyword=${encodeURIComponent(query)}`, { signal: AbortSignal.timeout(6000) });
|
||||||
const results = [];
|
const results = [];
|
||||||
let idx = 0;
|
let idx = 0;
|
||||||
while ((idx = html.indexOf('nv-anime-thumb', idx)) !== -1) {
|
while ((idx = html.indexOf('nv-anime-thumb', idx)) !== -1) {
|
||||||
const startHref = html.lastIndexOf('<a', idx);
|
const startHref = html.lastIndexOf('<a', idx);
|
||||||
if (startHref === -1) { idx += 14; continue; }
|
if (startHref === -1) { idx += 14; continue; }
|
||||||
const hrefMatch = html.slice(startHref, html.indexOf('>', startHref)).match(/href=["']([^"']+)["']/i);
|
const hrefMatch = html.slice(startHref, html.indexOf('>', startHref)).match(/href=["']([^"']+)["']/i);
|
||||||
if (!hrefMatch) { idx += 14; continue; }
|
if (!hrefMatch) { idx += 14; continue; }
|
||||||
const slug = hrefMatch[1].split('/').pop();
|
const slug = hrefMatch[1].split('/').pop();
|
||||||
const h3Start = html.indexOf('nv-anime-title', idx);
|
const h3Start = html.indexOf('nv-anime-title', idx);
|
||||||
if (h3Start !== -1) {
|
if (h3Start !== -1) {
|
||||||
const contentStart = html.indexOf('>', h3Start) + 1;
|
const contentStart = html.indexOf('>', h3Start) + 1;
|
||||||
const contentEnd = html.indexOf('<', contentStart);
|
const contentEnd = html.indexOf('<', contentStart);
|
||||||
if (contentEnd !== -1) results.push({ slug, text: html.slice(contentStart, contentEnd).trim() });
|
if (contentEnd !== -1) results.push({ slug, text: html.slice(contentStart, contentEnd).trim() });
|
||||||
}
|
}
|
||||||
idx += 14;
|
idx += 14;
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
} catch { return []; }
|
} catch { return []; }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function extractHls(embedUrl) {
|
async function extractHls(embedUrl) {
|
||||||
try {
|
try {
|
||||||
const html = await fetchText(embedUrl, { headers: { Referer: `${BASE_URL}/` }, signal: AbortSignal.timeout(6000) });
|
const html = await fetchText(embedUrl, { headers: { Referer: `${BASE_URL}/` }, signal: AbortSignal.timeout(6000) });
|
||||||
const m = html.match(/const\s+src\s*=\s*["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/i) || html.match(/file\s*:\s*["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/i) || html.match(/["'](https?:\/\/[^"']+\/master\.m3u8[^"']*)["']/i) || html.match(/["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/i);
|
const m = html.match(/const\s+src\s*=\s*["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/i) || html.match(/file\s*:\s*["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/i) || html.match(/["'](https?:\/\/[^"']+\/master\.m3u8[^"']*)["']/i) || html.match(/["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/i);
|
||||||
if (m) return decodeEntities(m[1]);
|
if (m) return decodeEntities(m[1]);
|
||||||
} catch { }
|
} catch { }
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStream({ id, s, e, audio }) {
|
export async function getStream({ id, s, e, audio }) {
|
||||||
try {
|
try {
|
||||||
const info = await getTmdbInfo(id, s ? 'tv' : 'movie', s);
|
const info = await getTmdbInfo(id, s ? 'tv' : 'movie', s);
|
||||||
if (!info || !info.isAnime) return null;
|
if (!info || !info.isAnime) return null;
|
||||||
let seriesSlug = null;
|
let seriesSlug = null;
|
||||||
for (const title of info.titles) {
|
for (const title of info.titles) {
|
||||||
const results = await search(title);
|
const results = await search(title);
|
||||||
const targetClean = cleanTitle(title);
|
const targetClean = cleanTitle(title);
|
||||||
for (const r of results) if (cleanTitle(r.text) === targetClean || r.text.toLowerCase().includes(title.toLowerCase())) { seriesSlug = r.slug; break; }
|
for (const r of results) if (cleanTitle(r.text) === targetClean || r.text.toLowerCase().includes(title.toLowerCase())) { seriesSlug = r.slug; break; }
|
||||||
if (seriesSlug) break;
|
if (seriesSlug) break;
|
||||||
}
|
}
|
||||||
if (!seriesSlug && info.titles.length) {
|
if (!seriesSlug && info.titles.length) {
|
||||||
const fSearch = await search(info.titles[0]);
|
const fSearch = await search(info.titles[0]);
|
||||||
if (fSearch.length) seriesSlug = fSearch[0].slug;
|
if (fSearch.length) seriesSlug = fSearch[0].slug;
|
||||||
}
|
}
|
||||||
if (!seriesSlug) return null;
|
if (!seriesSlug) return null;
|
||||||
const watchHtml = await fetchText(`${BASE_URL}/watch/${seriesSlug}/ep-${e || 1}`, { headers: { Referer: `${BASE_URL}/watch/${seriesSlug}` }, signal: AbortSignal.timeout(6000) });
|
const watchHtml = await fetchText(`${BASE_URL}/watch/${seriesSlug}/ep-${e || 1}`, { headers: { Referer: `${BASE_URL}/watch/${seriesSlug}` }, signal: AbortSignal.timeout(6000) });
|
||||||
const byAudio = { sub: [], dub: [] };
|
const byAudio = { sub: [], dub: [] };
|
||||||
for (const panel of watchHtml.matchAll(/<div\b[^>]*class=["'][^"']*nv-server-grid[^"']*["'][^>]*data-id=["']([^"']+)["'][^>]*>([\s\S]*?)(?=<div\b[^>]*class=["'][^"']*nv-server-grid|$)/gi)) {
|
for (const panel of watchHtml.matchAll(/<div\b[^>]*class=["'][^"']*nv-server-grid[^"']*["'][^>]*data-id=["']([^"']+)["'][^>]*>([\s\S]*?)(?=<div\b[^>]*class=["'][^"']*nv-server-grid|$)/gi)) {
|
||||||
const panelAudio = panel[1].toLowerCase().includes("dub") ? "dub" : "sub";
|
const panelAudio = panel[1].toLowerCase().includes("dub") ? "dub" : "sub";
|
||||||
for (const btn of panel[2].matchAll(/data-video=["']([^"']+)["']/gi)) byAudio[panelAudio].push(decodeEntities(btn[1]));
|
for (const btn of panel[2].matchAll(/data-video=["']([^"']+)["']/gi)) byAudio[panelAudio].push(decodeEntities(btn[1]));
|
||||||
}
|
}
|
||||||
const audiosToTry = audio === "all" ? ["sub", "dub"] : (audio === "dub" ? ["dub", "sub"] : ["sub", "dub"]);
|
const audiosToTry = audio === "all" ? ["sub", "dub"] : (audio === "dub" ? ["dub", "sub"] : ["sub", "dub"]);
|
||||||
const allUrls = [];
|
const allUrls = [];
|
||||||
for (const aud of audiosToTry) {
|
for (const aud of audiosToTry) {
|
||||||
for (const embed of byAudio[aud]) {
|
for (const embed of byAudio[aud]) {
|
||||||
const hls = await extractHls(embed);
|
const hls = await extractHls(embed);
|
||||||
if (hls) allUrls.push({ url: hls, type: "hls", audio: aud, server: "AniNeko", skipProxy: false });
|
if (hls) allUrls.push({ url: hls, type: "hls", audio: aud, server: "AniNeko", skipProxy: false });
|
||||||
}
|
}
|
||||||
if (allUrls.length) break;
|
if (allUrls.length) break;
|
||||||
}
|
}
|
||||||
return allUrls.length ? { allUrls } : null;
|
return allUrls.length ? { allUrls } : null;
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSources(args) {
|
export async function getSources(args) {
|
||||||
const stream = await getStream(args);
|
const stream = await getStream(args);
|
||||||
return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server))] : [];
|
return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server))] : [];
|
||||||
}
|
}
|
||||||
@@ -1,70 +1,70 @@
|
|||||||
import { getTmdbInfo, fetchJson } from '../utils/helpers.js';
|
import { getTmdbInfo, fetchJson } from '../utils/helpers.js';
|
||||||
|
|
||||||
const BASE_URL = "https://ani.pm";
|
const BASE_URL = "https://ani.pm";
|
||||||
|
|
||||||
function cleanTitle(t) { return t ? t.toLowerCase().replace(/[^a-z0-9]/g, '') : ''; }
|
function cleanTitle(t) { return t ? t.toLowerCase().replace(/[^a-z0-9]/g, '') : ''; }
|
||||||
|
|
||||||
async function searchCatalog(query) {
|
async function searchCatalog(query) {
|
||||||
try {
|
try {
|
||||||
const data = await fetchJson(`${BASE_URL}/api/anime/catalog?q=${encodeURIComponent(query)}&page=1`, { signal: AbortSignal.timeout(6000) });
|
const data = await fetchJson(`${BASE_URL}/api/anime/catalog?q=${encodeURIComponent(query)}&page=1`, { signal: AbortSignal.timeout(6000) });
|
||||||
return Array.isArray(data?.items) ? data.items : [];
|
return Array.isArray(data?.items) ? data.items : [];
|
||||||
} catch { return []; }
|
} catch { return []; }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveEntry(titles, year) {
|
async function resolveEntry(titles, year) {
|
||||||
const orderedTitles = titles.length > 1 ? [...titles].reverse() : titles;
|
const orderedTitles = titles.length > 1 ? [...titles].reverse() : titles;
|
||||||
let best = null, bestScore = -1;
|
let best = null, bestScore = -1;
|
||||||
for (const title of orderedTitles) {
|
for (const title of orderedTitles) {
|
||||||
const items = await searchCatalog(title);
|
const items = await searchCatalog(title);
|
||||||
const targetClean = cleanTitle(title);
|
const targetClean = cleanTitle(title);
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const itemTitles = [item.title, item.native].filter(Boolean).map(cleanTitle);
|
const itemTitles = [item.title, item.native].filter(Boolean).map(cleanTitle);
|
||||||
let score = 0;
|
let score = 0;
|
||||||
if (itemTitles.some(t => t === targetClean)) score += 5;
|
if (itemTitles.some(t => t === targetClean)) score += 5;
|
||||||
else if (itemTitles.some(t => t.includes(targetClean) || targetClean.includes(t))) score += 3;
|
else if (itemTitles.some(t => t.includes(targetClean) || targetClean.includes(t))) score += 3;
|
||||||
else continue;
|
else continue;
|
||||||
if (item.type === 'TV') score += 4;
|
if (item.type === 'TV') score += 4;
|
||||||
else if (item.type === 'Movie' || item.type === 'Special' || item.type === 'ONA') score -= 2;
|
else if (item.type === 'Movie' || item.type === 'Special' || item.type === 'ONA') score -= 2;
|
||||||
if (year && item.year) {
|
if (year && item.year) {
|
||||||
const diff = Math.abs(item.year - year);
|
const diff = Math.abs(item.year - year);
|
||||||
if (diff === 0) score += 3;
|
if (diff === 0) score += 3;
|
||||||
else if (diff === 1) score += 1;
|
else if (diff === 1) score += 1;
|
||||||
else if (diff > 2) score -= 3;
|
else if (diff > 2) score -= 3;
|
||||||
}
|
}
|
||||||
if (score > bestScore) { bestScore = score; best = item; }
|
if (score > bestScore) { bestScore = score; best = item; }
|
||||||
}
|
}
|
||||||
if (bestScore >= 9) break;
|
if (bestScore >= 9) break;
|
||||||
}
|
}
|
||||||
return best;
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStream({ id, s, e, audio }) {
|
export async function getStream({ id, s, e, audio }) {
|
||||||
try {
|
try {
|
||||||
const info = await getTmdbInfo(id, s ? 'tv' : 'movie', s);
|
const info = await getTmdbInfo(id, s ? 'tv' : 'movie', s);
|
||||||
if (!info || !info.isAnime) return null;
|
if (!info || !info.isAnime) return null;
|
||||||
const entry = await resolveEntry(info.titles, info.year);
|
const entry = await resolveEntry(info.titles, info.year);
|
||||||
if (!entry) return null;
|
if (!entry) return null;
|
||||||
const episodeNum = e || 1;
|
const episodeNum = e || 1;
|
||||||
const params = new URLSearchParams({ title: entry.title || '', ep: String(episodeNum) });
|
const params = new URLSearchParams({ title: entry.title || '', ep: String(episodeNum) });
|
||||||
if (entry.year) params.set('year', entry.year);
|
if (entry.year) params.set('year', entry.year);
|
||||||
if (entry.anilistId) params.set('anilistId', entry.anilistId);
|
if (entry.anilistId) params.set('anilistId', entry.anilistId);
|
||||||
if (entry.malId) params.set('malId', entry.malId);
|
if (entry.malId) params.set('malId', entry.malId);
|
||||||
const servers = await fetchJson(`${BASE_URL}/api/anime/src/servers?${params}`, { signal: AbortSignal.timeout(10000) });
|
const servers = await fetchJson(`${BASE_URL}/api/anime/src/servers?${params}`, { signal: AbortSignal.timeout(10000) });
|
||||||
if (!servers) return null;
|
if (!servers) return null;
|
||||||
const audiosToTry = audio === "all" ? ["sub", "dub"] : (audio === "dub" ? ["dub", "sub"] : ["sub", "dub"]);
|
const audiosToTry = audio === "all" ? ["sub", "dub"] : (audio === "dub" ? ["dub", "sub"] : ["sub", "dub"]);
|
||||||
for (const aud of audiosToTry) {
|
for (const aud of audiosToTry) {
|
||||||
const list = servers[aud];
|
const list = servers[aud];
|
||||||
if (!Array.isArray(list) || !list.length) continue;
|
if (!Array.isArray(list) || !list.length) continue;
|
||||||
const allUrls = list.filter(srv => (srv.kind === "hls" || srv.kind === "file") && srv.url)
|
const allUrls = list.filter(srv => (srv.kind === "hls" || srv.kind === "file") && srv.url)
|
||||||
.sort((a, b) => (b.priority || 0) - (a.priority || 0))
|
.sort((a, b) => (b.priority || 0) - (a.priority || 0))
|
||||||
.map(srv => ({ url: srv.url.startsWith('/') ? `${BASE_URL}${srv.url}` : srv.url, type: srv.kind === "hls" ? "hls" : "mp4", audio: aud, server: srv.provider ? `AniPM-${srv.provider}` : "AniPM", headers: { Referer: `${BASE_URL}/`, Origin: BASE_URL }, skipProxy: false }));
|
.map(srv => ({ url: srv.url.startsWith('/') ? `${BASE_URL}${srv.url}` : srv.url, type: srv.kind === "hls" ? "hls" : "mp4", audio: aud, server: srv.provider ? `AniPM-${srv.provider}` : "AniPM", headers: { Referer: `${BASE_URL}/`, Origin: BASE_URL }, skipProxy: false }));
|
||||||
if (allUrls.length) return { allUrls };
|
if (allUrls.length) return { allUrls };
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSources(args) {
|
export async function getSources(args) {
|
||||||
const stream = await getStream(args);
|
const stream = await getStream(args);
|
||||||
return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server))] : [];
|
return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server))] : [];
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
import { fetchJson } from '../utils/helpers.js';
|
import { fetchJson } from '../utils/helpers.js';
|
||||||
|
|
||||||
const API_BASE = 'https://itjiocunahckqxcnzpoy.supabase.co/functions/v1';
|
const API_BASE = 'https://itjiocunahckqxcnzpoy.supabase.co/functions/v1';
|
||||||
const APIKEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Iml0amlvY3VuYWhja3F4Y256cG95Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzczMzM1MjQsImV4cCI6MjA5MjkwOTUyNH0.9x9ykdHAzrv_GSvnawPeQaOxQeh3sZg0QAh4u9VOF4M';
|
const APIKEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Iml0amlvY3VuYWhja3F4Y256cG95Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzczMzM1MjQsImV4cCI6MjA5MjkwOTUyNH0.9x9ykdHAzrv_GSvnawPeQaOxQeh3sZg0QAh4u9VOF4M';
|
||||||
const HEADERS = { 'apikey': APIKEY, 'authorization': `Bearer ${APIKEY}`, 'content-type': 'application/json', 'origin': 'https://flaxmovies.xyz', 'referer': 'https://flaxmovies.xyz/' };
|
const HEADERS = { 'apikey': APIKEY, 'authorization': `Bearer ${APIKEY}`, 'content-type': 'application/json', 'origin': 'https://flaxmovies.xyz', 'referer': 'https://flaxmovies.xyz/' };
|
||||||
|
|
||||||
export async function getStream({ id, s, e }) {
|
export async function getStream({ id, s, e }) {
|
||||||
try {
|
try {
|
||||||
const isMovie = !s;
|
const isMovie = !s;
|
||||||
const data = await fetchJson(isMovie ? `${API_BASE}/get-movie` : `${API_BASE}/get-tv`, { method: 'POST', headers: HEADERS, body: JSON.stringify(isMovie ? { id: String(id) } : { id: String(id), season: Number(s), episode: Number(e) }), signal: AbortSignal.timeout(15000) });
|
const data = await fetchJson(isMovie ? `${API_BASE}/get-movie` : `${API_BASE}/get-tv`, { method: 'POST', headers: HEADERS, body: JSON.stringify(isMovie ? { id: String(id) } : { id: String(id), season: Number(s), episode: Number(e) }), signal: AbortSignal.timeout(15000) });
|
||||||
if (!data?.signed_url) return null;
|
if (!data?.signed_url) return null;
|
||||||
return { allUrls: [{ url: data.signed_url, headers: { 'Referer': 'https://flaxmovies.xyz/', 'Origin': 'https://flaxmovies.xyz' } }] };
|
return { allUrls: [{ url: data.signed_url, headers: { 'Referer': 'https://flaxmovies.xyz/', 'Origin': 'https://flaxmovies.xyz' } }] };
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
@@ -1,44 +1,44 @@
|
|||||||
import { fetchText, fetchJson, USER_AGENT, getTmdbInfo } from '../utils/helpers.js';
|
import { fetchText, fetchJson, USER_AGENT, getTmdbInfo } from '../utils/helpers.js';
|
||||||
|
|
||||||
const BASE_URL = 'https://fsharetv.cc';
|
const BASE_URL = 'https://fsharetv.cc';
|
||||||
const HEADERS = { 'User-Agent': USER_AGENT, 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'en-US,en;q=0.9', 'Referer': `${BASE_URL}/` };
|
const HEADERS = { 'User-Agent': USER_AGENT, 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'en-US,en;q=0.9', 'Referer': `${BASE_URL}/` };
|
||||||
const API_HEADERS = { ...HEADERS, 'Accept': 'application/json, */*; q=0.01', 'X-Requested-With': 'XMLHttpRequest' };
|
const API_HEADERS = { ...HEADERS, 'Accept': 'application/json, */*; q=0.01', 'X-Requested-With': 'XMLHttpRequest' };
|
||||||
|
|
||||||
export async function getStream({ id, s }) {
|
export async function getStream({ id, s }) {
|
||||||
if (s != null) return null;
|
if (s != null) return null;
|
||||||
try {
|
try {
|
||||||
const info = await getTmdbInfo(id, 'movie');
|
const info = await getTmdbInfo(id, 'movie');
|
||||||
if (!info?.imdbId) return null;
|
if (!info?.imdbId) return null;
|
||||||
const html = await fetchText(`${BASE_URL}/movie/${info.imdbId}`, { headers: HEADERS, signal: AbortSignal.timeout(8000) });
|
const html = await fetchText(`${BASE_URL}/movie/${info.imdbId}`, { headers: HEADERS, signal: AbortSignal.timeout(8000) });
|
||||||
let watchPath = null;
|
let watchPath = null;
|
||||||
const match = html.match(/href="(\/w\/[^"]+)"/);
|
const match = html.match(/href="(\/w\/[^"]+)"/);
|
||||||
if (match) watchPath = match[1];
|
if (match) watchPath = match[1];
|
||||||
if (!watchPath) return null;
|
if (!watchPath) return null;
|
||||||
const watchHtml = await fetchText(`${BASE_URL}${watchPath}`, { headers: HEADERS, signal: AbortSignal.timeout(8000) });
|
const watchHtml = await fetchText(`${BASE_URL}${watchPath}`, { headers: HEADERS, signal: AbortSignal.timeout(8000) });
|
||||||
let sourceId = null;
|
let sourceId = null;
|
||||||
const sidMatch = watchHtml.match(/(?:source_id|file_id|setSource)[\s=:\('"]+([^'"\)]+)/i);
|
const sidMatch = watchHtml.match(/(?:source_id|file_id|setSource)[\s=:\('"]+([^'"\)]+)/i);
|
||||||
if (sidMatch) sourceId = sidMatch[1];
|
if (sidMatch) sourceId = sidMatch[1];
|
||||||
if (!sourceId) return null;
|
if (!sourceId) return null;
|
||||||
const json = await fetchJson(`${BASE_URL}/api/file/${sourceId}/source?trailer=Png81APqcxU&type=watch`, { headers: { ...API_HEADERS, 'Referer': `${BASE_URL}${watchPath}` }, signal: AbortSignal.timeout(8000) });
|
const json = await fetchJson(`${BASE_URL}/api/file/${sourceId}/source?trailer=Png81APqcxU&type=watch`, { headers: { ...API_HEADERS, 'Referer': `${BASE_URL}${watchPath}` }, signal: AbortSignal.timeout(8000) });
|
||||||
if (json.status !== 'ok') return null;
|
if (json.status !== 'ok') return null;
|
||||||
const groups = [];
|
const groups = [];
|
||||||
if (Array.isArray(json.data?.file?.sources)) groups.push(json.data.file.sources);
|
if (Array.isArray(json.data?.file?.sources)) groups.push(json.data.file.sources);
|
||||||
if (Array.isArray(json.data?.file?.alternatives)) json.data.file.alternatives.forEach(g => Array.isArray(g) && groups.push(g));
|
if (Array.isArray(json.data?.file?.alternatives)) json.data.file.alternatives.forEach(g => Array.isArray(g) && groups.push(g));
|
||||||
const out = [], seen = new Set();
|
const out = [], seen = new Set();
|
||||||
for (const group of groups) {
|
for (const group of groups) {
|
||||||
for (const item of group.filter(st => st?.src).sort((a, b) => parseInt(b.quality) - parseInt(a.quality))) {
|
for (const item of group.filter(st => st?.src).sort((a, b) => parseInt(b.quality) - parseInt(a.quality))) {
|
||||||
const url = item.src.startsWith('http') ? item.src : `${BASE_URL}${item.src}`;
|
const url = item.src.startsWith('http') ? item.src : `${BASE_URL}${item.src}`;
|
||||||
if (seen.has(url)) continue;
|
if (seen.has(url)) continue;
|
||||||
seen.add(url);
|
seen.add(url);
|
||||||
out.push({ url, quality: parseInt(item.quality) || 0, label: item.label || null });
|
out.push({ url, quality: parseInt(item.quality) || 0, label: item.label || null });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out.sort((a, b) => b.quality - a.quality);
|
out.sort((a, b) => b.quality - a.quality);
|
||||||
return out.length ? { allUrls: out.map(u => ({ url: u.url, headers: HEADERS, skipProxy: false })) } : null;
|
return out.length ? { allUrls: out.map(u => ({ url: u.url, headers: HEADERS, skipProxy: false })) } : null;
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSources(args) {
|
export async function getSources(args) {
|
||||||
const stream = await getStream(args);
|
const stream = await getStream(args);
|
||||||
return stream?.allUrls ? stream.allUrls.map(u => u.url) : [];
|
return stream?.allUrls ? stream.allUrls.map(u => u.url) : [];
|
||||||
}
|
}
|
||||||
@@ -1,47 +1,47 @@
|
|||||||
import { fetchText, fetchJson, USER_AGENT, getTmdbInfo } from '../utils/helpers.js';
|
import { fetchText, fetchJson, USER_AGENT, getTmdbInfo } from '../utils/helpers.js';
|
||||||
|
|
||||||
const BASE_URL = 'https://www.fsonic.net';
|
const BASE_URL = 'https://www.fsonic.net';
|
||||||
const FSHARE_BASE = 'https://fsharetv.co';
|
const FSHARE_BASE = 'https://fsharetv.co';
|
||||||
const HEADERS = { 'User-Agent': USER_AGENT, 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'en-US,en;q=0.9' };
|
const HEADERS = { 'User-Agent': USER_AGENT, 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'en-US,en;q=0.9' };
|
||||||
|
|
||||||
export async function getStream({ id, s }) {
|
export async function getStream({ id, s }) {
|
||||||
if (s != null) return null;
|
if (s != null) return null;
|
||||||
try {
|
try {
|
||||||
const info = await getTmdbInfo(id, 'movie');
|
const info = await getTmdbInfo(id, 'movie');
|
||||||
if (!info?.titles?.length) return null;
|
if (!info?.titles?.length) return null;
|
||||||
let watchSlug = null;
|
let watchSlug = null;
|
||||||
for (const title of info.titles) {
|
for (const title of info.titles) {
|
||||||
try {
|
try {
|
||||||
const html = await fetchText(`${BASE_URL}/movie/search/${encodeURIComponent(title)}`, { headers: HEADERS, signal: AbortSignal.timeout(8000) });
|
const html = await fetchText(`${BASE_URL}/movie/search/${encodeURIComponent(title)}`, { headers: HEADERS, signal: AbortSignal.timeout(8000) });
|
||||||
let idx = 0, found = null;
|
let idx = 0, found = null;
|
||||||
const yearStr = String(info.year);
|
const yearStr = String(info.year);
|
||||||
while ((idx = html.indexOf('href="/watch/', idx)) !== -1) {
|
while ((idx = html.indexOf('href="/watch/', idx)) !== -1) {
|
||||||
const start = idx + 6;
|
const start = idx + 6;
|
||||||
const end = html.indexOf('"', start);
|
const end = html.indexOf('"', start);
|
||||||
const link = html.slice(start, end);
|
const link = html.slice(start, end);
|
||||||
if (!found) found = link;
|
if (!found) found = link;
|
||||||
if (link.includes(yearStr)) { found = link; break; }
|
if (link.includes(yearStr)) { found = link; break; }
|
||||||
idx = end;
|
idx = end;
|
||||||
}
|
}
|
||||||
if (found) { watchSlug = found; break; }
|
if (found) { watchSlug = found; break; }
|
||||||
} catch { }
|
} catch { }
|
||||||
}
|
}
|
||||||
if (!watchSlug) return null;
|
if (!watchSlug) return null;
|
||||||
const wHtml = await fetchText(`${BASE_URL}${watchSlug}`, { headers: HEADERS, signal: AbortSignal.timeout(8000) });
|
const wHtml = await fetchText(`${BASE_URL}${watchSlug}`, { headers: HEADERS, signal: AbortSignal.timeout(8000) });
|
||||||
let token = null, trailer = null;
|
let token = null, trailer = null;
|
||||||
const match = wHtml.match(/init\('([^']+)',\s*(?:'[^']*',\s*)?'([^']+)'/);
|
const match = wHtml.match(/init\('([^']+)',\s*(?:'[^']*',\s*)?'([^']+)'/);
|
||||||
if (match) { token = match[1]; trailer = match[2]; }
|
if (match) { token = match[1]; trailer = match[2]; }
|
||||||
if (!token) return null;
|
if (!token) return null;
|
||||||
const json = await fetchJson(`${BASE_URL}/api/source/${token}?trailer=${trailer}&type=watch`, { headers: { ...HEADERS, 'Accept': 'application/json, text/plain, */*', 'Referer': `${BASE_URL}${watchSlug}` }, signal: AbortSignal.timeout(8000) });
|
const json = await fetchJson(`${BASE_URL}/api/source/${token}?trailer=${trailer}&type=watch`, { headers: { ...HEADERS, 'Accept': 'application/json, text/plain, */*', 'Referer': `${BASE_URL}${watchSlug}` }, signal: AbortSignal.timeout(8000) });
|
||||||
if (json.status !== 'ok') return null;
|
if (json.status !== 'ok') return null;
|
||||||
const allGroups = [];
|
const allGroups = [];
|
||||||
if (json.data?.file?.sources?.length) allGroups.push(json.data.file.sources);
|
if (json.data?.file?.sources?.length) allGroups.push(json.data.file.sources);
|
||||||
if (json.data?.file?.alternatives) for (const g of json.data.file.alternatives) if (g?.length) allGroups.push(g);
|
if (json.data?.file?.alternatives) for (const g of json.data.file.alternatives) if (g?.length) allGroups.push(g);
|
||||||
const urls = new Set();
|
const urls = new Set();
|
||||||
for (const group of allGroups) {
|
for (const group of allGroups) {
|
||||||
const best = [...group].filter(st => st?.src).sort((a, b) => parseInt(b.quality) - parseInt(a.quality))[0];
|
const best = [...group].filter(st => st?.src).sort((a, b) => parseInt(b.quality) - parseInt(a.quality))[0];
|
||||||
if (best) urls.add(best.src.startsWith('http') ? best.src : `${FSHARE_BASE}${best.src}`);
|
if (best) urls.add(best.src.startsWith('http') ? best.src : `${FSHARE_BASE}${best.src}`);
|
||||||
}
|
}
|
||||||
return urls.size ? { allUrls: [...urls].map(url => ({ url, headers: { ...HEADERS, 'Referer': `${FSHARE_BASE}/` }, skipProxy: false })) } : null;
|
return urls.size ? { allUrls: [...urls].map(url => ({ url, headers: { ...HEADERS, 'Referer': `${FSHARE_BASE}/` }, skipProxy: false })) } : null;
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
@@ -1,46 +1,46 @@
|
|||||||
import { fetchText, USER_AGENT, getTmdbInfo } from '../utils/helpers.js';
|
import { fetchText, USER_AGENT, getTmdbInfo } from '../utils/helpers.js';
|
||||||
|
|
||||||
const ORIGIN = 'https://www3.fsonline.app';
|
const ORIGIN = 'https://www3.fsonline.app';
|
||||||
const AJAX_URL = `${ORIGIN}/wp-admin/admin-ajax.php`;
|
const AJAX_URL = `${ORIGIN}/wp-admin/admin-ajax.php`;
|
||||||
const HEADERS = { 'User-Agent': USER_AGENT, 'Origin': ORIGIN, 'Referer': `${ORIGIN}/` };
|
const HEADERS = { 'User-Agent': USER_AGENT, 'Origin': ORIGIN, 'Referer': `${ORIGIN}/` };
|
||||||
|
|
||||||
export async function getStream({ id, s, e }) {
|
export async function getStream({ id, s, e }) {
|
||||||
try {
|
try {
|
||||||
const isTv = s != null && e != null;
|
const isTv = s != null && e != null;
|
||||||
const info = await getTmdbInfo(id, isTv ? 'tv' : 'movie');
|
const info = await getTmdbInfo(id, isTv ? 'tv' : 'movie');
|
||||||
if (!info?.titles?.length) return null;
|
if (!info?.titles?.length) return null;
|
||||||
const searchHtml = await fetchText(`${ORIGIN}/?s=${encodeURIComponent(info.titles[0] + (info.year ? ' ' + info.year : ''))}`, { headers: HEADERS });
|
const searchHtml = await fetchText(`${ORIGIN}/?s=${encodeURIComponent(info.titles[0] + (info.year ? ' ' + info.year : ''))}`, { headers: HEADERS });
|
||||||
const typeFolder = isTv ? 'seriale' : 'film';
|
const typeFolder = isTv ? 'seriale' : 'film';
|
||||||
const linkMatch = searchHtml.match(new RegExp(`href=["'](https?://www3\\.fsonline\\.app/${typeFolder}/([^"'/]+)/)["']`, 'i'));
|
const linkMatch = searchHtml.match(new RegExp(`href=["'](https?://www3\\.fsonline\\.app/${typeFolder}/([^"'/]+)/)["']`, 'i'));
|
||||||
if (!linkMatch) return null;
|
if (!linkMatch) return null;
|
||||||
const targetPageUrl = isTv ? `${ORIGIN}/episoade/${linkMatch[2].replace(/-\d{4}$/, '')}-sezonul-${s}-episodul-${e}/` : linkMatch[1];
|
const targetPageUrl = isTv ? `${ORIGIN}/episoade/${linkMatch[2].replace(/-\d{4}$/, '')}-sezonul-${s}-episodul-${e}/` : linkMatch[1];
|
||||||
const pageHtml = await fetchText(targetPageUrl, { headers: HEADERS });
|
const pageHtml = await fetchText(targetPageUrl, { headers: HEADERS });
|
||||||
const movieId = (pageHtml.match(/movie-id=['"]([^'"]+)['"]/) || pageHtml.match(/movie-id=([^ >]+)/))?.[1];
|
const movieId = (pageHtml.match(/movie-id=['"]([^'"]+)['"]/) || pageHtml.match(/movie-id=([^ >]+)/))?.[1];
|
||||||
if (!movieId) return null;
|
if (!movieId) return null;
|
||||||
const ajaxHtml = await fetchText(AJAX_URL, { method: 'POST', headers: { ...HEADERS, 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest', 'Referer': targetPageUrl }, body: `action=lazy_player&movieID=${movieId}` });
|
const ajaxHtml = await fetchText(AJAX_URL, { method: 'POST', headers: { ...HEADERS, 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest', 'Referer': targetPageUrl }, body: `action=lazy_player&movieID=${movieId}` });
|
||||||
const results = [];
|
const results = [];
|
||||||
let idx = 0;
|
let idx = 0;
|
||||||
while ((idx = ajaxHtml.indexOf('data-vs="', idx)) !== -1) {
|
while ((idx = ajaxHtml.indexOf('data-vs="', idx)) !== -1) {
|
||||||
const embedStart = idx + 9;
|
const embedStart = idx + 9;
|
||||||
const embedEnd = ajaxHtml.indexOf('"', embedStart);
|
const embedEnd = ajaxHtml.indexOf('"', embedStart);
|
||||||
const embedUrl = ajaxHtml.slice(embedStart, embedEnd);
|
const embedUrl = ajaxHtml.slice(embedStart, embedEnd);
|
||||||
const spanStart = ajaxHtml.indexOf('<span>', embedEnd);
|
const spanStart = ajaxHtml.indexOf('<span>', embedEnd);
|
||||||
const spanEnd = ajaxHtml.indexOf('</span>', spanStart);
|
const spanEnd = ajaxHtml.indexOf('</span>', spanStart);
|
||||||
const serverLabel = ajaxHtml.slice(spanStart + 6, spanEnd).trim().toLowerCase();
|
const serverLabel = ajaxHtml.slice(spanStart + 6, spanEnd).trim().toLowerCase();
|
||||||
if (serverLabel.includes('filesun')) {
|
if (serverLabel.includes('filesun')) {
|
||||||
try {
|
try {
|
||||||
const rHtml = await fetchText(embedUrl, { headers: { 'Referer': ORIGIN, 'User-Agent': USER_AGENT } });
|
const rHtml = await fetchText(embedUrl, { headers: { 'Referer': ORIGIN, 'User-Agent': USER_AGENT } });
|
||||||
const m3u8Match = rHtml.match(/file:\s*["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/) || rHtml.match(/["']?file["']?:\s*["'](https?:\/\/[^"']+)["']/);
|
const m3u8Match = rHtml.match(/file:\s*["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/) || rHtml.match(/["']?file["']?:\s*["'](https?:\/\/[^"']+)["']/);
|
||||||
if (m3u8Match) results.push({ url: m3u8Match[1].replace(/\\\//g, '/'), server: 'FSOnline - FileSuN', headers: { 'Referer': embedUrl, 'Origin': 'https://player.fsonline.app', 'User-Agent': USER_AGENT } });
|
if (m3u8Match) results.push({ url: m3u8Match[1].replace(/\\\//g, '/'), server: 'FSOnline - FileSuN', headers: { 'Referer': embedUrl, 'Origin': 'https://player.fsonline.app', 'User-Agent': USER_AGENT } });
|
||||||
} catch { }
|
} catch { }
|
||||||
}
|
}
|
||||||
idx = spanEnd;
|
idx = spanEnd;
|
||||||
}
|
}
|
||||||
return results.length ? { allUrls: results } : null;
|
return results.length ? { allUrls: results } : null;
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSources(args) {
|
export async function getSources(args) {
|
||||||
const res = await getStream(args);
|
const res = await getStream(args);
|
||||||
return res?.allUrls ? [...new Set(res.allUrls.map(u => u.server))] : [];
|
return res?.allUrls ? [...new Set(res.allUrls.map(u => u.server))] : [];
|
||||||
}
|
}
|
||||||
@@ -1,262 +1,262 @@
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
|
||||||
export const ID = "goated";
|
export const ID = "goated";
|
||||||
export const DOMAIN = "https://goated.cx";
|
export const DOMAIN = "https://goated.cx";
|
||||||
export const RETURN_ALL_URLS = true;
|
export const RETURN_ALL_URLS = true;
|
||||||
|
|
||||||
const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
|
const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
|
||||||
|
|
||||||
const enc = new TextEncoder();
|
const enc = new TextEncoder();
|
||||||
const info = enc.encode("goated-api-v1");
|
const info = enc.encode("goated-api-v1");
|
||||||
const salt = enc.encode("goated-stream-salt");
|
const salt = enc.encode("goated-stream-salt");
|
||||||
|
|
||||||
const generateRandomIP = () => {
|
const generateRandomIP = () => {
|
||||||
const p = () => Math.floor(Math.random() * 255);
|
const p = () => Math.floor(Math.random() * 255);
|
||||||
let first = p();
|
let first = p();
|
||||||
while ([0, 10, 127, 169, 172, 192].includes(first)) first = p();
|
while ([0, 10, 127, 169, 172, 192].includes(first)) first = p();
|
||||||
return `${first}.${p()}.${p()}.${p()}`;
|
return `${first}.${p()}.${p()}.${p()}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
let cachedSession = null;
|
let cachedSession = null;
|
||||||
let sessionExpiresAt = 0;
|
let sessionExpiresAt = 0;
|
||||||
let cachedProviders = null;
|
let cachedProviders = null;
|
||||||
let providersExpiresAt = 0;
|
let providersExpiresAt = 0;
|
||||||
|
|
||||||
async function getValidSession(sessionIp) {
|
async function getValidSession(sessionIp) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (cachedSession && now < sessionExpiresAt) {
|
if (cachedSession && now < sessionExpiresAt) {
|
||||||
return cachedSession;
|
return cachedSession;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const chalRes = await fetch(`${DOMAIN}/api/auth/challenge`, {
|
const chalRes = await fetch(`${DOMAIN}/api/auth/challenge`, {
|
||||||
headers: {
|
headers: {
|
||||||
"User-Agent": UA,
|
"User-Agent": UA,
|
||||||
"X-Forwarded-For": sessionIp,
|
"X-Forwarded-For": sessionIp,
|
||||||
"X-Real-IP": sessionIp,
|
"X-Real-IP": sessionIp,
|
||||||
Origin: DOMAIN,
|
Origin: DOMAIN,
|
||||||
Referer: DOMAIN + "/",
|
Referer: DOMAIN + "/",
|
||||||
},
|
},
|
||||||
signal: AbortSignal.timeout(7000),
|
signal: AbortSignal.timeout(7000),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!chalRes.ok) throw new Error();
|
if (!chalRes.ok) throw new Error();
|
||||||
const chalData = await chalRes.json();
|
const chalData = await chalRes.json();
|
||||||
const { challenge, difficulty } = chalData;
|
const { challenge, difficulty } = chalData;
|
||||||
const targetPrefix = "0".repeat(Math.ceil((difficulty || 16) / 4));
|
const targetPrefix = "0".repeat(Math.ceil((difficulty || 16) / 4));
|
||||||
let nonceStr = "";
|
let nonceStr = "";
|
||||||
|
|
||||||
for (let i = 0; i < 5000000; i++) {
|
for (let i = 0; i < 5000000; i++) {
|
||||||
const nonce = Math.random().toString(36).substring(2, 10);
|
const nonce = Math.random().toString(36).substring(2, 10);
|
||||||
const hash = crypto.createHash("sha256").update(`${challenge}:${nonce}`).digest("hex");
|
const hash = crypto.createHash("sha256").update(`${challenge}:${nonce}`).digest("hex");
|
||||||
if (hash.startsWith(targetPrefix)) {
|
if (hash.startsWith(targetPrefix)) {
|
||||||
nonceStr = nonce;
|
nonceStr = nonce;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!nonceStr) throw new Error();
|
if (!nonceStr) throw new Error();
|
||||||
|
|
||||||
const res = await fetch(`${DOMAIN}/api/auth/session`, {
|
const res = await fetch(`${DOMAIN}/api/auth/session`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"User-Agent": UA,
|
"User-Agent": UA,
|
||||||
"X-Forwarded-For": sessionIp,
|
"X-Forwarded-For": sessionIp,
|
||||||
"X-Real-IP": sessionIp,
|
"X-Real-IP": sessionIp,
|
||||||
Origin: DOMAIN,
|
Origin: DOMAIN,
|
||||||
Referer: DOMAIN + "/",
|
Referer: DOMAIN + "/",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ challenge, nonce: nonceStr }),
|
body: JSON.stringify({ challenge, nonce: nonceStr }),
|
||||||
signal: AbortSignal.timeout(7000),
|
signal: AbortSignal.timeout(7000),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) throw new Error();
|
if (!res.ok) throw new Error();
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (!data.token) throw new Error();
|
if (!data.token) throw new Error();
|
||||||
|
|
||||||
cachedSession = data.token;
|
cachedSession = data.token;
|
||||||
sessionExpiresAt = now + (data.expiresIn || 600) * 1000 - 30000;
|
sessionExpiresAt = now + (data.expiresIn || 600) * 1000 - 30000;
|
||||||
return cachedSession;
|
return cachedSession;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error();
|
throw new Error();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getProviders() {
|
async function getProviders() {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (cachedProviders && now < providersExpiresAt) return cachedProviders;
|
if (cachedProviders && now < providersExpiresAt) return cachedProviders;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${DOMAIN}/api/providers`, {
|
const res = await fetch(`${DOMAIN}/api/providers`, {
|
||||||
headers: { "User-Agent": UA },
|
headers: { "User-Agent": UA },
|
||||||
signal: AbortSignal.timeout(5000)
|
signal: AbortSignal.timeout(5000)
|
||||||
});
|
});
|
||||||
if (!res.ok) return [];
|
if (!res.ok) return [];
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
cachedProviders = data.providers || [];
|
cachedProviders = data.providers || [];
|
||||||
providersExpiresAt = now + 3600 * 1000;
|
providersExpiresAt = now + 3600 * 1000;
|
||||||
return cachedProviders;
|
return cachedProviders;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deriveKey(tokenString) {
|
async function deriveKey(tokenString) {
|
||||||
const rawKey = await crypto.subtle.importKey("raw", enc.encode(tokenString), "HKDF", false, ["deriveKey"]);
|
const rawKey = await crypto.subtle.importKey("raw", enc.encode(tokenString), "HKDF", false, ["deriveKey"]);
|
||||||
return crypto.subtle.deriveKey(
|
return crypto.subtle.deriveKey(
|
||||||
{ name: "HKDF", hash: "SHA-256", salt, info },
|
{ name: "HKDF", hash: "SHA-256", salt, info },
|
||||||
rawKey,
|
rawKey,
|
||||||
{ name: "AES-GCM", length: 256 },
|
{ name: "AES-GCM", length: 256 },
|
||||||
false,
|
false,
|
||||||
["encrypt", "decrypt"],
|
["encrypt", "decrypt"],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function encryptParams(payloadObj, key) {
|
async function encryptParams(payloadObj, key) {
|
||||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||||
const data = enc.encode(JSON.stringify(payloadObj));
|
const data = enc.encode(JSON.stringify(payloadObj));
|
||||||
const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, data);
|
const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, data);
|
||||||
const combined = new Uint8Array(iv.length + ct.byteLength);
|
const combined = new Uint8Array(iv.length + ct.byteLength);
|
||||||
combined.set(iv);
|
combined.set(iv);
|
||||||
combined.set(new Uint8Array(ct), iv.length);
|
combined.set(new Uint8Array(ct), iv.length);
|
||||||
return Buffer.from(combined).toString("base64");
|
return Buffer.from(combined).toString("base64");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function decryptUrl(b64Url, key) {
|
async function decryptUrl(b64Url, key) {
|
||||||
const raw = Buffer.from(b64Url, "base64");
|
const raw = Buffer.from(b64Url, "base64");
|
||||||
const iv = raw.slice(0, 12);
|
const iv = raw.slice(0, 12);
|
||||||
const ct = raw.slice(12);
|
const ct = raw.slice(12);
|
||||||
try {
|
try {
|
||||||
const plain = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ct);
|
const plain = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ct);
|
||||||
return Buffer.from(plain).toString("utf8");
|
return Buffer.from(plain).toString("utf8");
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function decryptDeep(obj, key) {
|
async function decryptDeep(obj, key) {
|
||||||
if (!obj || typeof obj !== "object") return obj;
|
if (!obj || typeof obj !== "object") return obj;
|
||||||
if (Array.isArray(obj)) return Promise.all(obj.map(x => decryptDeep(x, key)));
|
if (Array.isArray(obj)) return Promise.all(obj.map(x => decryptDeep(x, key)));
|
||||||
|
|
||||||
const out = {};
|
const out = {};
|
||||||
for (const [k, v] of Object.entries(obj)) {
|
for (const [k, v] of Object.entries(obj)) {
|
||||||
if ((k === "url" || k === "file") && typeof v === "string" && v.length > 40) {
|
if ((k === "url" || k === "file") && typeof v === "string" && v.length > 40) {
|
||||||
try {
|
try {
|
||||||
const dec = await decryptUrl(v, key);
|
const dec = await decryptUrl(v, key);
|
||||||
out[k] = dec ? dec : v;
|
out[k] = dec ? dec : v;
|
||||||
} catch {
|
} catch {
|
||||||
out[k] = v;
|
out[k] = v;
|
||||||
}
|
}
|
||||||
} else if (k === "links" && Array.isArray(v)) {
|
} else if (k === "links" && Array.isArray(v)) {
|
||||||
out[k] = await Promise.all(v.map(async x => {
|
out[k] = await Promise.all(v.map(async x => {
|
||||||
if (typeof x === "string" && x.length > 40) {
|
if (typeof x === "string" && x.length > 40) {
|
||||||
try {
|
try {
|
||||||
const dec = await decryptUrl(x, key);
|
const dec = await decryptUrl(x, key);
|
||||||
return dec ? dec : x;
|
return dec ? dec : x;
|
||||||
} catch {
|
} catch {
|
||||||
return x;
|
return x;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return x;
|
return x;
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
out[k] = await decryptDeep(v, key);
|
out[k] = await decryptDeep(v, key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStream({ id, s, e, clientIP }) {
|
export async function getStream({ id, s, e, clientIP }) {
|
||||||
try {
|
try {
|
||||||
const sessionIp = generateRandomIP();
|
const sessionIp = generateRandomIP();
|
||||||
const sessionJwt = await getValidSession(sessionIp);
|
const sessionJwt = await getValidSession(sessionIp);
|
||||||
const actualKey = await deriveKey(sessionJwt);
|
const actualKey = await deriveKey(sessionJwt);
|
||||||
|
|
||||||
const isTv = s && e && s !== "null" && e !== "null";
|
const isTv = s && e && s !== "null" && e !== "null";
|
||||||
const contentType = isTv ? "tv" : "movies";
|
const contentType = isTv ? "tv" : "movies";
|
||||||
const refererPath = isTv ? `tv/${id}-${s}-${e}/play` : `movie/${id}/play`;
|
const refererPath = isTv ? `tv/${id}-${s}-${e}/play` : `movie/${id}/play`;
|
||||||
|
|
||||||
let providers = await getProviders();
|
let providers = await getProviders();
|
||||||
let activeProviders = providers.filter(p => p.active && (p.endpoints || []).includes(contentType));
|
let activeProviders = providers.filter(p => p.active && (p.endpoints || []).includes(contentType));
|
||||||
if (activeProviders.length === 0) activeProviders = [{ name: null }];
|
if (activeProviders.length === 0) activeProviders = [{ name: null }];
|
||||||
|
|
||||||
const fetchProviderStream = async (provider) => {
|
const fetchProviderStream = async (provider) => {
|
||||||
try {
|
try {
|
||||||
const payload = isTv
|
const payload = isTv
|
||||||
? { type: "tv", id: id.toString(), season: s.toString(), episode: e.toString() }
|
? { type: "tv", id: id.toString(), season: s.toString(), episode: e.toString() }
|
||||||
: { type: "movie", id: id.toString() };
|
: { type: "movie", id: id.toString() };
|
||||||
|
|
||||||
if (provider.name) payload.provider = provider.name;
|
if (provider.name) payload.provider = provider.name;
|
||||||
|
|
||||||
const dParam = await encryptParams(payload, actualKey);
|
const dParam = await encryptParams(payload, actualKey);
|
||||||
|
|
||||||
const streamRes = await fetch(`${DOMAIN}/api/stream?d=${encodeURIComponent(dParam)}`, {
|
const streamRes = await fetch(`${DOMAIN}/api/stream?d=${encodeURIComponent(dParam)}`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${sessionJwt}`,
|
Authorization: `Bearer ${sessionJwt}`,
|
||||||
"User-Agent": UA,
|
"User-Agent": UA,
|
||||||
"X-Forwarded-For": sessionIp,
|
"X-Forwarded-For": sessionIp,
|
||||||
"X-Real-IP": sessionIp,
|
"X-Real-IP": sessionIp,
|
||||||
Origin: DOMAIN,
|
Origin: DOMAIN,
|
||||||
Referer: `${DOMAIN}/${refererPath}`,
|
Referer: `${DOMAIN}/${refererPath}`,
|
||||||
},
|
},
|
||||||
signal: AbortSignal.timeout(10000)
|
signal: AbortSignal.timeout(10000)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!streamRes.ok) return [];
|
if (!streamRes.ok) return [];
|
||||||
let rawData = await streamRes.json();
|
let rawData = await streamRes.json();
|
||||||
|
|
||||||
if (rawData.enc) {
|
if (rawData.enc) {
|
||||||
const raw = Buffer.from(rawData.enc, "base64");
|
const raw = Buffer.from(rawData.enc, "base64");
|
||||||
const decIv = raw.slice(0, 12);
|
const decIv = raw.slice(0, 12);
|
||||||
const decCt = raw.slice(12);
|
const decCt = raw.slice(12);
|
||||||
const decrypted = await crypto.subtle.decrypt({ name: "AES-GCM", iv: decIv }, actualKey, decCt);
|
const decrypted = await crypto.subtle.decrypt({ name: "AES-GCM", iv: decIv }, actualKey, decCt);
|
||||||
rawData = JSON.parse(Buffer.from(decrypted).toString("utf-8"));
|
rawData = JSON.parse(Buffer.from(decrypted).toString("utf-8"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const streamData = await decryptDeep(rawData, actualKey);
|
const streamData = await decryptDeep(rawData, actualKey);
|
||||||
|
|
||||||
if (!streamData.ok || !streamData.streams || streamData.streams.length === 0) return [];
|
if (!streamData.ok || !streamData.streams || streamData.streams.length === 0) return [];
|
||||||
|
|
||||||
const validStreams = [];
|
const validStreams = [];
|
||||||
for (const stream of streamData.streams) {
|
for (const stream of streamData.streams) {
|
||||||
if (!stream.url) continue;
|
if (!stream.url) continue;
|
||||||
if (stream.quality && stream.quality.toUpperCase() !== "AUTO" && streamData.streams.some(st => st.quality && st.quality.toUpperCase() === "AUTO")) continue;
|
if (stream.quality && stream.quality.toUpperCase() !== "AUTO" && streamData.streams.some(st => st.quality && st.quality.toUpperCase() === "AUTO")) continue;
|
||||||
|
|
||||||
let finalUrl = stream.url;
|
let finalUrl = stream.url;
|
||||||
if (finalUrl.startsWith("/api/")) finalUrl = `${DOMAIN}${finalUrl}`;
|
if (finalUrl.startsWith("/api/")) finalUrl = `${DOMAIN}${finalUrl}`;
|
||||||
|
|
||||||
validStreams.push({
|
validStreams.push({
|
||||||
url: finalUrl,
|
url: finalUrl,
|
||||||
headers: {
|
headers: {
|
||||||
"User-Agent": UA,
|
"User-Agent": UA,
|
||||||
Referer: DOMAIN + "/",
|
Referer: DOMAIN + "/",
|
||||||
Origin: DOMAIN,
|
Origin: DOMAIN,
|
||||||
},
|
},
|
||||||
server: stream.source || stream.server || provider.nickname || provider.name || "Goated",
|
server: stream.source || stream.server || provider.nickname || provider.name || "Goated",
|
||||||
quality: stream.quality || "Auto",
|
quality: stream.quality || "Auto",
|
||||||
type: finalUrl.includes(".mp4") ? "mp4" : "hls",
|
type: finalUrl.includes(".mp4") ? "mp4" : "hls",
|
||||||
skipVerify: true,
|
skipVerify: true,
|
||||||
skipProxy: true,
|
skipProxy: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return validStreams;
|
return validStreams;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const promises = activeProviders.map(p => fetchProviderStream(p));
|
const promises = activeProviders.map(p => fetchProviderStream(p));
|
||||||
const results = await Promise.allSettled(promises);
|
const results = await Promise.allSettled(promises);
|
||||||
|
|
||||||
const allUrls = [];
|
const allUrls = [];
|
||||||
for (const res of results) {
|
for (const res of results) {
|
||||||
if (res.status === "fulfilled" && res.value.length > 0) {
|
if (res.status === "fulfilled" && res.value.length > 0) {
|
||||||
allUrls.push(...res.value);
|
allUrls.push(...res.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allUrls.length > 0) {
|
if (allUrls.length > 0) {
|
||||||
return { ...allUrls[0], allUrls };
|
return { ...allUrls[0], allUrls };
|
||||||
}
|
}
|
||||||
} catch (err) { }
|
} catch (err) { }
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -1,82 +1,82 @@
|
|||||||
import crypto from 'node:crypto';
|
import crypto from 'node:crypto';
|
||||||
import { fetchJson, fetchText, USER_AGENT } from '../utils/helpers.js';
|
import { fetchJson, fetchText, USER_AGENT } from '../utils/helpers.js';
|
||||||
|
|
||||||
const API_BASE = 'https://enc-dec.app/api';
|
const API_BASE = 'https://enc-dec.app/api';
|
||||||
const DOMAINS = ['hexa.su', 'flixer.su'];
|
const DOMAINS = ['hexa.su', 'flixer.su'];
|
||||||
|
|
||||||
async function getChallengeToken() {
|
async function getChallengeToken() {
|
||||||
try {
|
try {
|
||||||
const data = await fetchJson(`${API_BASE}/enc-hexa`, { signal: AbortSignal.timeout(6000) });
|
const data = await fetchJson(`${API_BASE}/enc-hexa`, { signal: AbortSignal.timeout(6000) });
|
||||||
return data?.status === 200 ? data.result.token : null;
|
return data?.status === 200 ? data.result.token : null;
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStream({ id, s, e, server }) {
|
export async function getStream({ id, s, e, server }) {
|
||||||
try {
|
try {
|
||||||
const apiKey = crypto.randomBytes(32).toString('hex');
|
const apiKey = crypto.randomBytes(32).toString('hex');
|
||||||
const capToken = await getChallengeToken();
|
const capToken = await getChallengeToken();
|
||||||
if (!capToken) return null;
|
if (!capToken) return null;
|
||||||
|
|
||||||
const isTv = s != null && e != null;
|
const isTv = s != null && e != null;
|
||||||
let decrypted = null;
|
let decrypted = null;
|
||||||
|
|
||||||
for (const domain of DOMAINS) {
|
for (const domain of DOMAINS) {
|
||||||
try {
|
try {
|
||||||
const url = isTv
|
const url = isTv
|
||||||
? `https://theemoviedb.${domain}/api/tmdb/tv/${id}/season/${s}/episode/${e}/images`
|
? `https://theemoviedb.${domain}/api/tmdb/tv/${id}/season/${s}/episode/${e}/images`
|
||||||
: `https://theemoviedb.${domain}/api/tmdb/movie/${id}/images`;
|
: `https://theemoviedb.${domain}/api/tmdb/movie/${id}/images`;
|
||||||
|
|
||||||
const encrypted = await fetchText(url, {
|
const encrypted = await fetchText(url, {
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': USER_AGENT,
|
'User-Agent': USER_AGENT,
|
||||||
'Referer': `https://${domain}/`,
|
'Referer': `https://${domain}/`,
|
||||||
'Accept': 'text/plain',
|
'Accept': 'text/plain',
|
||||||
'X-Fingerprint-Lite': 'e9136c41504646444',
|
'X-Fingerprint-Lite': 'e9136c41504646444',
|
||||||
'X-Api-Key': apiKey,
|
'X-Api-Key': apiKey,
|
||||||
'X-Cap-Token': capToken
|
'X-Cap-Token': capToken
|
||||||
},
|
},
|
||||||
signal: AbortSignal.timeout(10000)
|
signal: AbortSignal.timeout(10000)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!encrypted) continue;
|
if (!encrypted) continue;
|
||||||
|
|
||||||
const decData = await fetchJson(`${API_BASE}/dec-hexa`, {
|
const decData = await fetchJson(`${API_BASE}/dec-hexa`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ text: encrypted, key: apiKey }),
|
body: JSON.stringify({ text: encrypted, key: apiKey }),
|
||||||
signal: AbortSignal.timeout(8000)
|
signal: AbortSignal.timeout(8000)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (decData?.status === 200 && decData.result?.sources?.length) {
|
if (decData?.status === 200 && decData.result?.sources?.length) {
|
||||||
decrypted = decData.result;
|
decrypted = decData.result;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch { continue; }
|
} catch { continue; }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!decrypted?.sources?.length) return null;
|
if (!decrypted?.sources?.length) return null;
|
||||||
|
|
||||||
let sources = decrypted.sources.map(src => ({
|
let sources = decrypted.sources.map(src => ({
|
||||||
url: src.url,
|
url: src.url,
|
||||||
server: `Hexa - ${src.name || src.server || 'Server'}`,
|
server: `Hexa - ${src.name || src.server || 'Server'}`,
|
||||||
type: src.url.includes('.m3u8') ? 'hls' : 'mp4',
|
type: src.url.includes('.m3u8') ? 'hls' : 'mp4',
|
||||||
quality: src.quality || 'Auto',
|
quality: src.quality || 'Auto',
|
||||||
subtitles: (decrypted.subtitles || []).map(sub => ({
|
subtitles: (decrypted.subtitles || []).map(sub => ({
|
||||||
url: sub.url,
|
url: sub.url,
|
||||||
lang: sub.label || sub.lang || 'Unknown'
|
lang: sub.label || sub.lang || 'Unknown'
|
||||||
})),
|
})),
|
||||||
skipProxy: false
|
skipProxy: false
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (server && server !== 'all') {
|
if (server && server !== 'all') {
|
||||||
const cleanName = server.replace('Hexa - ', '');
|
const cleanName = server.replace('Hexa - ', '');
|
||||||
sources = sources.filter(u => u.server.includes(cleanName));
|
sources = sources.filter(u => u.server.includes(cleanName));
|
||||||
}
|
}
|
||||||
|
|
||||||
return sources.length ? { allUrls: sources } : null;
|
return sources.length ? { allUrls: sources } : null;
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSources() {
|
export async function getSources() {
|
||||||
return ['Hexa'];
|
return ['Hexa'];
|
||||||
}
|
}
|
||||||
@@ -1,60 +1,60 @@
|
|||||||
import { fetchJson, USER_AGENT, getTmdbInfo } from '../utils/helpers.js';
|
import { fetchJson, USER_AGENT, getTmdbInfo } from '../utils/helpers.js';
|
||||||
|
|
||||||
export const SKIP_VERIFY = true;
|
export const SKIP_VERIFY = true;
|
||||||
export const MULTI_URL = false;
|
export const MULTI_URL = false;
|
||||||
|
|
||||||
const ENC_API = "https://enc-dec.app/api";
|
const ENC_API = "https://enc-dec.app/api";
|
||||||
const BASE = "https://kisskh.do";
|
const BASE = "https://kisskh.do";
|
||||||
const HEADERS = { 'User-Agent': USER_AGENT, Accept: 'application/json' };
|
const HEADERS = { 'User-Agent': USER_AGENT, Accept: 'application/json' };
|
||||||
|
|
||||||
function cleanTitle(t) { return t ? t.toLowerCase().replace(/\(\d{4}\)/g, '').replace(/[^a-z0-9]/g, '') : ''; }
|
function cleanTitle(t) { return t ? t.toLowerCase().replace(/\(\d{4}\)/g, '').replace(/[^a-z0-9]/g, '') : ''; }
|
||||||
|
|
||||||
async function encKey(episodeId, type) {
|
async function encKey(episodeId, type) {
|
||||||
try {
|
try {
|
||||||
const data = await fetchJson(`${ENC_API}/enc-kisskh?text=${episodeId}&type=${type}`, { signal: AbortSignal.timeout(6000) });
|
const data = await fetchJson(`${ENC_API}/enc-kisskh?text=${episodeId}&type=${type}`, { signal: AbortSignal.timeout(6000) });
|
||||||
return data?.status === 200 ? data.result : null;
|
return data?.status === 200 ? data.result : null;
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStream({ id, s, e }) {
|
export async function getStream({ id, s, e }) {
|
||||||
try {
|
try {
|
||||||
const info = await getTmdbInfo(id, s ? 'tv' : 'movie', s);
|
const info = await getTmdbInfo(id, s ? 'tv' : 'movie', s);
|
||||||
if (!info) return null;
|
if (!info) return null;
|
||||||
let drama = null, bestScore = -1;
|
let drama = null, bestScore = -1;
|
||||||
for (const title of (info.titles.length > 1 ? [...info.titles].reverse() : info.titles)) {
|
for (const title of (info.titles.length > 1 ? [...info.titles].reverse() : info.titles)) {
|
||||||
const results = await fetchJson(`${BASE}/api/DramaList/Search?q=${encodeURIComponent(title)}`, { headers: HEADERS, signal: AbortSignal.timeout(6000) }).catch(() => []);
|
const results = await fetchJson(`${BASE}/api/DramaList/Search?q=${encodeURIComponent(title)}`, { headers: HEADERS, signal: AbortSignal.timeout(6000) }).catch(() => []);
|
||||||
const targetClean = cleanTitle(title);
|
const targetClean = cleanTitle(title);
|
||||||
for (const item of results) {
|
for (const item of results) {
|
||||||
const parts = (item.title || '').split(/\s*-\s*/).map(cleanTitle);
|
const parts = (item.title || '').split(/\s*-\s*/).map(cleanTitle);
|
||||||
let score = 0;
|
let score = 0;
|
||||||
if (parts.some(p => p === targetClean)) score += 5;
|
if (parts.some(p => p === targetClean)) score += 5;
|
||||||
else if (parts.some(p => p.includes(targetClean) || targetClean.includes(p))) score += 3;
|
else if (parts.some(p => p.includes(targetClean) || targetClean.includes(p))) score += 3;
|
||||||
else continue;
|
else continue;
|
||||||
if (score > bestScore) { bestScore = score; drama = item; }
|
if (score > bestScore) { bestScore = score; drama = item; }
|
||||||
}
|
}
|
||||||
if (drama && bestScore >= 5) break;
|
if (drama && bestScore >= 5) break;
|
||||||
}
|
}
|
||||||
if (!drama) return null;
|
if (!drama) return null;
|
||||||
const detail = await fetchJson(`${BASE}/api/DramaList/Drama/${drama.id}`, { headers: HEADERS, signal: AbortSignal.timeout(8000) });
|
const detail = await fetchJson(`${BASE}/api/DramaList/Drama/${drama.id}`, { headers: HEADERS, signal: AbortSignal.timeout(8000) });
|
||||||
const episodeId = detail?.episodes?.find(ep => Math.floor(ep.number) === Number(e || 1))?.id;
|
const episodeId = detail?.episodes?.find(ep => Math.floor(ep.number) === Number(e || 1))?.id;
|
||||||
if (!episodeId) return null;
|
if (!episodeId) return null;
|
||||||
const vidKey = await encKey(episodeId, 'vid');
|
const vidKey = await encKey(episodeId, 'vid');
|
||||||
if (!vidKey) return null;
|
if (!vidKey) return null;
|
||||||
const videoData = await fetchJson(`${BASE}/api/DramaList/Episode/${episodeId}.png?err=false&ts=&time=&kkey=${vidKey}`, { headers: HEADERS, signal: AbortSignal.timeout(10000) });
|
const videoData = await fetchJson(`${BASE}/api/DramaList/Episode/${episodeId}.png?err=false&ts=&time=&kkey=${vidKey}`, { headers: HEADERS, signal: AbortSignal.timeout(10000) });
|
||||||
if (!videoData?.Video) return null;
|
if (!videoData?.Video) return null;
|
||||||
let subtitles = [];
|
let subtitles = [];
|
||||||
const subKey = await encKey(episodeId, 'sub');
|
const subKey = await encKey(episodeId, 'sub');
|
||||||
if (subKey) {
|
if (subKey) {
|
||||||
try {
|
try {
|
||||||
const subList = await fetchJson(`${BASE}/api/Sub/${episodeId}?kkey=${subKey}`, { headers: HEADERS, signal: AbortSignal.timeout(8000) });
|
const subList = await fetchJson(`${BASE}/api/Sub/${episodeId}?kkey=${subKey}`, { headers: HEADERS, signal: AbortSignal.timeout(8000) });
|
||||||
if (Array.isArray(subList)) subtitles = subList.map(sub => ({ url: sub.src, label: sub.label, lang: sub.land, default: !!sub.default }));
|
if (Array.isArray(subList)) subtitles = subList.map(sub => ({ url: sub.src, label: sub.label, lang: sub.land, default: !!sub.default }));
|
||||||
} catch { }
|
} catch { }
|
||||||
}
|
}
|
||||||
return { allUrls: [{ url: videoData.Video, type: videoData.Video.includes('.m3u8') ? 'hls' : 'mp4', audio: 'sub', server: 'KissKH', skipProxy: false, subtitles }] };
|
return { allUrls: [{ url: videoData.Video, type: videoData.Video.includes('.m3u8') ? 'hls' : 'mp4', audio: 'sub', server: 'KissKH', skipProxy: false, subtitles }] };
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSources(args) {
|
export async function getSources(args) {
|
||||||
const stream = await getStream(args);
|
const stream = await getStream(args);
|
||||||
return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server))] : [];
|
return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server))] : [];
|
||||||
}
|
}
|
||||||
@@ -1,78 +1,78 @@
|
|||||||
import { USER_AGENT, fetchText, fetchJson } from '../utils/helpers.js';
|
import { USER_AGENT, fetchText, fetchJson } from '../utils/helpers.js';
|
||||||
|
|
||||||
const TMDB_BASE = 'https://api.themoviedb.org/3';
|
const TMDB_BASE = 'https://api.themoviedb.org/3';
|
||||||
const LM_DOMAINS = ['https://www.lookmovie2.to', 'https://lookmovie2.to', 'https://lookmovie.foundation'];
|
const LM_DOMAINS = ['https://www.lookmovie2.to', 'https://lookmovie2.to', 'https://lookmovie.foundation'];
|
||||||
const HEADERS_BASE = { 'User-Agent': USER_AGENT, 'Accept-Language': 'en-US,en;q=0.9' };
|
const HEADERS_BASE = { 'User-Agent': USER_AGENT, 'Accept-Language': 'en-US,en;q=0.9' };
|
||||||
|
|
||||||
async function searchLookMovie(type, title, year, clientIP) {
|
async function searchLookMovie(type, title, year, clientIP) {
|
||||||
for (const base of LM_DOMAINS) {
|
for (const base of LM_DOMAINS) {
|
||||||
try {
|
try {
|
||||||
const headers = { ...HEADERS_BASE, 'Accept': 'application/json', 'Referer': `${base}/`, 'X-Requested-With': 'XMLHttpRequest', ...(clientIP && { 'X-Forwarded-For': clientIP }) };
|
const headers = { ...HEADERS_BASE, 'Accept': 'application/json', 'Referer': `${base}/`, 'X-Requested-With': 'XMLHttpRequest', ...(clientIP && { 'X-Forwarded-For': clientIP }) };
|
||||||
const data = await fetchJson(`${base}/api/v1/${type}/do-search/?q=${encodeURIComponent(title)}`, { headers, signal: AbortSignal.timeout(4000) });
|
const data = await fetchJson(`${base}/api/v1/${type}/do-search/?q=${encodeURIComponent(title)}`, { headers, signal: AbortSignal.timeout(4000) });
|
||||||
const results = data?.result;
|
const results = data?.result;
|
||||||
if (results?.length) {
|
if (results?.length) {
|
||||||
const match = results.find(r => String(r.year) === String(year)) ?? results.find(r => r.title?.toLowerCase() === title.toLowerCase()) ?? results[0];
|
const match = results.find(r => String(r.year) === String(year)) ?? results.find(r => r.title?.toLowerCase() === title.toLowerCase()) ?? results[0];
|
||||||
if (match) return { match, base };
|
if (match) return { match, base };
|
||||||
}
|
}
|
||||||
} catch { }
|
} catch { }
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const STORAGE_RE = /window\[['"](?:movie|show)_storage['"]\]\s*=\s*\{([^}]+)\}/s;
|
const STORAGE_RE = /window\[['"](?:movie|show)_storage['"]\]\s*=\s*\{([^}]+)\}/s;
|
||||||
const ID_MOVIE_RE = /['"]?(?:id_movie|movieId)['"]?\s*[:=]\s*['"]?(\d+)['"]?/i;
|
const ID_MOVIE_RE = /['"]?(?:id_movie|movieId)['"]?\s*[:=]\s*['"]?(\d+)['"]?/i;
|
||||||
const SEASONS_RE = /seasons\s*:\s*(\[[\s\S]+?\])\s*[,}]/;
|
const SEASONS_RE = /seasons\s*:\s*(\[[\s\S]+?\])\s*[,}]/;
|
||||||
|
|
||||||
function getEpisodeIdFast(html, s, e) {
|
function getEpisodeIdFast(html, s, e) {
|
||||||
const sMatch = html.match(STORAGE_RE);
|
const sMatch = html.match(STORAGE_RE);
|
||||||
if (sMatch) {
|
if (sMatch) {
|
||||||
const block = sMatch[1], sm = block.match(SEASONS_RE);
|
const block = sMatch[1], sm = block.match(SEASONS_RE);
|
||||||
if (sm) {
|
if (sm) {
|
||||||
try {
|
try {
|
||||||
const seasons = JSON.parse(sm[1]);
|
const seasons = JSON.parse(sm[1]);
|
||||||
const season = seasons.find(x => String(x?.season ?? x?.meta?.season) === String(s));
|
const season = seasons.find(x => String(x?.season ?? x?.meta?.season) === String(s));
|
||||||
if (season) {
|
if (season) {
|
||||||
const ep = Array.isArray(season.episodes) ? season.episodes.find(x => String(x.episode) === String(e)) : (season.episodes?.[String(e)] || Object.values(season.episodes || {}).find(x => String(x.episode) === String(e)));
|
const ep = Array.isArray(season.episodes) ? season.episodes.find(x => String(x.episode) === String(e)) : (season.episodes?.[String(e)] || Object.values(season.episodes || {}).find(x => String(x.episode) === String(e)));
|
||||||
if (ep) return String(ep.id_episode ?? ep.id);
|
if (ep) return String(ep.id_episode ?? ep.id);
|
||||||
}
|
}
|
||||||
} catch { }
|
} catch { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const parts = html.split(/id_episode['"]?\s*[:=]\s*['"]?(\d+)['"]?/i);
|
const parts = html.split(/id_episode['"]?\s*[:=]\s*['"]?(\d+)['"]?/i);
|
||||||
for (let i = 1; i < parts.length; i += 2) {
|
for (let i = 1; i < parts.length; i += 2) {
|
||||||
const context = parts[i - 1].slice(-300) + parts[i + 1].slice(0, 300);
|
const context = parts[i - 1].slice(-300) + parts[i + 1].slice(0, 300);
|
||||||
if (context.match(new RegExp(`['"]?episode['"]?\\s*[:=]\\s*['"]?${e}['"]?`, 'i')) && context.match(new RegExp(`['"]?season['"]?\\s*[:=]\\s*['"]?${s}['"]?`, 'i'))) return parts[i];
|
if (context.match(new RegExp(`['"]?episode['"]?\\s*[:=]\\s*['"]?${e}['"]?`, 'i')) && context.match(new RegExp(`['"]?season['"]?\\s*[:=]\\s*['"]?${s}['"]?`, 'i'))) return parts[i];
|
||||||
}
|
}
|
||||||
const am = html.match(new RegExp(`data-season=["']${s}["'][^>]*?data-episode=["']${e}["'][^>]*?data-id=["'](\\d+)["']`, 'i')) || html.match(new RegExp(`data-episode=["']${e}["'][^>]*?data-season=["']${s}["'][^>]*?data-id=["'](\\d+)["']`, 'i'));
|
const am = html.match(new RegExp(`data-season=["']${s}["'][^>]*?data-episode=["']${e}["'][^>]*?data-id=["'](\\d+)["']`, 'i')) || html.match(new RegExp(`data-episode=["']${e}["'][^>]*?data-season=["']${s}["'][^>]*?data-id=["'](\\d+)["']`, 'i'));
|
||||||
return am ? am[1] : null;
|
return am ? am[1] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStream({ id, s, e, tmdbApiKey, clientIP }) {
|
export async function getStream({ id, s, e, tmdbApiKey, clientIP }) {
|
||||||
try {
|
try {
|
||||||
const tmdbKey = tmdbApiKey;
|
const tmdbKey = tmdbApiKey;
|
||||||
if (!tmdbKey) return null;
|
if (!tmdbKey) return null;
|
||||||
const isTV = s != null && e != null;
|
const isTV = s != null && e != null;
|
||||||
const typeStr = isTV ? 'shows' : 'movies';
|
const typeStr = isTV ? 'shows' : 'movies';
|
||||||
const tmdbData = await fetchJson(`${TMDB_BASE}/${isTV ? 'tv' : 'movie'}/${id}?api_key=${tmdbKey}`, { signal: AbortSignal.timeout(3000) });
|
const tmdbData = await fetchJson(`${TMDB_BASE}/${isTV ? 'tv' : 'movie'}/${id}?api_key=${tmdbKey}`, { signal: AbortSignal.timeout(3000) });
|
||||||
const title = tmdbData?.title || tmdbData?.name;
|
const title = tmdbData?.title || tmdbData?.name;
|
||||||
const year = (tmdbData?.first_air_date || tmdbData?.release_date || '').slice(0, 4);
|
const year = (tmdbData?.first_air_date || tmdbData?.release_date || '').slice(0, 4);
|
||||||
if (!title) return null;
|
if (!title) return null;
|
||||||
const searchRes = await searchLookMovie(typeStr, title, year, clientIP);
|
const searchRes = await searchLookMovie(typeStr, title, year, clientIP);
|
||||||
if (!searchRes?.match?.slug) return null;
|
if (!searchRes?.match?.slug) return null;
|
||||||
const { match, base } = searchRes;
|
const { match, base } = searchRes;
|
||||||
const headers = { ...HEADERS_BASE, 'Accept': 'text/html', 'Referer': `${base}/`, ...(clientIP && { 'X-Forwarded-For': clientIP }) };
|
const headers = { ...HEADERS_BASE, 'Accept': 'text/html', 'Referer': `${base}/`, ...(clientIP && { 'X-Forwarded-For': clientIP }) };
|
||||||
const html = await fetchText(`${base}/${typeStr}/play/${match.slug}`, { headers, signal: AbortSignal.timeout(8000) });
|
const html = await fetchText(`${base}/${typeStr}/play/${match.slug}`, { headers, signal: AbortSignal.timeout(8000) });
|
||||||
const storageMatch = html.match(STORAGE_RE);
|
const storageMatch = html.match(STORAGE_RE);
|
||||||
if (!storageMatch) return null;
|
if (!storageMatch) return null;
|
||||||
const hashMatch = storageMatch[1].match(/hash\s*:\s*['"]([^'"]+)['"]/);
|
const hashMatch = storageMatch[1].match(/hash\s*:\s*['"]([^'"]+)['"]/);
|
||||||
const expiresMatch = storageMatch[1].match(/expires\s*:\s*(\d+)/);
|
const expiresMatch = storageMatch[1].match(/expires\s*:\s*(\d+)/);
|
||||||
if (!hashMatch || !expiresMatch) return null;
|
if (!hashMatch || !expiresMatch) return null;
|
||||||
const streamId = isTV ? getEpisodeIdFast(html, s, e) : (match.id_movie || match.id || html.match(ID_MOVIE_RE)?.[1]);
|
const streamId = isTV ? getEpisodeIdFast(html, s, e) : (match.id_movie || match.id || html.match(ID_MOVIE_RE)?.[1]);
|
||||||
if (!streamId) return null;
|
if (!streamId) return null;
|
||||||
const accessHeaders = { ...HEADERS_BASE, 'Accept': 'application/json', 'Referer': `${base}/`, 'X-Requested-With': 'XMLHttpRequest', ...(clientIP && { 'X-Forwarded-For': clientIP }) };
|
const accessHeaders = { ...HEADERS_BASE, 'Accept': 'application/json', 'Referer': `${base}/`, 'X-Requested-With': 'XMLHttpRequest', ...(clientIP && { 'X-Forwarded-For': clientIP }) };
|
||||||
const data = await fetchJson(`${base}/api/v1/security/${isTV ? 'episode' : 'movie'}-access?id_${isTV ? 'episode' : 'movie'}=${streamId}&hash=${hashMatch[1]}&expires=${expiresMatch[1]}`, { headers: accessHeaders, signal: AbortSignal.timeout(8000) });
|
const data = await fetchJson(`${base}/api/v1/security/${isTV ? 'episode' : 'movie'}-access?id_${isTV ? 'episode' : 'movie'}=${streamId}&hash=${hashMatch[1]}&expires=${expiresMatch[1]}`, { headers: accessHeaders, signal: AbortSignal.timeout(8000) });
|
||||||
const streams = data?.streams ?? data?.result?.streams ?? data?.data?.streams ?? data;
|
const streams = data?.streams ?? data?.result?.streams ?? data?.data?.streams ?? data;
|
||||||
const allUrls = Object.entries(streams || {}).filter(([, v]) => typeof v === 'string' && v.includes('.m3u8')).map(([quality, url]) => ({ url, quality }));
|
const allUrls = Object.entries(streams || {}).filter(([, v]) => typeof v === 'string' && v.includes('.m3u8')).map(([quality, url]) => ({ url, quality }));
|
||||||
return allUrls.length ? { allUrls } : null;
|
return allUrls.length ? { allUrls } : null;
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
@@ -1,115 +1,115 @@
|
|||||||
import { USER_AGENT } from '../utils/helpers.js';
|
import { USER_AGENT } from '../utils/helpers.js';
|
||||||
|
|
||||||
const BASE_URL = "https://mapple.club";
|
const BASE_URL = "https://mapple.club";
|
||||||
const PROXY_HEADERS = { Referer: `${BASE_URL}/`, Origin: BASE_URL, "User-Agent": USER_AGENT };
|
const PROXY_HEADERS = { Referer: `${BASE_URL}/`, Origin: BASE_URL, "User-Agent": USER_AGENT };
|
||||||
|
|
||||||
const K = new Uint32Array([0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2]);
|
const K = new Uint32Array([0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2]);
|
||||||
const W_BUF = new Uint32Array(64);
|
const W_BUF = new Uint32Array(64);
|
||||||
|
|
||||||
function sha256Into(buf, total, out) {
|
function sha256Into(buf, total, out) {
|
||||||
let h0 = 0x6a09e667, h1 = 0xbb67ae85, h2 = 0x3c6ef372, h3 = 0xa54ff53a, h4 = 0x510e527f, h5 = 0x9b05688c, h6 = 0x1f83d9ab, h7 = 0x5be0cd19;
|
let h0 = 0x6a09e667, h1 = 0xbb67ae85, h2 = 0x3c6ef372, h3 = 0xa54ff53a, h4 = 0x510e527f, h5 = 0x9b05688c, h6 = 0x1f83d9ab, h7 = 0x5be0cd19;
|
||||||
const dv = new DataView(buf.buffer, buf.byteOffset, total);
|
const dv = new DataView(buf.buffer, buf.byteOffset, total);
|
||||||
for (let off = 0; off < total; off += 64) {
|
for (let off = 0; off < total; off += 64) {
|
||||||
for (let i = 0; i < 16; i++) W_BUF[i] = dv.getUint32(off + i * 4, false);
|
for (let i = 0; i < 16; i++) W_BUF[i] = dv.getUint32(off + i * 4, false);
|
||||||
for (let i = 16; i < 64; i++) {
|
for (let i = 16; i < 64; i++) {
|
||||||
const a = W_BUF[i - 15], b = W_BUF[i - 2];
|
const a = W_BUF[i - 15], b = W_BUF[i - 2];
|
||||||
const s0 = ((a >>> 7) | (a << 25)) ^ ((a >>> 18) | (a << 14)) ^ (a >>> 3);
|
const s0 = ((a >>> 7) | (a << 25)) ^ ((a >>> 18) | (a << 14)) ^ (a >>> 3);
|
||||||
const s1 = ((b >>> 17) | (b << 15)) ^ ((b >>> 19) | (b << 13)) ^ (b >>> 10);
|
const s1 = ((b >>> 17) | (b << 15)) ^ ((b >>> 19) | (b << 13)) ^ (b >>> 10);
|
||||||
W_BUF[i] = (W_BUF[i - 16] + s0 + W_BUF[i - 7] + s1) | 0;
|
W_BUF[i] = (W_BUF[i - 16] + s0 + W_BUF[i - 7] + s1) | 0;
|
||||||
}
|
}
|
||||||
let a = h0, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, h = h7;
|
let a = h0, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, h = h7;
|
||||||
for (let i = 0; i < 64; i++) {
|
for (let i = 0; i < 64; i++) {
|
||||||
const S1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7));
|
const S1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7));
|
||||||
const ch = (e & f) ^ (~e & g);
|
const ch = (e & f) ^ (~e & g);
|
||||||
const t1 = (h + S1 + ch + K[i] + W_BUF[i]) | 0;
|
const t1 = (h + S1 + ch + K[i] + W_BUF[i]) | 0;
|
||||||
const S0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10));
|
const S0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10));
|
||||||
const maj = (a & b) ^ (a & c) ^ (b & c);
|
const maj = (a & b) ^ (a & c) ^ (b & c);
|
||||||
const t2 = (S0 + maj) | 0;
|
const t2 = (S0 + maj) | 0;
|
||||||
h = g; g = f; f = e; e = (d + t1) | 0; d = c; c = b; b = a; a = (t1 + t2) | 0;
|
h = g; g = f; f = e; e = (d + t1) | 0; d = c; c = b; b = a; a = (t1 + t2) | 0;
|
||||||
}
|
}
|
||||||
h0 = (h0 + a) | 0; h1 = (h1 + b) | 0; h2 = (h2 + c) | 0; h3 = (h3 + d) | 0; h4 = (h4 + e) | 0; h5 = (h5 + f) | 0; h6 = (h6 + g) | 0; h7 = (h7 + h) | 0;
|
h0 = (h0 + a) | 0; h1 = (h1 + b) | 0; h2 = (h2 + c) | 0; h3 = (h3 + d) | 0; h4 = (h4 + e) | 0; h5 = (h5 + f) | 0; h6 = (h6 + g) | 0; h7 = (h7 + h) | 0;
|
||||||
}
|
}
|
||||||
const odv = new DataView(out.buffer, out.byteOffset, 32);
|
const odv = new DataView(out.buffer, out.byteOffset, 32);
|
||||||
odv.setUint32(0, h0 >>> 0, false); odv.setUint32(4, h1 >>> 0, false); odv.setUint32(8, h2 >>> 0, false); odv.setUint32(12, h3 >>> 0, false); odv.setUint32(16, h4 >>> 0, false); odv.setUint32(20, h5 >>> 0, false); odv.setUint32(24, h6 >>> 0, false); odv.setUint32(28, h7 >>> 0, false);
|
odv.setUint32(0, h0 >>> 0, false); odv.setUint32(4, h1 >>> 0, false); odv.setUint32(8, h2 >>> 0, false); odv.setUint32(12, h3 >>> 0, false); odv.setUint32(16, h4 >>> 0, false); odv.setUint32(20, h5 >>> 0, false); odv.setUint32(24, h6 >>> 0, false); odv.setUint32(28, h7 >>> 0, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function solvePoW(challenge, difficulty) {
|
function solvePoW(challenge, difficulty) {
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
const cBuf = encoder.encode(challenge);
|
const cBuf = encoder.encode(challenge);
|
||||||
const maxLen = cBuf.length + 16;
|
const maxLen = cBuf.length + 16;
|
||||||
const total = (maxLen + 1 + 8 + 63) & ~63;
|
const total = (maxLen + 1 + 8 + 63) & ~63;
|
||||||
const buf = new Uint8Array(total);
|
const buf = new Uint8Array(total);
|
||||||
buf.set(cBuf);
|
buf.set(cBuf);
|
||||||
const hash = new Uint8Array(32);
|
const hash = new Uint8Array(32);
|
||||||
const maskBytes = Math.floor(difficulty / 8);
|
const maskBytes = Math.floor(difficulty / 8);
|
||||||
const maskBits = difficulty % 8;
|
const maskBits = difficulty % 8;
|
||||||
const finalMask = maskBits > 0 ? (0xff << (8 - maskBits)) & 0xff : 0;
|
const finalMask = maskBits > 0 ? (0xff << (8 - maskBits)) & 0xff : 0;
|
||||||
const dv = new DataView(buf.buffer);
|
const dv = new DataView(buf.buffer);
|
||||||
for (let nonce = 0; nonce < 10000000; nonce++) {
|
for (let nonce = 0; nonce < 10000000; nonce++) {
|
||||||
const str = nonce.toString();
|
const str = nonce.toString();
|
||||||
const len = cBuf.length + str.length;
|
const len = cBuf.length + str.length;
|
||||||
for (let i = 0; i < str.length; i++) buf[cBuf.length + i] = str.charCodeAt(i);
|
for (let i = 0; i < str.length; i++) buf[cBuf.length + i] = str.charCodeAt(i);
|
||||||
buf[len] = 0x80;
|
buf[len] = 0x80;
|
||||||
for (let i = len + 1; i < total - 8; i++) buf[i] = 0;
|
for (let i = len + 1; i < total - 8; i++) buf[i] = 0;
|
||||||
dv.setUint32(total - 4, len * 8, false);
|
dv.setUint32(total - 4, len * 8, false);
|
||||||
dv.setUint32(total - 8, 0, false);
|
dv.setUint32(total - 8, 0, false);
|
||||||
sha256Into(buf, total, hash);
|
sha256Into(buf, total, hash);
|
||||||
let ok = true;
|
let ok = true;
|
||||||
for (let i = 0; i < maskBytes; i++) if (hash[i] !== 0) { ok = false; break; }
|
for (let i = 0; i < maskBytes; i++) if (hash[i] !== 0) { ok = false; break; }
|
||||||
if (ok && (finalMask === 0 || (hash[maskBytes] & finalMask) === 0)) return { found: true, nonce: str };
|
if (ok && (finalMask === 0 || (hash[maskBytes] & finalMask) === 0)) return { found: true, nonce: str };
|
||||||
}
|
}
|
||||||
return { found: false };
|
return { found: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const SERVER_LIST = [{ id: "mapple", name: "Mapple" }, { id: "s1", name: "Nexus" }, { id: "s2", name: "Cipher" }, { id: "s3", name: "Pulse" }, { id: "s4", name: "Vertex" }, { id: "s10", name: "Chimp" }];
|
const SERVER_LIST = [{ id: "mapple", name: "Mapple" }, { id: "s1", name: "Nexus" }, { id: "s2", name: "Cipher" }, { id: "s3", name: "Pulse" }, { id: "s4", name: "Vertex" }, { id: "s10", name: "Chimp" }];
|
||||||
|
|
||||||
export async function getStream(args) {
|
export async function getStream(args) {
|
||||||
const { id, s, e, server } = args;
|
const { id, s, e, server } = args;
|
||||||
const isTv = s != null && e != null;
|
const isTv = s != null && e != null;
|
||||||
const mediaType = isTv ? "tv" : "movie";
|
const mediaType = isTv ? "tv" : "movie";
|
||||||
const tvSlug = isTv ? `${s}-${e}` : "";
|
const tvSlug = isTv ? `${s}-${e}` : "";
|
||||||
const pageUrl = isTv ? `${BASE_URL}/watch/tv/${id}/${s}/${e}` : `${BASE_URL}/watch/movie/${id}`;
|
const pageUrl = isTv ? `${BASE_URL}/watch/tv/${id}/${s}/${e}` : `${BASE_URL}/watch/movie/${id}`;
|
||||||
try {
|
try {
|
||||||
const pageRes = await fetch(pageUrl, { headers: PROXY_HEADERS, signal: AbortSignal.timeout(10000) });
|
const pageRes = await fetch(pageUrl, { headers: PROXY_HEADERS, signal: AbortSignal.timeout(10000) });
|
||||||
if (!pageRes.ok) return null;
|
if (!pageRes.ok) return null;
|
||||||
const html = await pageRes.text();
|
const html = await pageRes.text();
|
||||||
const reqMatch = html.match(/window\.__REQUEST_TOKEN__\s*=\s*"([^"]+)"/);
|
const reqMatch = html.match(/window\.__REQUEST_TOKEN__\s*=\s*"([^"]+)"/);
|
||||||
if (!reqMatch) return null;
|
if (!reqMatch) return null;
|
||||||
const requestToken = reqMatch[1];
|
const requestToken = reqMatch[1];
|
||||||
const initRes = await fetch(`${BASE_URL}/api/playback-init`, { method: "POST", headers: { "Content-Type": "application/json", ...PROXY_HEADERS, Referer: pageUrl }, body: JSON.stringify({ mediaId: Number(id), mediaType, requestToken }), signal: AbortSignal.timeout(10000) });
|
const initRes = await fetch(`${BASE_URL}/api/playback-init`, { method: "POST", headers: { "Content-Type": "application/json", ...PROXY_HEADERS, Referer: pageUrl }, body: JSON.stringify({ mediaId: Number(id), mediaType, requestToken }), signal: AbortSignal.timeout(10000) });
|
||||||
const initData = await initRes.json();
|
const initData = await initRes.json();
|
||||||
let streamToken = initData.token;
|
let streamToken = initData.token;
|
||||||
if (initData.requiresPow && initData.pow) {
|
if (initData.requiresPow && initData.pow) {
|
||||||
const powRes = solvePoW(initData.pow.challenge, initData.pow.difficulty);
|
const powRes = solvePoW(initData.pow.challenge, initData.pow.difficulty);
|
||||||
if (powRes.found) {
|
if (powRes.found) {
|
||||||
const solveRes = await fetch(`${BASE_URL}/api/playback-init`, { method: "POST", headers: { "Content-Type": "application/json", ...PROXY_HEADERS, Referer: pageUrl }, body: JSON.stringify({ mediaId: Number(id), mediaType, requestToken, pow: { challengeId: initData.pow.challengeId, nonce: powRes.nonce } }), signal: AbortSignal.timeout(10000) });
|
const solveRes = await fetch(`${BASE_URL}/api/playback-init`, { method: "POST", headers: { "Content-Type": "application/json", ...PROXY_HEADERS, Referer: pageUrl }, body: JSON.stringify({ mediaId: Number(id), mediaType, requestToken, pow: { challengeId: initData.pow.challengeId, nonce: powRes.nonce } }), signal: AbortSignal.timeout(10000) });
|
||||||
streamToken = (await solveRes.json()).token;
|
streamToken = (await solveRes.json()).token;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!streamToken) return null;
|
if (!streamToken) return null;
|
||||||
let targets = SERVER_LIST;
|
let targets = SERVER_LIST;
|
||||||
if (server && server !== "all") {
|
if (server && server !== "all") {
|
||||||
const clean = server.replace("Mapple - ", "");
|
const clean = server.replace("Mapple - ", "");
|
||||||
targets = SERVER_LIST.filter(sv => sv.name === clean);
|
targets = SERVER_LIST.filter(sv => sv.name === clean);
|
||||||
if (!targets.length) targets = SERVER_LIST;
|
if (!targets.length) targets = SERVER_LIST;
|
||||||
}
|
}
|
||||||
const allUrls = [];
|
const allUrls = [];
|
||||||
for (const srv of targets) {
|
for (const srv of targets) {
|
||||||
try {
|
try {
|
||||||
const streamUrl = `${BASE_URL}/api/stream?mediaId=${id}&mediaType=${mediaType}&tv_slug=${tvSlug}&source=${srv.id}&apikey=mptv_sk_a8f29c4e7b3d1f&requestToken=${requestToken}&token=${streamToken}`;
|
const streamUrl = `${BASE_URL}/api/stream?mediaId=${id}&mediaType=${mediaType}&tv_slug=${tvSlug}&source=${srv.id}&apikey=mptv_sk_a8f29c4e7b3d1f&requestToken=${requestToken}&token=${streamToken}`;
|
||||||
const streamRes = await fetch(streamUrl, { headers: { ...PROXY_HEADERS, Referer: pageUrl }, signal: AbortSignal.timeout(8000) });
|
const streamRes = await fetch(streamUrl, { headers: { ...PROXY_HEADERS, Referer: pageUrl }, signal: AbortSignal.timeout(8000) });
|
||||||
const streamData = await streamRes.json();
|
const streamData = await streamRes.json();
|
||||||
if (streamData.success && streamData.data?.stream_url) {
|
if (streamData.success && streamData.data?.stream_url) {
|
||||||
let fileUrl = streamData.data.stream_url;
|
let fileUrl = streamData.data.stream_url;
|
||||||
if (fileUrl.includes('omena-puu') || fileUrl.includes('nocach')) fileUrl += fileUrl.includes('?') ? '&format=.m3u8' : '?format=.m3u8';
|
if (fileUrl.includes('omena-puu') || fileUrl.includes('nocach')) fileUrl += fileUrl.includes('?') ? '&format=.m3u8' : '?format=.m3u8';
|
||||||
allUrls.push({ url: fileUrl, server: `Mapple - ${srv.name}`, headers: PROXY_HEADERS, skipProxy: false, skipVerify: true, skipHlsCheck: true });
|
allUrls.push({ url: fileUrl, server: `Mapple - ${srv.name}`, headers: PROXY_HEADERS, skipProxy: false, skipVerify: true, skipHlsCheck: true });
|
||||||
}
|
}
|
||||||
} catch { }
|
} catch { }
|
||||||
}
|
}
|
||||||
return allUrls.length ? { allUrls } : null;
|
return allUrls.length ? { allUrls } : null;
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSources() { return SERVER_LIST.map(s => `Mapple - ${s.name}`); }
|
export async function getSources() { return SERVER_LIST.map(s => `Mapple - ${s.name}`); }
|
||||||
export const SKIP_VERIFY = true;
|
export const SKIP_VERIFY = true;
|
||||||
export const MULTI_URL = true;
|
export const MULTI_URL = true;
|
||||||
@@ -1,111 +1,111 @@
|
|||||||
import { fetchJson, USER_AGENT } from '../utils/helpers.js';
|
import { fetchJson, USER_AGENT } from '../utils/helpers.js';
|
||||||
|
|
||||||
const API_BASE = 'https://api.meowtv.ru';
|
const API_BASE = 'https://api.meowtv.ru';
|
||||||
const REFERER = 'https://meowtv.ru/';
|
const REFERER = 'https://meowtv.ru/';
|
||||||
const ENC_DEC_API = 'https://enc-dec.app/api/dec-meowtv';
|
const ENC_DEC_API = 'https://enc-dec.app/api/dec-meowtv';
|
||||||
|
|
||||||
const HEADERS = {
|
const HEADERS = {
|
||||||
'User-Agent': USER_AGENT,
|
'User-Agent': USER_AGENT,
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
'Referer': REFERER,
|
'Referer': REFERER,
|
||||||
'Origin': 'https://meowtv.ru',
|
'Origin': 'https://meowtv.ru',
|
||||||
'Accept-Language': 'en-US,en;q=0.9'
|
'Accept-Language': 'en-US,en;q=0.9'
|
||||||
};
|
};
|
||||||
|
|
||||||
const SERVERS = [
|
const SERVERS = [
|
||||||
'pseudo',
|
'pseudo',
|
||||||
'lynx',
|
'lynx',
|
||||||
'tik',
|
'tik',
|
||||||
'ipcloud',
|
'ipcloud',
|
||||||
'v4:English',
|
'v4:English',
|
||||||
'turkce',
|
'turkce',
|
||||||
'v5:Hindi',
|
'v5:Hindi',
|
||||||
'v4:Hindi',
|
'v4:Hindi',
|
||||||
'v6:Hindi'
|
'v6:Hindi'
|
||||||
];
|
];
|
||||||
|
|
||||||
const TIMEOUT_MS = 15000;
|
const TIMEOUT_MS = 15000;
|
||||||
|
|
||||||
export const VERIFY_HEADERS = { 'User-Agent': USER_AGENT, 'Referer': REFERER, 'Origin': 'https://meowtv.ru' };
|
export const VERIFY_HEADERS = { 'User-Agent': USER_AGENT, 'Referer': REFERER, 'Origin': 'https://meowtv.ru' };
|
||||||
|
|
||||||
async function decryptPayload(payload) {
|
async function decryptPayload(payload) {
|
||||||
try {
|
try {
|
||||||
const response = await fetchJson(ENC_DEC_API, {
|
const response = await fetchJson(ENC_DEC_API, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ data: payload })
|
body: JSON.stringify({ data: payload })
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response?.status !== 200 || !response?.result) {
|
if (response?.status !== 200 || !response?.result) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return typeof response.result === 'string' ? JSON.parse(response.result) : response.result;
|
return typeof response.result === 'string' ? JSON.parse(response.result) : response.result;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStream({ id, s, e, server }) {
|
export async function getStream({ id, s, e, server }) {
|
||||||
try {
|
try {
|
||||||
const isTv = !!s;
|
const isTv = !!s;
|
||||||
let targets = SERVERS;
|
let targets = SERVERS;
|
||||||
if (server && server !== 'all') {
|
if (server && server !== 'all') {
|
||||||
const cleanName = server.replace('MeowTV - ', '');
|
const cleanName = server.replace('MeowTV - ', '');
|
||||||
targets = SERVERS.includes(cleanName) ? [cleanName] : [targets[0]];
|
targets = SERVERS.includes(cleanName) ? [cleanName] : [targets[0]];
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = await Promise.all(targets.map(async srv => {
|
const results = await Promise.all(targets.map(async srv => {
|
||||||
try {
|
try {
|
||||||
return await Promise.race([
|
return await Promise.race([
|
||||||
(async () => {
|
(async () => {
|
||||||
const path = isTv
|
const path = isTv
|
||||||
? `/streams/tv/${id}/${s}/${e}?s=${encodeURIComponent(srv)}`
|
? `/streams/tv/${id}/${s}/${e}?s=${encodeURIComponent(srv)}`
|
||||||
: `/streams/movie/${id}?s=${encodeURIComponent(srv)}`;
|
: `/streams/movie/${id}?s=${encodeURIComponent(srv)}`;
|
||||||
|
|
||||||
const payload = await fetchJson(`${API_BASE}${path}`, {
|
const payload = await fetchJson(`${API_BASE}${path}`, {
|
||||||
headers: HEADERS
|
headers: HEADERS
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!payload) return null;
|
if (!payload) return null;
|
||||||
|
|
||||||
const data = await decryptPayload(payload);
|
const data = await decryptPayload(payload);
|
||||||
if (!data) return null;
|
if (!data) return null;
|
||||||
|
|
||||||
const urls = [];
|
const urls = [];
|
||||||
if (data?.url?.startsWith('http')) {
|
if (data?.url?.startsWith('http')) {
|
||||||
urls.push({
|
urls.push({
|
||||||
url: data.url,
|
url: data.url,
|
||||||
server: `MeowTV - ${srv}`,
|
server: `MeowTV - ${srv}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(data?.streams)) {
|
if (Array.isArray(data?.streams)) {
|
||||||
for (const stream of data.streams) {
|
for (const stream of data.streams) {
|
||||||
if (stream?.url?.startsWith('http')) {
|
if (stream?.url?.startsWith('http')) {
|
||||||
urls.push({
|
urls.push({
|
||||||
url: stream.url,
|
url: stream.url,
|
||||||
server: `MeowTV - ${srv} (${stream.language || 'Unknown'})`,
|
server: `MeowTV - ${srv} (${stream.language || 'Unknown'})`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return urls;
|
return urls;
|
||||||
})(),
|
})(),
|
||||||
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout exceeded')), TIMEOUT_MS))
|
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout exceeded')), TIMEOUT_MS))
|
||||||
]);
|
]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const allUrls = results.filter(Boolean).flat();
|
const allUrls = results.filter(Boolean).flat();
|
||||||
return allUrls.length ? { allUrls } : null;
|
return allUrls.length ? { allUrls } : null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSources() {
|
export async function getSources() {
|
||||||
return SERVERS.map(s => `MeowTV - ${s}`);
|
return SERVERS.map(s => `MeowTV - ${s}`);
|
||||||
}
|
}
|
||||||
@@ -1,62 +1,62 @@
|
|||||||
import { fetchJson } from '../utils/helpers.js';
|
import { fetchJson } from '../utils/helpers.js';
|
||||||
|
|
||||||
const BASE_URL = 'https://nebula.work.gd/private/7f6b9d179ba7ecd77a39520e';
|
const BASE_URL = 'https://nebula.work.gd/private/7f6b9d179ba7ecd77a39520e';
|
||||||
|
|
||||||
async function getImdbId(tmdbApiKey, tmdbId, s, e) {
|
async function getImdbId(tmdbApiKey, tmdbId, s, e) {
|
||||||
const key = tmdbApiKey;
|
const key = tmdbApiKey;
|
||||||
if (!key) return null;
|
if (!key) return null;
|
||||||
try {
|
try {
|
||||||
const type = s != null && e != null ? 'tv' : 'movie';
|
const type = s != null && e != null ? 'tv' : 'movie';
|
||||||
const url = `https://api.themoviedb.org/3/${type}/${tmdbId}/external_ids?api_key=${key}`;
|
const url = `https://api.themoviedb.org/3/${type}/${tmdbId}/external_ids?api_key=${key}`;
|
||||||
const res = await fetchJson(url);
|
const res = await fetchJson(url);
|
||||||
return res?.imdb_id || null;
|
return res?.imdb_id || null;
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStream({ id, s, e, tmdbApiKey }) {
|
export async function getStream({ id, s, e, tmdbApiKey }) {
|
||||||
try {
|
try {
|
||||||
const imdbId = await getImdbId(tmdbApiKey, id, s, e);
|
const imdbId = await getImdbId(tmdbApiKey, id, s, e);
|
||||||
if (!imdbId) return null;
|
if (!imdbId) return null;
|
||||||
|
|
||||||
const path = s != null && e != null
|
const path = s != null && e != null
|
||||||
? `/stream/series/${imdbId}:${s}:${e}.json`
|
? `/stream/series/${imdbId}:${s}:${e}.json`
|
||||||
: `/stream/movie/${imdbId}.json`;
|
: `/stream/movie/${imdbId}.json`;
|
||||||
|
|
||||||
const response = await fetch(`${BASE_URL}${path}`, {
|
const response = await fetch(`${BASE_URL}${path}`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json'
|
'Accept': 'application/json'
|
||||||
},
|
},
|
||||||
signal: AbortSignal.timeout(15000)
|
signal: AbortSignal.timeout(15000)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) return null;
|
if (!response.ok) return null;
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (!data?.streams?.length) return null;
|
if (!data?.streams?.length) return null;
|
||||||
|
|
||||||
const allUrls = data.streams
|
const allUrls = data.streams
|
||||||
.filter(stream => stream.url && stream.url.trim() !== '' && !stream.url.includes('hubcloud'))
|
.filter(stream => stream.url && stream.url.trim() !== '' && !stream.url.includes('hubcloud'))
|
||||||
.map(stream => {
|
.map(stream => {
|
||||||
const headers = stream.behaviorHints?.proxyHeaders?.request || {};
|
const headers = stream.behaviorHints?.proxyHeaders?.request || {};
|
||||||
|
|
||||||
let serverName = stream.name || 'Unknown';
|
let serverName = stream.name || 'Unknown';
|
||||||
if (serverName.includes('\n')) serverName = serverName.split('\n')[0];
|
if (serverName.includes('\n')) serverName = serverName.split('\n')[0];
|
||||||
|
|
||||||
const filename = stream.filename || stream.behaviorHints?.filename;
|
const filename = stream.filename || stream.behaviorHints?.filename;
|
||||||
if (filename) serverName += ` - ${filename}`;
|
if (filename) serverName += ` - ${filename}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url: stream.url,
|
url: stream.url,
|
||||||
server: `Nebula - ${serverName}`,
|
server: `Nebula - ${serverName}`,
|
||||||
type: stream.url.includes('.m3u8') ? 'hls' : 'mp4',
|
type: stream.url.includes('.m3u8') ? 'hls' : 'mp4',
|
||||||
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
||||||
skipProxy: stream.behaviorHints?.notWebReady === false
|
skipProxy: stream.behaviorHints?.notWebReady === false
|
||||||
};
|
};
|
||||||
}).filter(Boolean);
|
}).filter(Boolean);
|
||||||
return allUrls.length ? { allUrls } : null;
|
return allUrls.length ? { allUrls } : null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,57 +1,57 @@
|
|||||||
import { fetchJson, fetchText, getTmdbInfo, tmdbToAnilist } from '../utils/helpers.js';
|
import { fetchJson, fetchText, getTmdbInfo, tmdbToAnilist } from '../utils/helpers.js';
|
||||||
|
|
||||||
const BASE_URL = "https://nekowatch.xyz";
|
const BASE_URL = "https://nekowatch.xyz";
|
||||||
const ANIME_JS_URL = `${BASE_URL}/js/anime.js?v=v263_fast_player_start`;
|
const ANIME_JS_URL = `${BASE_URL}/js/anime.js?v=v263_fast_player_start`;
|
||||||
const FALLBACK_ORDER = ['kiwi', 'ally', 'bee', 'arc', 'anikoto', 'jet', 'bonk', 'moo', 'hop', 'pulsar', 'pahe'];
|
const FALLBACK_ORDER = ['kiwi', 'ally', 'bee', 'arc', 'anikoto', 'jet', 'bonk', 'moo', 'hop', 'pulsar', 'pahe'];
|
||||||
|
|
||||||
let providerOrderCache = null, providerOrderTs = 0;
|
let providerOrderCache = null, providerOrderTs = 0;
|
||||||
|
|
||||||
async function getProviderOrder() {
|
async function getProviderOrder() {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (providerOrderCache && now - providerOrderTs < 1800000) return providerOrderCache;
|
if (providerOrderCache && now - providerOrderTs < 1800000) return providerOrderCache;
|
||||||
try {
|
try {
|
||||||
const js = await fetchText(ANIME_JS_URL, { signal: AbortSignal.timeout(6000) });
|
const js = await fetchText(ANIME_JS_URL, { signal: AbortSignal.timeout(6000) });
|
||||||
const match = js.match(/PROVIDER_ORDER\s*=\s*\[([\s\S]*?)\]/);
|
const match = js.match(/PROVIDER_ORDER\s*=\s*\[([\s\S]*?)\]/);
|
||||||
if (match) {
|
if (match) {
|
||||||
const order = [...match[1].matchAll(/['"]([a-z0-9_]+)['"]/gi)].map(m => m[1]);
|
const order = [...match[1].matchAll(/['"]([a-z0-9_]+)['"]/gi)].map(m => m[1]);
|
||||||
if (order.length) { providerOrderCache = order; providerOrderTs = now; return order; }
|
if (order.length) { providerOrderCache = order; providerOrderTs = now; return order; }
|
||||||
}
|
}
|
||||||
} catch { }
|
} catch { }
|
||||||
return providerOrderCache || FALLBACK_ORDER;
|
return providerOrderCache || FALLBACK_ORDER;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStream({ id, s, e, audio }) {
|
export async function getStream({ id, s, e, audio }) {
|
||||||
try {
|
try {
|
||||||
const info = await getTmdbInfo(id, s ? 'tv' : 'movie', s);
|
const info = await getTmdbInfo(id, s ? 'tv' : 'movie', s);
|
||||||
if (!info || !info.isAnime) return null;
|
if (!info || !info.isAnime) return null;
|
||||||
const anilistId = await tmdbToAnilist(id, s ? 'tv' : 'movie', s, info.titles, info.year);
|
const anilistId = await tmdbToAnilist(id, s ? 'tv' : 'movie', s, info.titles, info.year);
|
||||||
if (!anilistId) return null;
|
if (!anilistId) return null;
|
||||||
const ep = e || 1;
|
const ep = e || 1;
|
||||||
const audiosToTry = audio === "all" ? ["sub", "dub"] : (audio === "dub" ? ["dub", "sub"] : ["sub", "dub"]);
|
const audiosToTry = audio === "all" ? ["sub", "dub"] : (audio === "dub" ? ["dub", "sub"] : ["sub", "dub"]);
|
||||||
const providerOrder = await getProviderOrder();
|
const providerOrder = await getProviderOrder();
|
||||||
for (const aud of audiosToTry) {
|
for (const aud of audiosToTry) {
|
||||||
const results = await Promise.allSettled(providerOrder.map(p => fetchJson(`${BASE_URL}/api/anime/watch/${p}/${anilistId}/${aud}/${p}-${ep}`, { signal: AbortSignal.timeout(8000) })));
|
const results = await Promise.allSettled(providerOrder.map(p => fetchJson(`${BASE_URL}/api/anime/watch/${p}/${anilistId}/${aud}/${p}-${ep}`, { signal: AbortSignal.timeout(8000) })));
|
||||||
const allUrls = [], seenActual = new Set();
|
const allUrls = [], seenActual = new Set();
|
||||||
for (const r of results) {
|
for (const r of results) {
|
||||||
if (r.status !== 'fulfilled' || !r.value) continue;
|
if (r.status !== 'fulfilled' || !r.value) continue;
|
||||||
const data = r.value;
|
const data = r.value;
|
||||||
let streams = [], actual = data.attempted?.actualProvider || data.provider;
|
let streams = [], actual = data.attempted?.actualProvider || data.provider;
|
||||||
if (Array.isArray(data.streams)) streams = data.streams;
|
if (Array.isArray(data.streams)) streams = data.streams;
|
||||||
else {
|
else {
|
||||||
for (const key of Object.keys(data)) if (data[key] && Array.isArray(data[key].streams)) { streams.push(...data[key].streams); if (!actual) actual = data[key].provider || key; }
|
for (const key of Object.keys(data)) if (data[key] && Array.isArray(data[key].streams)) { streams.push(...data[key].streams); if (!actual) actual = data[key].provider || key; }
|
||||||
}
|
}
|
||||||
if (!streams.length) continue;
|
if (!streams.length) continue;
|
||||||
if (actual) { if (seenActual.has(actual)) continue; seenActual.add(actual); }
|
if (actual) { if (seenActual.has(actual)) continue; seenActual.add(actual); }
|
||||||
const valid = streams.filter(st => st.type === "hls" && st.url).sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
const valid = streams.filter(st => st.type === "hls" && st.url).sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
||||||
for (const st of valid) allUrls.push({ url: st.url, type: "hls", audio: aud, server: st.server ? `NekoWatch-${st.server}` : "NekoWatch", headers: st.referer ? { Referer: st.referer } : undefined, skipProxy: /\.workers\.dev(\/|$)/i.test(st.url) });
|
for (const st of valid) allUrls.push({ url: st.url, type: "hls", audio: aud, server: st.server ? `NekoWatch-${st.server}` : "NekoWatch", headers: st.referer ? { Referer: st.referer } : undefined, skipProxy: /\.workers\.dev(\/|$)/i.test(st.url) });
|
||||||
}
|
}
|
||||||
if (allUrls.length) return { allUrls };
|
if (allUrls.length) return { allUrls };
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSources(args) {
|
export async function getSources(args) {
|
||||||
const stream = await getStream(args);
|
const stream = await getStream(args);
|
||||||
return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server))] : [];
|
return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server))] : [];
|
||||||
}
|
}
|
||||||
@@ -1,123 +1,123 @@
|
|||||||
import { getTmdbInfo, USER_AGENT } from '../utils/helpers.js';
|
import { getTmdbInfo, USER_AGENT } from '../utils/helpers.js';
|
||||||
|
|
||||||
const BASE_URL = 'https://opstream.fun';
|
const BASE_URL = 'https://opstream.fun';
|
||||||
const API_BASE = `${BASE_URL}/api/resolve`;
|
const API_BASE = `${BASE_URL}/api/resolve`;
|
||||||
|
|
||||||
const API_HEADERS = {
|
const API_HEADERS = {
|
||||||
'User-Agent': USER_AGENT,
|
'User-Agent': USER_AGENT,
|
||||||
'Accept': 'application/x-ndjson; charset=utf-8',
|
'Accept': 'application/x-ndjson; charset=utf-8',
|
||||||
'Referer': `${BASE_URL}/`,
|
'Referer': `${BASE_URL}/`,
|
||||||
'Origin': BASE_URL,
|
'Origin': BASE_URL,
|
||||||
};
|
};
|
||||||
|
|
||||||
const STREAM_HEADERS = {
|
const STREAM_HEADERS = {
|
||||||
'User-Agent': USER_AGENT,
|
'User-Agent': USER_AGENT,
|
||||||
'Referer': `${BASE_URL}/`,
|
'Referer': `${BASE_URL}/`,
|
||||||
'Origin': BASE_URL,
|
'Origin': BASE_URL,
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getStream({ id, s, e }) {
|
export async function getStream({ id, s, e }) {
|
||||||
try {
|
try {
|
||||||
const isTv = s != null && e != null;
|
const isTv = s != null && e != null;
|
||||||
const info = await getTmdbInfo(id, isTv ? 'tv' : 'movie');
|
const info = await getTmdbInfo(id, isTv ? 'tv' : 'movie');
|
||||||
|
|
||||||
if (!info?.titles?.length || !info.imdbId) return null;
|
if (!info?.titles?.length || !info.imdbId) return null;
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
type: isTv ? 'tv' : 'movie',
|
type: isTv ? 'tv' : 'movie',
|
||||||
tmdbId: id,
|
tmdbId: id,
|
||||||
title: info.titles[0],
|
title: info.titles[0],
|
||||||
year: info.year || '',
|
year: info.year || '',
|
||||||
imdbId: info.imdbId,
|
imdbId: info.imdbId,
|
||||||
progress: '1'
|
progress: '1'
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isTv) {
|
if (isTv) {
|
||||||
params.append('season', s);
|
params.append('season', s);
|
||||||
params.append('episode', e);
|
params.append('episode', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE}?${params}`, {
|
const res = await fetch(`${API_BASE}?${params}`, {
|
||||||
headers: API_HEADERS,
|
headers: API_HEADERS,
|
||||||
signal: AbortSignal.timeout(15000)
|
signal: AbortSignal.timeout(15000)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) return null;
|
if (!res.ok) return null;
|
||||||
|
|
||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
const lines = text.split('\n');
|
const lines = text.split('\n');
|
||||||
|
|
||||||
let streamData = null;
|
let streamData = null;
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
if (!trimmed) continue;
|
if (!trimmed) continue;
|
||||||
try {
|
try {
|
||||||
const json = JSON.parse(trimmed);
|
const json = JSON.parse(trimmed);
|
||||||
if (json.t === 'done' && json.data) {
|
if (json.t === 'done' && json.data) {
|
||||||
streamData = json.data;
|
streamData = json.data;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch { continue; }
|
} catch { continue; }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!streamData) return null;
|
if (!streamData) return null;
|
||||||
|
|
||||||
const subtitles = (streamData.captions || []).map(c => ({
|
const subtitles = (streamData.captions || []).map(c => ({
|
||||||
url: c.url.startsWith('http') ? c.url : `${BASE_URL}${c.url}`,
|
url: c.url.startsWith('http') ? c.url : `${BASE_URL}${c.url}`,
|
||||||
lang: c.label || 'Unknown'
|
lang: c.label || 'Unknown'
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const allUrls = [];
|
const allUrls = [];
|
||||||
|
|
||||||
if (streamData.qualities && streamData.qualities.length > 0) {
|
if (streamData.qualities && streamData.qualities.length > 0) {
|
||||||
for (const q of streamData.qualities) {
|
for (const q of streamData.qualities) {
|
||||||
let finalUrl = q.url;
|
let finalUrl = q.url;
|
||||||
if (!finalUrl.startsWith('http')) {
|
if (!finalUrl.startsWith('http')) {
|
||||||
finalUrl = `${BASE_URL}${finalUrl}`;
|
finalUrl = `${BASE_URL}${finalUrl}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDash = finalUrl.includes('.mpd');
|
const isDash = finalUrl.includes('.mpd');
|
||||||
const isHls = finalUrl.includes('.m3u8');
|
const isHls = finalUrl.includes('.m3u8');
|
||||||
|
|
||||||
allUrls.push({
|
allUrls.push({
|
||||||
url: finalUrl,
|
url: finalUrl,
|
||||||
server: `OpStream - ${q.label}`,
|
server: `OpStream - ${q.label}`,
|
||||||
type: isDash ? 'dash' : (isHls ? 'hls' : 'mp4'),
|
type: isDash ? 'dash' : (isHls ? 'hls' : 'mp4'),
|
||||||
headers: STREAM_HEADERS,
|
headers: STREAM_HEADERS,
|
||||||
subtitles,
|
subtitles,
|
||||||
skipProxy: true
|
skipProxy: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (streamData.url) {
|
} else if (streamData.url) {
|
||||||
let finalUrl = streamData.url;
|
let finalUrl = streamData.url;
|
||||||
if (finalUrl.includes('?u=')) {
|
if (finalUrl.includes('?u=')) {
|
||||||
try {
|
try {
|
||||||
const uParam = new URL(finalUrl, BASE_URL).searchParams.get('u');
|
const uParam = new URL(finalUrl, BASE_URL).searchParams.get('u');
|
||||||
if (uParam) finalUrl = Buffer.from(uParam, 'base64').toString('utf8');
|
if (uParam) finalUrl = Buffer.from(uParam, 'base64').toString('utf8');
|
||||||
} catch {}
|
} catch {}
|
||||||
} else if (!finalUrl.startsWith('http')) {
|
} else if (!finalUrl.startsWith('http')) {
|
||||||
finalUrl = `${BASE_URL}${finalUrl}`;
|
finalUrl = `${BASE_URL}${finalUrl}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDash = streamData.kind === 'dash' || finalUrl.includes('.mpd');
|
const isDash = streamData.kind === 'dash' || finalUrl.includes('.mpd');
|
||||||
const isHls = streamData.kind === 'hls' || finalUrl.includes('.m3u8');
|
const isHls = streamData.kind === 'hls' || finalUrl.includes('.m3u8');
|
||||||
|
|
||||||
allUrls.push({
|
allUrls.push({
|
||||||
url: finalUrl,
|
url: finalUrl,
|
||||||
server: 'OpStream',
|
server: 'OpStream',
|
||||||
type: isDash ? 'dash' : (isHls ? 'hls' : 'mp4'),
|
type: isDash ? 'dash' : (isHls ? 'hls' : 'mp4'),
|
||||||
headers: STREAM_HEADERS,
|
headers: STREAM_HEADERS,
|
||||||
subtitles,
|
subtitles,
|
||||||
skipProxy: true
|
skipProxy: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allUrls.length === 0) return null;
|
if (allUrls.length === 0) return null;
|
||||||
|
|
||||||
return { allUrls };
|
return { allUrls };
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSources() {
|
export async function getSources() {
|
||||||
return ['OpStream'];
|
return ['OpStream'];
|
||||||
}
|
}
|
||||||
@@ -1,78 +1,78 @@
|
|||||||
import { fetchJson, USER_AGENT } from '../utils/helpers.js';
|
import { fetchJson, USER_AGENT } from '../utils/helpers.js';
|
||||||
|
|
||||||
const DEC_API = "https://enc-dec.app/api/dec-peachify";
|
const DEC_API = "https://enc-dec.app/api/dec-peachify";
|
||||||
const ORIGIN = "https://peachify.top";
|
const ORIGIN = "https://peachify.top";
|
||||||
const HEADERS = { "User-Agent": USER_AGENT, "Origin": ORIGIN, "Referer": `${ORIGIN}/` };
|
const HEADERS = { "User-Agent": USER_AGENT, "Origin": ORIGIN, "Referer": `${ORIGIN}/` };
|
||||||
|
|
||||||
const SERVERS = [
|
const SERVERS = [
|
||||||
{ label: "Wolf", path: "air", api: "https://usa.eat-peach.sbs" },
|
{ label: "Wolf", path: "air", api: "https://usa.eat-peach.sbs" },
|
||||||
{ label: "Spider", path: "holly", api: "https://usa.eat-peach.sbs" },
|
{ label: "Spider", path: "holly", api: "https://usa.eat-peach.sbs" },
|
||||||
{ label: "Iron", path: "moviebox", api: "https://uwu.eat-peach.sbs" },
|
{ label: "Iron", path: "moviebox", api: "https://uwu.eat-peach.sbs" },
|
||||||
{ label: "Multi", path: "multi", api: "https://usa.eat-peach.sbs" },
|
{ label: "Multi", path: "multi", api: "https://usa.eat-peach.sbs" },
|
||||||
{ label: "Dark", path: "net", api: "https://uwu.eat-peach.sbs" },
|
{ label: "Dark", path: "net", api: "https://uwu.eat-peach.sbs" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export async function getStream({ id, s, e, server }) {
|
export async function getStream({ id, s, e, server }) {
|
||||||
try {
|
try {
|
||||||
const isTv = s != null && e != null;
|
const isTv = s != null && e != null;
|
||||||
const segment = isTv ? `tv/${id}/${s}/${e}` : `movie/${id}`;
|
const segment = isTv ? `tv/${id}/${s}/${e}` : `movie/${id}`;
|
||||||
|
|
||||||
let targets = SERVERS;
|
let targets = SERVERS;
|
||||||
if (server && server !== 'all') {
|
if (server && server !== 'all') {
|
||||||
const cleanName = server.replace('Peachify - ', '');
|
const cleanName = server.replace('Peachify - ', '');
|
||||||
targets = SERVERS.filter(srv => srv.label === cleanName);
|
targets = SERVERS.filter(srv => srv.label === cleanName);
|
||||||
if (!targets.length) targets = SERVERS;
|
if (!targets.length) targets = SERVERS;
|
||||||
}
|
}
|
||||||
|
|
||||||
const settled = await Promise.allSettled(targets.map(async srv => {
|
const settled = await Promise.allSettled(targets.map(async srv => {
|
||||||
const url = `${srv.api}/${srv.path}/${segment}`;
|
const url = `${srv.api}/${srv.path}/${segment}`;
|
||||||
const rawData = await fetchJson(url, { headers: HEADERS, signal: AbortSignal.timeout(10000) });
|
const rawData = await fetchJson(url, { headers: HEADERS, signal: AbortSignal.timeout(10000) });
|
||||||
|
|
||||||
if (!rawData?.data) throw new Error();
|
if (!rawData?.data) throw new Error();
|
||||||
|
|
||||||
const decJson = await fetchJson(DEC_API, {
|
const decJson = await fetchJson(DEC_API, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ text: rawData.data }),
|
body: JSON.stringify({ text: rawData.data }),
|
||||||
signal: AbortSignal.timeout(10000)
|
signal: AbortSignal.timeout(10000)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (decJson.status !== 200 || !decJson.result) throw new Error();
|
if (decJson.status !== 200 || !decJson.result) throw new Error();
|
||||||
const res = decJson.result;
|
const res = decJson.result;
|
||||||
|
|
||||||
const sources = Array.isArray(res) ? res : (res.sources || [res]);
|
const sources = Array.isArray(res) ? res : (res.sources || [res]);
|
||||||
const subs = res.subtitles || [];
|
const subs = res.subtitles || [];
|
||||||
|
|
||||||
return sources.map(source => {
|
return sources.map(source => {
|
||||||
const streamUrl = source.url || source.file || source.link;
|
const streamUrl = source.url || source.file || source.link;
|
||||||
if (!streamUrl) return null;
|
if (!streamUrl) return null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url: streamUrl,
|
url: streamUrl,
|
||||||
server: `Peachify - ${srv.label}`,
|
server: `Peachify - ${srv.label}`,
|
||||||
quality: source.quality || "Auto",
|
quality: source.quality || "Auto",
|
||||||
type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
|
type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
|
||||||
headers: { ...HEADERS, "Origin": ORIGIN },
|
headers: { ...HEADERS, "Origin": ORIGIN },
|
||||||
subtitles: subs.map(sub => ({
|
subtitles: subs.map(sub => ({
|
||||||
url: sub.url || sub.file,
|
url: sub.url || sub.file,
|
||||||
lang: sub.language || sub.lang || sub.label || 'Unknown'
|
lang: sub.language || sub.lang || sub.label || 'Unknown'
|
||||||
})),
|
})),
|
||||||
skipProxy: false,
|
skipProxy: false,
|
||||||
skipVerify: true,
|
skipVerify: true,
|
||||||
skipHlsCheck: true
|
skipHlsCheck: true
|
||||||
};
|
};
|
||||||
}).filter(Boolean);
|
}).filter(Boolean);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const allUrls = [];
|
const allUrls = [];
|
||||||
for (const r of settled) {
|
for (const r of settled) {
|
||||||
if (r.status === 'fulfilled' && r.value) allUrls.push(...r.value);
|
if (r.status === 'fulfilled' && r.value) allUrls.push(...r.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return allUrls.length ? { allUrls } : null;
|
return allUrls.length ? { allUrls } : null;
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSources() {
|
export async function getSources() {
|
||||||
return SERVERS.map(s => `Peachify - ${s.label}`);
|
return SERVERS.map(s => `Peachify - ${s.label}`);
|
||||||
}
|
}
|
||||||
@@ -1,45 +1,45 @@
|
|||||||
import { fetchJson } from '../utils/helpers.js';
|
import { fetchJson } from '../utils/helpers.js';
|
||||||
|
|
||||||
const PENGU_CONFIG = '%7B%22source_4khdhub%22%3A%22on%22%2C%22source_moviebox%22%3A%22on%22%2C%22source_moviesdrives%22%3A%22on%22%2C%22source_vaplayer%22%3A%22on%22%2C%22source_hdghartv%22%3A%22on%22%2C%22res_2160%22%3A%22on%22%2C%22res_1080%22%3A%22on%22%2C%22res_720%22%3A%22on%22%2C%22disable_direct%22%3A%22on%22%7D';
|
const PENGU_CONFIG = '%7B%22source_4khdhub%22%3A%22on%22%2C%22source_moviebox%22%3A%22on%22%2C%22source_moviesdrives%22%3A%22on%22%2C%22source_vaplayer%22%3A%22on%22%2C%22source_hdghartv%22%3A%22on%22%2C%22res_2160%22%3A%22on%22%2C%22res_1080%22%3A%22on%22%2C%22res_720%22%3A%22on%22%2C%22disable_direct%22%3A%22on%22%7D';
|
||||||
const BASE_URL = `https://pengu.uk/${PENGU_CONFIG}`;
|
const BASE_URL = `https://pengu.uk/${PENGU_CONFIG}`;
|
||||||
|
|
||||||
async function getImdbId(tmdbApiKey, tmdbId, s, e) {
|
async function getImdbId(tmdbApiKey, tmdbId, s, e) {
|
||||||
const key = tmdbApiKey;
|
const key = tmdbApiKey;
|
||||||
if (!key) return null;
|
if (!key) return null;
|
||||||
try {
|
try {
|
||||||
const url = s != null && e != null
|
const url = s != null && e != null
|
||||||
? `https://api.themoviedb.org/3/tv/${tmdbId}/external_ids?api_key=${key}`
|
? `https://api.themoviedb.org/3/tv/${tmdbId}/external_ids?api_key=${key}`
|
||||||
: `https://api.themoviedb.org/3/movie/${tmdbId}?api_key=${key}`;
|
: `https://api.themoviedb.org/3/movie/${tmdbId}?api_key=${key}`;
|
||||||
const res = await fetchJson(url);
|
const res = await fetchJson(url);
|
||||||
return res?.imdb_id || null;
|
return res?.imdb_id || null;
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStream({ id, s, e, tmdbApiKey }) {
|
export async function getStream({ id, s, e, tmdbApiKey }) {
|
||||||
try {
|
try {
|
||||||
const imdbId = await getImdbId(tmdbApiKey, id, s, e);
|
const imdbId = await getImdbId(tmdbApiKey, id, s, e);
|
||||||
if (!imdbId) return null;
|
if (!imdbId) return null;
|
||||||
|
|
||||||
const path = s != null && e != null
|
const path = s != null && e != null
|
||||||
? `/stream/series/${imdbId}:${s}:${e}.json`
|
? `/stream/series/${imdbId}:${s}:${e}.json`
|
||||||
: `/stream/movie/${imdbId}.json`;
|
: `/stream/movie/${imdbId}.json`;
|
||||||
|
|
||||||
const res = await fetchJson(`${BASE_URL}${path}`);
|
const res = await fetchJson(`${BASE_URL}${path}`);
|
||||||
if (!res?.streams?.length) return null;
|
if (!res?.streams?.length) return null;
|
||||||
|
|
||||||
const allUrls = res.streams.map(stream => {
|
const allUrls = res.streams.map(stream => {
|
||||||
if (!stream.url) return null;
|
if (!stream.url) return null;
|
||||||
const headers = stream.behaviorHints?.proxyHeaders?.request || {};
|
const headers = stream.behaviorHints?.proxyHeaders?.request || {};
|
||||||
return {
|
return {
|
||||||
url: stream.url,
|
url: stream.url,
|
||||||
server: `Pengu - ${stream.name || 'Server'}`,
|
server: `Pengu - ${stream.name || 'Server'}`,
|
||||||
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
||||||
skipProxy: stream.behaviorHints?.notWebReady === false
|
skipProxy: stream.behaviorHints?.notWebReady === false
|
||||||
};
|
};
|
||||||
}).filter(Boolean);
|
}).filter(Boolean);
|
||||||
|
|
||||||
return allUrls.length ? { allUrls } : null;
|
return allUrls.length ? { allUrls } : null;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,104 +1,104 @@
|
|||||||
import { fetchJson, USER_AGENT, getTmdbInfo } from '../utils/helpers.js';
|
import { fetchJson, USER_AGENT, getTmdbInfo } from '../utils/helpers.js';
|
||||||
|
|
||||||
const DOMAIN = 'https://purstream.club';
|
const DOMAIN = 'https://purstream.club';
|
||||||
const API_BASE = 'https://api.purstream.club/api/v1';
|
const API_BASE = 'https://api.purstream.club/api/v1';
|
||||||
|
|
||||||
const HEADERS = {
|
const HEADERS = {
|
||||||
'User-Agent': USER_AGENT,
|
'User-Agent': USER_AGENT,
|
||||||
'Accept': 'application/json, text/plain, */*',
|
'Accept': 'application/json, text/plain, */*',
|
||||||
'Referer': `${DOMAIN}/`,
|
'Referer': `${DOMAIN}/`,
|
||||||
'Origin': DOMAIN,
|
'Origin': DOMAIN,
|
||||||
'X-Requested-With': 'XMLHttpRequest',
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
'Sec-Fetch-Dest': 'empty',
|
'Sec-Fetch-Dest': 'empty',
|
||||||
'Sec-Fetch-Mode': 'cors',
|
'Sec-Fetch-Mode': 'cors',
|
||||||
'Sec-Fetch-Site': 'same-site'
|
'Sec-Fetch-Site': 'same-site'
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getStream({ id, s, e }) {
|
export async function getStream({ id, s, e }) {
|
||||||
try {
|
try {
|
||||||
const isTv = s != null;
|
const isTv = s != null;
|
||||||
|
|
||||||
const info = await getTmdbInfo(id, isTv ? 'tv' : 'movie');
|
const info = await getTmdbInfo(id, isTv ? 'tv' : 'movie');
|
||||||
|
|
||||||
|
|
||||||
if (!info?.titles?.length) {
|
if (!info?.titles?.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = info.titles[0];
|
const title = info.titles[0];
|
||||||
|
|
||||||
const searchUrl = `${API_BASE}/search-bar/search/${encodeURIComponent(title)}`;
|
const searchUrl = `${API_BASE}/search-bar/search/${encodeURIComponent(title)}`;
|
||||||
|
|
||||||
const searchData = await fetchJson(searchUrl, {
|
const searchData = await fetchJson(searchUrl, {
|
||||||
headers: HEADERS,
|
headers: HEADERS,
|
||||||
signal: AbortSignal.timeout(10000)
|
signal: AbortSignal.timeout(10000)
|
||||||
});
|
});
|
||||||
|
|
||||||
const items = searchData?.data?.items?.movies?.items || [];
|
const items = searchData?.data?.items?.movies?.items || [];
|
||||||
|
|
||||||
const type = isTv ? 'tv' : 'movie';
|
const type = isTv ? 'tv' : 'movie';
|
||||||
const lowerTitle = title.toLowerCase();
|
const lowerTitle = title.toLowerCase();
|
||||||
|
|
||||||
let match = items.find(item =>
|
let match = items.find(item =>
|
||||||
item.type === type &&
|
item.type === type &&
|
||||||
item.title?.toLowerCase() === lowerTitle &&
|
item.title?.toLowerCase() === lowerTitle &&
|
||||||
(!info.year || item.release_date?.startsWith(String(info.year)))
|
(!info.year || item.release_date?.startsWith(String(info.year)))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!match) {
|
if (!match) {
|
||||||
match = items.find(item => item.type === type);
|
match = items.find(item => item.type === type);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!match?.id) {
|
if (!match?.id) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const streamUrl = isTv
|
const streamUrl = isTv
|
||||||
? `${API_BASE}/stream/${match.id}/episode?season=${s}&episode=${e}`
|
? `${API_BASE}/stream/${match.id}/episode?season=${s}&episode=${e}`
|
||||||
: `${API_BASE}/stream/${match.id}`;
|
: `${API_BASE}/stream/${match.id}`;
|
||||||
|
|
||||||
const json = await fetchJson(streamUrl, {
|
const json = await fetchJson(streamUrl, {
|
||||||
headers: HEADERS,
|
headers: HEADERS,
|
||||||
signal: AbortSignal.timeout(10000)
|
signal: AbortSignal.timeout(10000)
|
||||||
});
|
});
|
||||||
|
|
||||||
const sources = json?.data?.items?.sources;
|
const sources = json?.data?.items?.sources;
|
||||||
|
|
||||||
if (json?.type !== 'success' || !Array.isArray(sources) || !sources.length) {
|
if (json?.type !== 'success' || !Array.isArray(sources) || !sources.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chosen = sources.find(src => src.stream_url);
|
const chosen = sources.find(src => src.stream_url);
|
||||||
|
|
||||||
if (!chosen?.stream_url) {
|
if (!chosen?.stream_url) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
url: chosen.stream_url,
|
url: chosen.stream_url,
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': USER_AGENT,
|
'User-Agent': USER_AGENT,
|
||||||
'Referer': `${DOMAIN}/`
|
'Referer': `${DOMAIN}/`
|
||||||
},
|
},
|
||||||
server: 'purstream',
|
server: 'purstream',
|
||||||
quality: chosen.source_name || 'Auto',
|
quality: chosen.source_name || 'Auto',
|
||||||
type: chosen.format || 'hls',
|
type: chosen.format || 'hls',
|
||||||
skipProxy: true
|
skipProxy: true
|
||||||
};
|
};
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSources(args) {
|
export async function getSources(args) {
|
||||||
const stream = await getStream(args);
|
const stream = await getStream(args);
|
||||||
|
|
||||||
return stream ? [stream] : [];
|
return stream ? [stream] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VERIFY_HEADERS = {
|
export const VERIFY_HEADERS = {
|
||||||
...HEADERS
|
...HEADERS
|
||||||
};
|
};
|
||||||
@@ -1,115 +1,115 @@
|
|||||||
import { fetchJson, USER_AGENT } from '../utils/helpers.js';
|
import { fetchJson, USER_AGENT } from '../utils/helpers.js';
|
||||||
|
|
||||||
const API_BASE = 'https://streamdata.vaplayer.ru/api.php';
|
const API_BASE = 'https://streamdata.vaplayer.ru/api.php';
|
||||||
const EMBED_ORIGIN = 'https://nextgencloudfabric.com';
|
const EMBED_ORIGIN = 'https://nextgencloudfabric.com';
|
||||||
|
|
||||||
export const VERIFY_HEADERS = {
|
export const VERIFY_HEADERS = {
|
||||||
'User-Agent': USER_AGENT,
|
'User-Agent': USER_AGENT,
|
||||||
'Referer': `${EMBED_ORIGIN}/`,
|
'Referer': `${EMBED_ORIGIN}/`,
|
||||||
'Origin': EMBED_ORIGIN
|
'Origin': EMBED_ORIGIN
|
||||||
};
|
};
|
||||||
|
|
||||||
function extractStreamUrls(obj, urls = new Set()) {
|
function extractStreamUrls(obj, urls = new Set()) {
|
||||||
if (!obj) return urls;
|
if (!obj) return urls;
|
||||||
if (typeof obj === 'string') {
|
if (typeof obj === 'string') {
|
||||||
if (obj.startsWith('http') && (obj.includes('.m3u8') || obj.includes('.mp4') || obj.includes('.mpd'))) {
|
if (obj.startsWith('http') && (obj.includes('.m3u8') || obj.includes('.mp4') || obj.includes('.mpd'))) {
|
||||||
urls.add(obj);
|
urls.add(obj);
|
||||||
}
|
}
|
||||||
} else if (Array.isArray(obj)) {
|
} else if (Array.isArray(obj)) {
|
||||||
for (const item of obj) extractStreamUrls(item, urls);
|
for (const item of obj) extractStreamUrls(item, urls);
|
||||||
} else if (typeof obj === 'object') {
|
} else if (typeof obj === 'object') {
|
||||||
for (const val of Object.values(obj)) {
|
for (const val of Object.values(obj)) {
|
||||||
extractStreamUrls(val, urls);
|
extractStreamUrls(val, urls);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return urls;
|
return urls;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStream({ id, s, e }) {
|
export async function getStream({ id, s, e }) {
|
||||||
try {
|
try {
|
||||||
const isTv = s != null && e != null;
|
const isTv = s != null && e != null;
|
||||||
const type = isTv ? 'tv' : 'movie';
|
const type = isTv ? 'tv' : 'movie';
|
||||||
|
|
||||||
const url = `${API_BASE}?tmdb=${id}&type=${type}${isTv ? `&season=${s}&episode=${e}` : ''}`;
|
const url = `${API_BASE}?tmdb=${id}&type=${type}${isTv ? `&season=${s}&episode=${e}` : ''}`;
|
||||||
|
|
||||||
const data = await fetchJson(url, {
|
const data = await fetchJson(url, {
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': '*/*',
|
'Accept': '*/*',
|
||||||
'Origin': EMBED_ORIGIN,
|
'Origin': EMBED_ORIGIN,
|
||||||
'Referer': `${EMBED_ORIGIN}/`,
|
'Referer': `${EMBED_ORIGIN}/`,
|
||||||
'User-Agent': USER_AGENT
|
'User-Agent': USER_AGENT
|
||||||
},
|
},
|
||||||
signal: AbortSignal.timeout(10000)
|
signal: AbortSignal.timeout(10000)
|
||||||
});
|
});
|
||||||
|
|
||||||
const streamUrls = Array.from(extractStreamUrls(data));
|
const streamUrls = Array.from(extractStreamUrls(data));
|
||||||
if (!streamUrls.length) return null;
|
if (!streamUrls.length) return null;
|
||||||
|
|
||||||
let subs = [];
|
let subs = [];
|
||||||
if (data.subtitles && Array.isArray(data.subtitles)) {
|
if (data.subtitles && Array.isArray(data.subtitles)) {
|
||||||
subs = data.subtitles;
|
subs = data.subtitles;
|
||||||
} else if (data.captions && Array.isArray(data.captions)) {
|
} else if (data.captions && Array.isArray(data.captions)) {
|
||||||
subs = data.captions;
|
subs = data.captions;
|
||||||
}
|
}
|
||||||
|
|
||||||
const subtitles = subs.map(sub => ({
|
const subtitles = subs.map(sub => ({
|
||||||
url: sub.url || sub.file || sub.src,
|
url: sub.url || sub.file || sub.src,
|
||||||
lang: sub.display || sub.label || sub.language || sub.lang || 'Unknown'
|
lang: sub.display || sub.label || sub.language || sub.lang || 'Unknown'
|
||||||
})).filter(s => s.url);
|
})).filter(s => s.url);
|
||||||
|
|
||||||
const allUrls = streamUrls.map(streamUrl => {
|
const allUrls = streamUrls.map(streamUrl => {
|
||||||
let type = 'mp4';
|
let type = 'mp4';
|
||||||
if (streamUrl.includes('.mpd')) type = 'dash';
|
if (streamUrl.includes('.mpd')) type = 'dash';
|
||||||
else if (streamUrl.includes('.m3u8')) type = 'hls';
|
else if (streamUrl.includes('.m3u8')) type = 'hls';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url: streamUrl,
|
url: streamUrl,
|
||||||
server: 'VidAPI',
|
server: 'VidAPI',
|
||||||
quality: 'Auto',
|
quality: 'Auto',
|
||||||
type,
|
type,
|
||||||
headers: VERIFY_HEADERS,
|
headers: VERIFY_HEADERS,
|
||||||
subtitles: subtitles.length > 0 ? subtitles : undefined,
|
subtitles: subtitles.length > 0 ? subtitles : undefined,
|
||||||
skipProxy: false
|
skipProxy: false
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return allUrls.length ? { allUrls } : null;
|
return allUrls.length ? { allUrls } : null;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSources() {
|
export async function getSources() {
|
||||||
return ['VidAPI'];
|
return ['VidAPI'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function proxyStream({ url, res, proxyUtils: { fetchUpstream, rewriteM3u8 } }) {
|
export async function proxyStream({ url, res, proxyUtils: { fetchUpstream, rewriteM3u8 } }) {
|
||||||
try {
|
try {
|
||||||
const upstream = await fetchUpstream(url, 0, VERIFY_HEADERS);
|
const upstream = await fetchUpstream(url, 0, VERIFY_HEADERS);
|
||||||
if (!upstream) return res.end();
|
if (!upstream) return res.end();
|
||||||
|
|
||||||
const ct = (upstream.headers?.['content-type'] || '').toLowerCase();
|
const ct = (upstream.headers?.['content-type'] || '').toLowerCase();
|
||||||
const isM3u8 = ct.includes('mpegurl') || ct.includes('m3u8') || /\.m3u8?(\?|$)/i.test(url);
|
const isM3u8 = ct.includes('mpegurl') || ct.includes('m3u8') || /\.m3u8?(\?|$)/i.test(url);
|
||||||
|
|
||||||
if (isM3u8) {
|
if (isM3u8) {
|
||||||
const chunks = [];
|
const chunks = [];
|
||||||
for await (const c of upstream) chunks.push(c);
|
for await (const c of upstream) chunks.push(c);
|
||||||
let body = Buffer.concat(chunks).toString('utf8');
|
let body = Buffer.concat(chunks).toString('utf8');
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'application/vnd.apple.mpegurl');
|
res.setHeader('Content-Type', 'application/vnd.apple.mpegurl');
|
||||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
|
||||||
return res.end(rewriteM3u8(body, url, '&va=1&tt=1'));
|
return res.end(rewriteM3u8(body, url, '&va=1&tt=1'));
|
||||||
}
|
}
|
||||||
|
|
||||||
res.setHeader('Content-Type', ct || 'application/octet-stream');
|
res.setHeader('Content-Type', ct || 'application/octet-stream');
|
||||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
res.setHeader('Cache-Control', 'public, max-age=3600');
|
res.setHeader('Cache-Control', 'public, max-age=3600');
|
||||||
upstream.pipe(res);
|
upstream.pipe(res);
|
||||||
} catch {
|
} catch {
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
res.statusCode = 502;
|
res.statusCode = 502;
|
||||||
res.end();
|
res.end();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,43 +1,43 @@
|
|||||||
import { fetchJson, fetchText, USER_AGENT } from '../utils/helpers.js';
|
import { fetchJson, fetchText, USER_AGENT } from '../utils/helpers.js';
|
||||||
|
|
||||||
const API_BASE = 'https://enc-dec.app/api';
|
const API_BASE = 'https://enc-dec.app/api';
|
||||||
const HEADERS = { 'User-Agent': USER_AGENT, 'Referer': 'https://vidcore.net/', 'X-Requested-With': 'XMLHttpRequest' };
|
const HEADERS = { 'User-Agent': USER_AGENT, 'Referer': 'https://vidcore.net/', 'X-Requested-With': 'XMLHttpRequest' };
|
||||||
|
|
||||||
async function getDynamicServers(id, s, e) {
|
async function getDynamicServers(id, s, e) {
|
||||||
try {
|
try {
|
||||||
const html = await fetchText(s != null && e != null ? `https://vidcore.net/tv/${id}/${s}/${e}/` : `https://vidcore.net/movie/${id}/`, { headers: HEADERS });
|
const html = await fetchText(s != null && e != null ? `https://vidcore.net/tv/${id}/${s}/${e}/` : `https://vidcore.net/movie/${id}/`, { headers: HEADERS });
|
||||||
|
|
||||||
const match = html.match(/\\"(?:en|token)\\":\\"(.*?)\\"/) || html.match(/"(?:en|token)":"(.*?)"/);
|
const match = html.match(/\\"(?:en|token)\\":\\"(.*?)\\"/) || html.match(/"(?:en|token)":"(.*?)"/);
|
||||||
if (!match?.[1]) return null;
|
if (!match?.[1]) return null;
|
||||||
|
|
||||||
const encData = await fetchJson(`${API_BASE}/enc-vidcore?text=${encodeURIComponent(match[1])}`);
|
const encData = await fetchJson(`${API_BASE}/enc-vidcore?text=${encodeURIComponent(match[1])}`);
|
||||||
if (!encData?.result) return null;
|
if (!encData?.result) return null;
|
||||||
const { servers: serversUrl, stream: streamUrl, token } = encData.result;
|
const { servers: serversUrl, stream: streamUrl, token } = encData.result;
|
||||||
const reqHeaders = { ...HEADERS, 'X-CSRF-Token': token };
|
const reqHeaders = { ...HEADERS, 'X-CSRF-Token': token };
|
||||||
const serversEncrypted = await fetchText(serversUrl, { method: 'POST', headers: reqHeaders });
|
const serversEncrypted = await fetchText(serversUrl, { method: 'POST', headers: reqHeaders });
|
||||||
const decData = await fetchJson(`${API_BASE}/dec-vidcore`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: serversEncrypted }) });
|
const decData = await fetchJson(`${API_BASE}/dec-vidcore`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: serversEncrypted }) });
|
||||||
return decData?.result ? { servers: decData.result, streamUrl, reqHeaders } : null;
|
return decData?.result ? { servers: decData.result, streamUrl, reqHeaders } : null;
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStream({ id, s, e }) {
|
export async function getStream({ id, s, e }) {
|
||||||
const data = await getDynamicServers(id, s, e);
|
const data = await getDynamicServers(id, s, e);
|
||||||
if (!data?.servers?.length) return null;
|
if (!data?.servers?.length) return null;
|
||||||
for (const srv of data.servers) {
|
for (const srv of data.servers) {
|
||||||
try {
|
try {
|
||||||
const streamEncrypted = await fetchText(`${data.streamUrl}/${srv.data}`, { method: 'POST', headers: data.reqHeaders });
|
const streamEncrypted = await fetchText(`${data.streamUrl}/${srv.data}`, { method: 'POST', headers: data.reqHeaders });
|
||||||
const decData = await fetchJson(`${API_BASE}/dec-vidcore`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: streamEncrypted }) });
|
const decData = await fetchJson(`${API_BASE}/dec-vidcore`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: streamEncrypted }) });
|
||||||
const url = decData?.result?.url;
|
const url = decData?.result?.url;
|
||||||
if (url) {
|
if (url) {
|
||||||
const r = await fetch(url, { headers: { ...HEADERS, 'Range': 'bytes=0-511' }, signal: AbortSignal.timeout(5000) });
|
const r = await fetch(url, { headers: { ...HEADERS, 'Range': 'bytes=0-511' }, signal: AbortSignal.timeout(5000) });
|
||||||
if (r.ok || r.status === 206) return { url, subtitles: (decData.result.captions || []).map(c => ({ url: c.file, lang: c.label || 'Unknown' })), headers: { ...HEADERS, 'Origin': 'https://vidcore.net' }, server: srv.name };
|
if (r.ok || r.status === 206) return { url, subtitles: (decData.result.captions || []).map(c => ({ url: c.file, lang: c.label || 'Unknown' })), headers: { ...HEADERS, 'Origin': 'https://vidcore.net' }, server: srv.name };
|
||||||
}
|
}
|
||||||
} catch { }
|
} catch { }
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSources(args) {
|
export async function getSources(args) {
|
||||||
const data = await getDynamicServers(args.id, args.s, args.e);
|
const data = await getDynamicServers(args.id, args.s, args.e);
|
||||||
return data?.servers ? data.servers.map(s => s.name) : [];
|
return data?.servers ? data.servers.map(s => s.name) : [];
|
||||||
}
|
}
|
||||||
@@ -1,98 +1,98 @@
|
|||||||
import { getTmdbInfo, fetchJson, fetchText, USER_AGENT } from '../utils/helpers.js';
|
import { getTmdbInfo, fetchJson, fetchText, USER_AGENT } from '../utils/helpers.js';
|
||||||
|
|
||||||
const WINGS_BASE = "https://api.wingsdatabase.com";
|
const WINGS_BASE = "https://api.wingsdatabase.com";
|
||||||
const HEADERS = { "Accept": "*/*", "Origin": "https://player.videasy.to", "Referer": "https://player.videasy.to/", "User-Agent": USER_AGENT };
|
const HEADERS = { "Accept": "*/*", "Origin": "https://player.videasy.to", "Referer": "https://player.videasy.to/", "User-Agent": USER_AGENT };
|
||||||
const SERVERS = [
|
const SERVERS = [
|
||||||
{ id: 'jett', name: 'Jett' },
|
{ id: 'jett', name: 'Jett' },
|
||||||
{ id: 'cdn', name: 'Yoru' },
|
{ id: 'cdn', name: 'Yoru' },
|
||||||
{ id: 'tejo', name: 'Tejo' },
|
{ id: 'tejo', name: 'Tejo' },
|
||||||
{ id: 'neon2', name: 'Neon' },
|
{ id: 'neon2', name: 'Neon' },
|
||||||
{ id: 'ym', name: 'Sage' },
|
{ id: 'ym', name: 'Sage' },
|
||||||
{ id: 'downloader2', name: 'Cypher' },
|
{ id: 'downloader2', name: 'Cypher' },
|
||||||
{ id: 'm4uhd', name: 'Breach' },
|
{ id: 'm4uhd', name: 'Breach' },
|
||||||
{ id: 'hdmovie', name: 'Vyse' },
|
{ id: 'hdmovie', name: 'Vyse' },
|
||||||
{ id: 'meine', name: 'Killjoy' },
|
{ id: 'meine', name: 'Killjoy' },
|
||||||
{ id: 'lamovie', name: 'Omen' },
|
{ id: 'lamovie', name: 'Omen' },
|
||||||
{ id: 'superflix', name: 'Raze' }
|
{ id: 'superflix', name: 'Raze' }
|
||||||
];
|
];
|
||||||
|
|
||||||
export async function getStream({ id, s, e, server }) {
|
export async function getStream({ id, s, e, server }) {
|
||||||
try {
|
try {
|
||||||
const isTv = s != null && e != null;
|
const isTv = s != null && e != null;
|
||||||
const info = await getTmdbInfo(id, isTv ? 'tv' : 'movie');
|
const info = await getTmdbInfo(id, isTv ? 'tv' : 'movie');
|
||||||
if (!info?.titles?.length) return null;
|
if (!info?.titles?.length) return null;
|
||||||
|
|
||||||
const seedData = await fetchJson(`${WINGS_BASE}/seed?mediaId=${id}`, { headers: HEADERS, signal: AbortSignal.timeout(5000) });
|
const seedData = await fetchJson(`${WINGS_BASE}/seed?mediaId=${id}`, { headers: HEADERS, signal: AbortSignal.timeout(5000) });
|
||||||
const seed = seedData?.seed;
|
const seed = seedData?.seed;
|
||||||
if (!seed) return null;
|
if (!seed) return null;
|
||||||
|
|
||||||
const encTitle = encodeURIComponent(encodeURIComponent(info.titles[0]));
|
const encTitle = encodeURIComponent(encodeURIComponent(info.titles[0]));
|
||||||
let targets = SERVERS;
|
let targets = SERVERS;
|
||||||
|
|
||||||
if (server && server !== 'all') {
|
if (server && server !== 'all') {
|
||||||
const cleanName = server.replace('VidEasy - ', '');
|
const cleanName = server.replace('VidEasy - ', '');
|
||||||
targets = SERVERS.filter(sv => sv.name === cleanName);
|
targets = SERVERS.filter(sv => sv.name === cleanName);
|
||||||
if (!targets.length) targets = SERVERS;
|
if (!targets.length) targets = SERVERS;
|
||||||
}
|
}
|
||||||
|
|
||||||
const settled = await Promise.allSettled(targets.map(async srv => {
|
const settled = await Promise.allSettled(targets.map(async srv => {
|
||||||
if (srv.id === 'cdn' && isTv) throw new Error();
|
if (srv.id === 'cdn' && isTv) throw new Error();
|
||||||
|
|
||||||
let url = `${WINGS_BASE}/${srv.id}/sources-with-title?title=${encTitle}&mediaType=${isTv ? 'tv' : 'movie'}&year=${info.year || ''}&tmdbId=${id}&imdbId=${info.imdbId || "tt0000000"}&enc=2&seed=${seed}`;
|
let url = `${WINGS_BASE}/${srv.id}/sources-with-title?title=${encTitle}&mediaType=${isTv ? 'tv' : 'movie'}&year=${info.year || ''}&tmdbId=${id}&imdbId=${info.imdbId || "tt0000000"}&enc=2&seed=${seed}`;
|
||||||
if (isTv) url += `&episodeId=${e}&seasonId=${s}`;
|
if (isTv) url += `&episodeId=${e}&seasonId=${s}`;
|
||||||
|
|
||||||
const encText = await fetchText(url, { headers: HEADERS, signal: AbortSignal.timeout(10000) });
|
const encText = await fetchText(url, { headers: HEADERS, signal: AbortSignal.timeout(10000) });
|
||||||
const decJson = await fetchJson("https://enc-dec.app/api/dec-videasy", {
|
const decJson = await fetchJson("https://enc-dec.app/api/dec-videasy", {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ text: encText, id: String(id), seed }),
|
body: JSON.stringify({ text: encText, id: String(id), seed }),
|
||||||
signal: AbortSignal.timeout(10000)
|
signal: AbortSignal.timeout(10000)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (decJson.status !== 200 || !decJson.result) throw new Error();
|
if (decJson.status !== 200 || !decJson.result) throw new Error();
|
||||||
|
|
||||||
let sourcesArray = [];
|
let sourcesArray = [];
|
||||||
let subsArray = [];
|
let subsArray = [];
|
||||||
|
|
||||||
if (Array.isArray(decJson.result)) {
|
if (Array.isArray(decJson.result)) {
|
||||||
sourcesArray = decJson.result;
|
sourcesArray = decJson.result;
|
||||||
} else if (decJson.result.sources && Array.isArray(decJson.result.sources)) {
|
} else if (decJson.result.sources && Array.isArray(decJson.result.sources)) {
|
||||||
sourcesArray = decJson.result.sources;
|
sourcesArray = decJson.result.sources;
|
||||||
if (decJson.result.subtitles && Array.isArray(decJson.result.subtitles)) {
|
if (decJson.result.subtitles && Array.isArray(decJson.result.subtitles)) {
|
||||||
subsArray = decJson.result.subtitles.map(sub => ({
|
subsArray = decJson.result.subtitles.map(sub => ({
|
||||||
url: sub.url || sub.file,
|
url: sub.url || sub.file,
|
||||||
lang: sub.language || sub.lang || sub.label || 'Unknown'
|
lang: sub.language || sub.lang || sub.label || 'Unknown'
|
||||||
})).filter(sub => sub.url);
|
})).filter(sub => sub.url);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
sourcesArray = [decJson.result];
|
sourcesArray = [decJson.result];
|
||||||
}
|
}
|
||||||
|
|
||||||
return sourcesArray.map(res => {
|
return sourcesArray.map(res => {
|
||||||
const streamUrl = res.url || res.file || res.link || res.playlist || res.stream;
|
const streamUrl = res.url || res.file || res.link || res.playlist || res.stream;
|
||||||
return streamUrl ? {
|
return streamUrl ? {
|
||||||
url: streamUrl,
|
url: streamUrl,
|
||||||
quality: res.quality || "Auto",
|
quality: res.quality || "Auto",
|
||||||
server: `VidEasy - ${srv.name}`,
|
server: `VidEasy - ${srv.name}`,
|
||||||
type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
|
type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
|
||||||
headers: HEADERS,
|
headers: HEADERS,
|
||||||
subtitles: subsArray.length > 0 ? subsArray : undefined,
|
subtitles: subsArray.length > 0 ? subsArray : undefined,
|
||||||
skipProxy: false,
|
skipProxy: false,
|
||||||
skipVerify: true,
|
skipVerify: true,
|
||||||
skipHlsCheck: true
|
skipHlsCheck: true
|
||||||
} : null;
|
} : null;
|
||||||
}).filter(Boolean);
|
}).filter(Boolean);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const allUrls = [];
|
const allUrls = [];
|
||||||
for (const r of settled) {
|
for (const r of settled) {
|
||||||
if (r.status === 'fulfilled' && r.value) allUrls.push(...r.value);
|
if (r.status === 'fulfilled' && r.value) allUrls.push(...r.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return allUrls.length ? { allUrls } : null;
|
return allUrls.length ? { allUrls } : null;
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSources() {
|
export async function getSources() {
|
||||||
return SERVERS.map(s => `VidEasy - ${s.name}`);
|
return SERVERS.map(s => `VidEasy - ${s.name}`);
|
||||||
}
|
}
|
||||||
@@ -1,53 +1,53 @@
|
|||||||
import { fetchJson, fetchText, USER_AGENT } from '../utils/helpers.js';
|
import { fetchJson, fetchText, USER_AGENT } from '../utils/helpers.js';
|
||||||
|
|
||||||
const API_BASE = 'https://enc-dec.app/api';
|
const API_BASE = 'https://enc-dec.app/api';
|
||||||
const DOMAIN = 'https://vidfast.vc';
|
const DOMAIN = 'https://vidfast.vc';
|
||||||
const HEADERS = { 'User-Agent': USER_AGENT, 'Referer': `${DOMAIN}/`, 'X-Requested-With': 'XMLHttpRequest' };
|
const HEADERS = { 'User-Agent': USER_AGENT, 'Referer': `${DOMAIN}/`, 'X-Requested-With': 'XMLHttpRequest' };
|
||||||
|
|
||||||
async function getVidfastMeta(id, s, e) {
|
async function getVidfastMeta(id, s, e) {
|
||||||
try {
|
try {
|
||||||
const embedUrl = s != null && e != null ? `${DOMAIN}/tv/${id}/${s}/${e}/` : `${DOMAIN}/movie/${id}/`;
|
const embedUrl = s != null && e != null ? `${DOMAIN}/tv/${id}/${s}/${e}/` : `${DOMAIN}/movie/${id}/`;
|
||||||
const html = await fetchText(embedUrl, { headers: { 'User-Agent': USER_AGENT } });
|
const html = await fetchText(embedUrl, { headers: { 'User-Agent': USER_AGENT } });
|
||||||
|
|
||||||
const match = html.match(/\\"(?:en|token)\\":\\"(.*?)\\"/) || html.match(/"(?:en|token)":"(.*?)"/);
|
const match = html.match(/\\"(?:en|token)\\":\\"(.*?)\\"/) || html.match(/"(?:en|token)":"(.*?)"/);
|
||||||
if (!match?.[1]) return null;
|
if (!match?.[1]) return null;
|
||||||
|
|
||||||
const encData = await fetchJson(`${API_BASE}/enc-vidfast?text=${encodeURIComponent(match[1])}`);
|
const encData = await fetchJson(`${API_BASE}/enc-vidfast?text=${encodeURIComponent(match[1])}`);
|
||||||
if (encData.status !== 200 || !encData.result) return null;
|
if (encData.status !== 200 || !encData.result) return null;
|
||||||
|
|
||||||
const { servers: serversUrl, stream: streamUrl, token } = encData.result;
|
const { servers: serversUrl, stream: streamUrl, token } = encData.result;
|
||||||
const reqHeaders = { ...HEADERS, 'X-CSRF-Token': token };
|
const reqHeaders = { ...HEADERS, 'X-CSRF-Token': token };
|
||||||
|
|
||||||
const serversEncrypted = await fetchText(serversUrl, { method: 'POST', headers: reqHeaders });
|
const serversEncrypted = await fetchText(serversUrl, { method: 'POST', headers: reqHeaders });
|
||||||
const decServersData = await fetchJson(`${API_BASE}/dec-vidfast`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: serversEncrypted }) });
|
const decServersData = await fetchJson(`${API_BASE}/dec-vidfast`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: serversEncrypted }) });
|
||||||
|
|
||||||
if (decServersData.status !== 200 || !decServersData.result) return null;
|
if (decServersData.status !== 200 || !decServersData.result) return null;
|
||||||
return { servers: decServersData.result, streamUrl, reqHeaders };
|
return { servers: decServersData.result, streamUrl, reqHeaders };
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStream({ id, s, e, server }) {
|
export async function getStream({ id, s, e, server }) {
|
||||||
const meta = await getVidfastMeta(id, s, e);
|
const meta = await getVidfastMeta(id, s, e);
|
||||||
if (!meta?.servers?.length) return null;
|
if (!meta?.servers?.length) return null;
|
||||||
const { servers, streamUrl, reqHeaders } = meta;
|
const { servers, streamUrl, reqHeaders } = meta;
|
||||||
let targets = servers;
|
let targets = servers;
|
||||||
if (server && server !== 'all') {
|
if (server && server !== 'all') {
|
||||||
const cleanName = server.replace('VidFast - ', '');
|
const cleanName = server.replace('VidFast - ', '');
|
||||||
targets = servers.filter(srv => srv.name === cleanName);
|
targets = servers.filter(srv => srv.name === cleanName);
|
||||||
if (!targets.length) targets = servers;
|
if (!targets.length) targets = servers;
|
||||||
}
|
}
|
||||||
const results = await Promise.allSettled(targets.map(async srv => {
|
const results = await Promise.allSettled(targets.map(async srv => {
|
||||||
const streamEncrypted = await fetchText(`${streamUrl}/${srv.data}`, { method: 'POST', headers: reqHeaders });
|
const streamEncrypted = await fetchText(`${streamUrl}/${srv.data}`, { method: 'POST', headers: reqHeaders });
|
||||||
const decStreamData = await fetchJson(`${API_BASE}/dec-vidfast`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: streamEncrypted }) });
|
const decStreamData = await fetchJson(`${API_BASE}/dec-vidfast`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: streamEncrypted }) });
|
||||||
if (decStreamData.status !== 200 || !decStreamData.result?.url) throw new Error();
|
if (decStreamData.status !== 200 || !decStreamData.result?.url) throw new Error();
|
||||||
return { url: decStreamData.result.url, server: `VidFast - ${srv.name || 'Server'}`, type: decStreamData.result.url.includes('.m3u8') ? 'hls' : 'mp4', subtitles: (decStreamData.result.captions || []).map(c => ({ url: c.file, lang: c.label || 'Unknown' })), headers: { ...HEADERS, 'Origin': DOMAIN } };
|
return { url: decStreamData.result.url, server: `VidFast - ${srv.name || 'Server'}`, type: decStreamData.result.url.includes('.m3u8') ? 'hls' : 'mp4', subtitles: (decStreamData.result.captions || []).map(c => ({ url: c.file, lang: c.label || 'Unknown' })), headers: { ...HEADERS, 'Origin': DOMAIN } };
|
||||||
}));
|
}));
|
||||||
const allUrls = [];
|
const allUrls = [];
|
||||||
for (const r of results) if (r.status === 'fulfilled') allUrls.push(r.value);
|
for (const r of results) if (r.status === 'fulfilled') allUrls.push(r.value);
|
||||||
return allUrls.length ? { allUrls } : null;
|
return allUrls.length ? { allUrls } : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSources(args) {
|
export async function getSources(args) {
|
||||||
const meta = await getVidfastMeta(args.id, args.s, args.e);
|
const meta = await getVidfastMeta(args.id, args.s, args.e);
|
||||||
return meta?.servers ? meta.servers.map(srv => `VidFast - ${srv.name}`) : [];
|
return meta?.servers ? meta.servers.map(srv => `VidFast - ${srv.name}`) : [];
|
||||||
}
|
}
|
||||||
@@ -1,28 +1,28 @@
|
|||||||
import { fetchJson, USER_AGENT } from '../utils/helpers.js';
|
import { fetchJson, USER_AGENT } from '../utils/helpers.js';
|
||||||
|
|
||||||
const BASE = "https://vidlink.pro";
|
const BASE = "https://vidlink.pro";
|
||||||
const HEADERS = { 'User-Agent': USER_AGENT, Origin: BASE, Referer: `${BASE}/` };
|
const HEADERS = { 'User-Agent': USER_AGENT, Origin: BASE, Referer: `${BASE}/` };
|
||||||
|
|
||||||
export async function getStream({ id, s, e }) {
|
export async function getStream({ id, s, e }) {
|
||||||
try {
|
try {
|
||||||
const encData = await fetchJson(`https://enc-dec.app/api/enc-vidlink?text=${id}`, { signal: AbortSignal.timeout(6000) });
|
const encData = await fetchJson(`https://enc-dec.app/api/enc-vidlink?text=${id}`, { signal: AbortSignal.timeout(6000) });
|
||||||
if (encData?.status !== 200 || !encData?.result) return null;
|
if (encData?.status !== 200 || !encData?.result) return null;
|
||||||
const data = await fetchJson(s ? `${BASE}/api/b/tv/${encData.result}/${s}/${e || 1}` : `${BASE}/api/b/movie/${encData.result}`, { headers: HEADERS, signal: AbortSignal.timeout(10000) });
|
const data = await fetchJson(s ? `${BASE}/api/b/tv/${encData.result}/${s}/${e || 1}` : `${BASE}/api/b/movie/${encData.result}`, { headers: HEADERS, signal: AbortSignal.timeout(10000) });
|
||||||
const stream = data?.stream;
|
const stream = data?.stream;
|
||||||
if (!stream) return null;
|
if (!stream) return null;
|
||||||
if (stream.type === 'file' && stream.qualities) {
|
if (stream.type === 'file' && stream.qualities) {
|
||||||
let picked = null;
|
let picked = null;
|
||||||
for (const q of ['1080', '720', '480', '360']) if (stream.qualities[q]?.url) { picked = stream.qualities[q].url; break; }
|
for (const q of ['1080', '720', '480', '360']) if (stream.qualities[q]?.url) { picked = stream.qualities[q].url; break; }
|
||||||
if (!picked) picked = stream.qualities[Object.keys(stream.qualities)[0]]?.url;
|
if (!picked) picked = stream.qualities[Object.keys(stream.qualities)[0]]?.url;
|
||||||
if (picked) return { allUrls: [{ url: picked, type: 'mp4', audio: 'sub', server: 'Vidlink', skipHlsCheck: true, skipCache: true }] };
|
if (picked) return { allUrls: [{ url: picked, type: 'mp4', audio: 'sub', server: 'Vidlink', skipHlsCheck: true, skipCache: true }] };
|
||||||
} else if (stream.playlist) {
|
} else if (stream.playlist) {
|
||||||
return { allUrls: [{ url: stream.playlist, type: 'hls', audio: 'sub', server: 'Vidlink', skipHlsCheck: true, skipCache: true }] };
|
return { allUrls: [{ url: stream.playlist, type: 'hls', audio: 'sub', server: 'Vidlink', skipHlsCheck: true, skipCache: true }] };
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSources(args) {
|
export async function getSources(args) {
|
||||||
const stream = await getStream(args);
|
const stream = await getStream(args);
|
||||||
return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server))] : [];
|
return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server))] : [];
|
||||||
}
|
}
|
||||||
@@ -1,77 +1,77 @@
|
|||||||
import { fetchJson, fetchText, USER_AGENT } from '../utils/helpers.js';
|
import { fetchJson, fetchText, USER_AGENT } from '../utils/helpers.js';
|
||||||
|
|
||||||
const BASE_URL = 'https://vidnest.fun';
|
const BASE_URL = 'https://vidnest.fun';
|
||||||
const API_BASE_URL = 'https://new.vidnest.fun';
|
const API_BASE_URL = 'https://new.vidnest.fun';
|
||||||
const REQUEST_HEADERS = { 'User-Agent': USER_AGENT, 'Accept': 'application/json, text/javascript, */*; q=0.01', 'Accept-Language': 'en-US,en;q=0.9', 'Referer': `${BASE_URL}/`, 'Origin': BASE_URL };
|
const REQUEST_HEADERS = { 'User-Agent': USER_AGENT, 'Accept': 'application/json, text/javascript, */*; q=0.01', 'Accept-Language': 'en-US,en;q=0.9', 'Referer': `${BASE_URL}/`, 'Origin': BASE_URL };
|
||||||
const CDN_PROXY_HEADERS = { 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0', 'accept': '*/*', 'accept-language': 'en-US,en;q=0.5', 'origin': 'https://megaplay.buzz', 'referer': 'https://megaplay.buzz/' };
|
const CDN_PROXY_HEADERS = { 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0', 'accept': '*/*', 'accept-language': 'en-US,en;q=0.5', 'origin': 'https://megaplay.buzz', 'referer': 'https://megaplay.buzz/' };
|
||||||
|
|
||||||
export const CDN_HEADERS = [
|
export const CDN_HEADERS = [
|
||||||
{ pattern: /letsgocdn\d+\.shop/i, headers: { 'Referer': BASE_URL, 'Origin': BASE_URL, 'User-Agent': USER_AGENT } },
|
{ pattern: /letsgocdn\d+\.shop/i, headers: { 'Referer': BASE_URL, 'Origin': BASE_URL, 'User-Agent': USER_AGENT } },
|
||||||
{ pattern: /cdn\.mewstream\.buzz/i, headers: { 'Referer': BASE_URL, 'Origin': BASE_URL, 'User-Agent': USER_AGENT } }
|
{ pattern: /cdn\.mewstream\.buzz/i, headers: { 'Referer': BASE_URL, 'Origin': BASE_URL, 'User-Agent': USER_AGENT } }
|
||||||
];
|
];
|
||||||
|
|
||||||
const ALPHABET = 'RB0fpH8ZEyVLkv7c2i6MAJ5u3IKFDxlS1NTsnGaqmXYdUrtzjwObCgQP94hoeW+/=';
|
const ALPHABET = 'RB0fpH8ZEyVLkv7c2i6MAJ5u3IKFDxlS1NTsnGaqmXYdUrtzjwObCgQP94hoeW+/=';
|
||||||
const REV_MAP = new Uint8Array(256);
|
const REV_MAP = new Uint8Array(256);
|
||||||
for (let i = 0; i < ALPHABET.length; i++) REV_MAP[ALPHABET.charCodeAt(i)] = i;
|
for (let i = 0; i < ALPHABET.length; i++) REV_MAP[ALPHABET.charCodeAt(i)] = i;
|
||||||
|
|
||||||
function decrypt(payload) {
|
function decrypt(payload) {
|
||||||
const len = payload.length;
|
const len = payload.length;
|
||||||
const bytes = new Uint8Array(len);
|
const bytes = new Uint8Array(len);
|
||||||
let j = 0;
|
let j = 0;
|
||||||
for (let i = 0; i < len; i += 4) {
|
for (let i = 0; i < len; i += 4) {
|
||||||
const c0 = REV_MAP[payload.charCodeAt(i)] ?? 64;
|
const c0 = REV_MAP[payload.charCodeAt(i)] ?? 64;
|
||||||
const c1 = REV_MAP[payload.charCodeAt(i + 1)] ?? 64;
|
const c1 = REV_MAP[payload.charCodeAt(i + 1)] ?? 64;
|
||||||
const c2 = REV_MAP[payload.charCodeAt(i + 2)] ?? 64;
|
const c2 = REV_MAP[payload.charCodeAt(i + 2)] ?? 64;
|
||||||
const c3 = REV_MAP[payload.charCodeAt(i + 3)] ?? 64;
|
const c3 = REV_MAP[payload.charCodeAt(i + 3)] ?? 64;
|
||||||
bytes[j++] = (c0 << 2) | (c1 >> 4);
|
bytes[j++] = (c0 << 2) | (c1 >> 4);
|
||||||
if (c2 !== 64) bytes[j++] = ((c1 & 15) << 4) | (c2 >> 2);
|
if (c2 !== 64) bytes[j++] = ((c1 & 15) << 4) | (c2 >> 2);
|
||||||
if (c3 !== 64) bytes[j++] = ((c2 & 3) << 6) | c3;
|
if (c3 !== 64) bytes[j++] = ((c2 & 3) << 6) | c3;
|
||||||
}
|
}
|
||||||
return JSON.parse(new TextDecoder().decode(bytes.subarray(0, j)));
|
return JSON.parse(new TextDecoder().decode(bytes.subarray(0, j)));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function tmdbToAnilist(tmdbId, season) {
|
async function tmdbToAnilist(tmdbId, season) {
|
||||||
try {
|
try {
|
||||||
const data = await fetchJson(`https://api.ani.zip/mappings?tmdb_id=${tmdbId}&type=tv&season=${season || 1}`, { signal: AbortSignal.timeout(6000) });
|
const data = await fetchJson(`https://api.ani.zip/mappings?tmdb_id=${tmdbId}&type=tv&season=${season || 1}`, { signal: AbortSignal.timeout(6000) });
|
||||||
return data?.mappings?.[0]?.anilist_id || null;
|
return data?.mappings?.[0]?.anilist_id || null;
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
const SERVER_KEYS = ['hollymoviehd', 'allmovies', 'catflix', 'purstream', 'lamda', 'vidlink', 'klikxxi'];
|
const SERVER_KEYS = ['hollymoviehd', 'allmovies', 'catflix', 'purstream', 'lamda', 'vidlink', 'klikxxi'];
|
||||||
|
|
||||||
export async function getStream({ id, s, e, tmdbApiKey, audio }) {
|
export async function getStream({ id, s, e, tmdbApiKey, audio }) {
|
||||||
if (s) {
|
if (s) {
|
||||||
const k = tmdbApiKey;
|
const k = tmdbApiKey;
|
||||||
const showData = k ? await fetchJson(`https://api.themoviedb.org/3/tv/${id}?api_key=${k}`, { signal: AbortSignal.timeout(5000) }).catch(() => null) : null;
|
const showData = k ? await fetchJson(`https://api.themoviedb.org/3/tv/${id}?api_key=${k}`, { signal: AbortSignal.timeout(5000) }).catch(() => null) : null;
|
||||||
if (showData?.genres?.some(g => g.id === 16)) {
|
if (showData?.genres?.some(g => g.id === 16)) {
|
||||||
const anilistId = await tmdbToAnilist(id, s);
|
const anilistId = await tmdbToAnilist(id, s);
|
||||||
if (anilistId) {
|
if (anilistId) {
|
||||||
try {
|
try {
|
||||||
const json = await fetchJson(`${API_BASE_URL}/hianime/anime/${anilistId}/${e || 1}/${audio === 'dub' ? 'dub' : 'sub'}`, { headers: REQUEST_HEADERS, signal: AbortSignal.timeout(15000) });
|
const json = await fetchJson(`${API_BASE_URL}/hianime/anime/${anilistId}/${e || 1}/${audio === 'dub' ? 'dub' : 'sub'}`, { headers: REQUEST_HEADERS, signal: AbortSignal.timeout(15000) });
|
||||||
const data = json.encrypted ? decrypt(json.data) : json.data;
|
const data = json.encrypted ? decrypt(json.data) : json.data;
|
||||||
const file = data?.sources?.[0]?.file;
|
const file = data?.sources?.[0]?.file;
|
||||||
if (file) return { url: `https://megacloud.animanga.fun/proxy?url=${encodeURIComponent(file)}&headers=${encodeURIComponent(JSON.stringify(CDN_PROXY_HEADERS))}`, headers: REQUEST_HEADERS };
|
if (file) return { url: `https://megacloud.animanga.fun/proxy?url=${encodeURIComponent(file)}&headers=${encodeURIComponent(JSON.stringify(CDN_PROXY_HEADERS))}`, headers: REQUEST_HEADERS };
|
||||||
} catch { }
|
} catch { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (audio === 'dub') return null;
|
if (audio === 'dub') return null;
|
||||||
const segment = s ? `tv/${id}/${s}/${e || 1}` : `movie/${id}`;
|
const segment = s ? `tv/${id}/${s}/${e || 1}` : `movie/${id}`;
|
||||||
const settled = await Promise.allSettled(SERVER_KEYS.map(async server => {
|
const settled = await Promise.allSettled(SERVER_KEYS.map(async server => {
|
||||||
const json = await fetchJson(`${API_BASE_URL}/${server}/${segment}`, { headers: REQUEST_HEADERS, signal: AbortSignal.timeout(10000) });
|
const json = await fetchJson(`${API_BASE_URL}/${server}/${segment}`, { headers: REQUEST_HEADERS, signal: AbortSignal.timeout(10000) });
|
||||||
const data = json.encrypted ? decrypt(json.data) : json.data;
|
const data = json.encrypted ? decrypt(json.data) : json.data;
|
||||||
const file = data?.sources?.[0]?.file ?? data?.streams?.[0]?.url ?? data?.url?.[0]?.link ?? data?.data?.stream?.playlist;
|
const file = data?.sources?.[0]?.file ?? data?.streams?.[0]?.url ?? data?.url?.[0]?.link ?? data?.data?.stream?.playlist;
|
||||||
if (file) return { url: file, server, headers: REQUEST_HEADERS };
|
if (file) return { url: file, server, headers: REQUEST_HEADERS };
|
||||||
throw new Error();
|
throw new Error();
|
||||||
}));
|
}));
|
||||||
const allUrls = [];
|
const allUrls = [];
|
||||||
for (const r of settled) if (r.status === 'fulfilled') allUrls.push(r.value);
|
for (const r of settled) if (r.status === 'fulfilled') allUrls.push(r.value);
|
||||||
return allUrls.length ? { allUrls } : null;
|
return allUrls.length ? { allUrls } : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSources(args) {
|
export async function getSources(args) {
|
||||||
const res = await getStream(args);
|
const res = await getStream(args);
|
||||||
return res?.allUrls ? res.allUrls.map(u => u.server) : [];
|
return res?.allUrls ? res.allUrls.map(u => u.server) : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VERIFY_HEADERS = { ...REQUEST_HEADERS };
|
export const VERIFY_HEADERS = { ...REQUEST_HEADERS };
|
||||||
@@ -1,95 +1,95 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const BASE = "https://embed.vidrift.in";
|
const BASE = "https://embed.vidrift.in";
|
||||||
|
|
||||||
const DEFAULT_HEADERS = {
|
const DEFAULT_HEADERS = {
|
||||||
"Accept": "*/*",
|
"Accept": "*/*",
|
||||||
"Accept-Language": "en-US,en;q=0.9",
|
"Accept-Language": "en-US,en;q=0.9",
|
||||||
"Cache-Control": "no-cache",
|
"Cache-Control": "no-cache",
|
||||||
"Pragma": "no-cache",
|
"Pragma": "no-cache",
|
||||||
"Referer": `${BASE}/`,
|
"Referer": `${BASE}/`,
|
||||||
"Origin": BASE,
|
"Origin": BASE,
|
||||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0"
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0"
|
||||||
};
|
};
|
||||||
|
|
||||||
function decodeStreams(encodedStr) {
|
function decodeStreams(encodedStr) {
|
||||||
try {
|
try {
|
||||||
const base64Str = encodedStr.replace(/-/g, '+').replace(/_/g, '/');
|
const base64Str = encodedStr.replace(/-/g, '+').replace(/_/g, '/');
|
||||||
|
|
||||||
const binaryStr = typeof atob === 'function'
|
const binaryStr = typeof atob === 'function'
|
||||||
? atob(base64Str)
|
? atob(base64Str)
|
||||||
: Buffer.from(base64Str, 'base64').toString('binary');
|
: Buffer.from(base64Str, 'base64').toString('binary');
|
||||||
|
|
||||||
const bytes = new Uint8Array(binaryStr.length);
|
const bytes = new Uint8Array(binaryStr.length);
|
||||||
for (let i = 0; i < binaryStr.length; i++) {
|
for (let i = 0; i < binaryStr.length; i++) {
|
||||||
bytes[i] = binaryStr.charCodeAt(i);
|
bytes[i] = binaryStr.charCodeAt(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = bytes[0];
|
const key = bytes[0];
|
||||||
let decodedStr = '';
|
let decodedStr = '';
|
||||||
for (let i = 1; i < bytes.length; i++) {
|
for (let i = 1; i < bytes.length; i++) {
|
||||||
decodedStr += String.fromCharCode(bytes[i] ^ key);
|
decodedStr += String.fromCharCode(bytes[i] ^ key);
|
||||||
}
|
}
|
||||||
|
|
||||||
return JSON.parse(decodedStr);
|
return JSON.parse(decodedStr);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStream(args) {
|
export async function getStream(args) {
|
||||||
const { id, s, e, server: serverName } = args;
|
const { id, s, e, server: serverName } = args;
|
||||||
const type = s != null && e != null ? "tv" : "movie";
|
const type = s != null && e != null ? "tv" : "movie";
|
||||||
|
|
||||||
const endpoint = type === "tv"
|
const endpoint = type === "tv"
|
||||||
? `${BASE}/embed/tv/${id}/${s}/${e}`
|
? `${BASE}/embed/tv/${id}/${s}/${e}`
|
||||||
: `${BASE}/embed/movie/${id}`;
|
: `${BASE}/embed/movie/${id}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(endpoint, {
|
const res = await fetch(endpoint, {
|
||||||
headers: {
|
headers: {
|
||||||
...DEFAULT_HEADERS,
|
...DEFAULT_HEADERS,
|
||||||
"Accept": "text/html",
|
"Accept": "text/html",
|
||||||
},
|
},
|
||||||
signal: AbortSignal.timeout(10000)
|
signal: AbortSignal.timeout(10000)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) return null;
|
if (!res.ok) return null;
|
||||||
|
|
||||||
const html = await res.text();
|
const html = await res.text();
|
||||||
|
|
||||||
const match = html.match(/var\s+_s\s*=\s*['"]([^'"]+)['"]/);
|
const match = html.match(/var\s+_s\s*=\s*['"]([^'"]+)['"]/);
|
||||||
if (!match) return null;
|
if (!match) return null;
|
||||||
|
|
||||||
const encodedStreams = match[1];
|
const encodedStreams = match[1];
|
||||||
const streamsData = decodeStreams(encodedStreams);
|
const streamsData = decodeStreams(encodedStreams);
|
||||||
|
|
||||||
if (!Array.isArray(streamsData) || !streamsData.length) {
|
if (!Array.isArray(streamsData) || !streamsData.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const allUrls = streamsData.map((stream, index) => ({
|
const allUrls = streamsData.map((stream, index) => ({
|
||||||
url: stream.url.startsWith("http")
|
url: stream.url.startsWith("http")
|
||||||
? stream.url
|
? stream.url
|
||||||
: `${BASE}${stream.url}`,
|
: `${BASE}${stream.url}`,
|
||||||
server: stream.label || `VidRift - Server ${index + 1}`,
|
server: stream.label || `VidRift - Server ${index + 1}`,
|
||||||
type: "hls",
|
type: "hls",
|
||||||
headers: DEFAULT_HEADERS
|
headers: DEFAULT_HEADERS
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (serverName && serverName !== "all") {
|
if (serverName && serverName !== "all") {
|
||||||
const filtered = allUrls.filter(x => x.server === serverName);
|
const filtered = allUrls.filter(x => x.server === serverName);
|
||||||
return filtered.length ? { allUrls: filtered } : null;
|
return filtered.length ? { allUrls: filtered } : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { allUrls };
|
return { allUrls };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSources(args) {
|
export async function getSources(args) {
|
||||||
const res = await getStream(args);
|
const res = await getStream(args);
|
||||||
if (!res?.allUrls) return ["VidRift - Server 1"];
|
if (!res?.allUrls) return ["VidRift - Server 1"];
|
||||||
return res.allUrls.map(x => x.server);
|
return res.allUrls.map(x => x.server);
|
||||||
}
|
}
|
||||||
@@ -1,145 +1,145 @@
|
|||||||
import { webcrypto } from 'node:crypto';
|
import { webcrypto } from 'node:crypto';
|
||||||
import { fetchJson, USER_AGENT } from '../utils/helpers.js';
|
import { fetchJson, USER_AGENT } from '../utils/helpers.js';
|
||||||
|
|
||||||
const GCM_HEX_KEY = '7f3e9c2a8b5d1f4e6a9c3b7d2e5f8a1c4b6d9e2f5a8c1b4d7e9f2a5c8b1d4e7f';
|
const GCM_HEX_KEY = '7f3e9c2a8b5d1f4e6a9c3b7d2e5f8a1c4b6d9e2f5a8c1b4d7e9f2a5c8b1d4e7f';
|
||||||
const BASE_URL = 'https://vidrock.ru/';
|
const BASE_URL = 'https://vidrock.ru/';
|
||||||
const PROXY_PREFIX = 'https://proxy.vidrock.store/';
|
const PROXY_PREFIX = 'https://proxy.vidrock.store/';
|
||||||
|
|
||||||
let cryptoKey = null;
|
let cryptoKey = null;
|
||||||
|
|
||||||
function hexToBytes(hex) {
|
function hexToBytes(hex) {
|
||||||
const bytes = new Uint8Array(hex.length / 2);
|
const bytes = new Uint8Array(hex.length / 2);
|
||||||
|
|
||||||
for (let i = 0; i < bytes.length; i++) {
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
function base64UrlToBytes(value) {
|
function base64UrlToBytes(value) {
|
||||||
let base64 = value.replace(/-/g, '+').replace(/_/g, '/');
|
let base64 = value.replace(/-/g, '+').replace(/_/g, '/');
|
||||||
|
|
||||||
const padding = base64.length % 4;
|
const padding = base64.length % 4;
|
||||||
|
|
||||||
if (padding === 2) {
|
if (padding === 2) {
|
||||||
base64 += '==';
|
base64 += '==';
|
||||||
} else if (padding === 3) {
|
} else if (padding === 3) {
|
||||||
base64 += '=';
|
base64 += '=';
|
||||||
} else if (padding === 1) {
|
} else if (padding === 1) {
|
||||||
throw new Error('Invalid base64url');
|
throw new Error('Invalid base64url');
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Uint8Array(Buffer.from(base64, 'base64'));
|
return new Uint8Array(Buffer.from(base64, 'base64'));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getKey() {
|
async function getKey() {
|
||||||
if (cryptoKey) return cryptoKey;
|
if (cryptoKey) return cryptoKey;
|
||||||
|
|
||||||
const keyBytes = hexToBytes(GCM_HEX_KEY);
|
const keyBytes = hexToBytes(GCM_HEX_KEY);
|
||||||
|
|
||||||
cryptoKey = await webcrypto.subtle.importKey(
|
cryptoKey = await webcrypto.subtle.importKey(
|
||||||
'raw',
|
'raw',
|
||||||
keyBytes,
|
keyBytes,
|
||||||
'AES-GCM',
|
'AES-GCM',
|
||||||
false,
|
false,
|
||||||
['decrypt']
|
['decrypt']
|
||||||
);
|
);
|
||||||
|
|
||||||
return cryptoKey;
|
return cryptoKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function decryptStreamUrl(value) {
|
async function decryptStreamUrl(value) {
|
||||||
try {
|
try {
|
||||||
const data = base64UrlToBytes(value);
|
const data = base64UrlToBytes(value);
|
||||||
|
|
||||||
if (data.length < 28) {
|
if (data.length < 28) {
|
||||||
throw new Error('Ciphertext too short');
|
throw new Error('Ciphertext too short');
|
||||||
}
|
}
|
||||||
|
|
||||||
const iv = data.slice(0, 12);
|
const iv = data.slice(0, 12);
|
||||||
const ciphertext = data.slice(12);
|
const ciphertext = data.slice(12);
|
||||||
|
|
||||||
const key = await getKey();
|
const key = await getKey();
|
||||||
|
|
||||||
const decrypted = await webcrypto.subtle.decrypt(
|
const decrypted = await webcrypto.subtle.decrypt(
|
||||||
{
|
{
|
||||||
name: 'AES-GCM',
|
name: 'AES-GCM',
|
||||||
iv
|
iv
|
||||||
},
|
},
|
||||||
key,
|
key,
|
||||||
ciphertext
|
ciphertext
|
||||||
);
|
);
|
||||||
|
|
||||||
return new TextDecoder().decode(decrypted);
|
return new TextDecoder().decode(decrypted);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStream({ id, s, e }) {
|
export async function getStream({ id, s, e }) {
|
||||||
try {
|
try {
|
||||||
const path = s
|
const path = s
|
||||||
? `tv/${id}/${s}/${e || 1}`
|
? `tv/${id}/${s}/${e || 1}`
|
||||||
: `movie/${id}`;
|
: `movie/${id}`;
|
||||||
|
|
||||||
const apiData = await fetchJson(`${BASE_URL}api/${path}`, {
|
const apiData = await fetchJson(`${BASE_URL}api/${path}`, {
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': USER_AGENT,
|
'User-Agent': USER_AGENT,
|
||||||
Referer: BASE_URL,
|
Referer: BASE_URL,
|
||||||
Origin: BASE_URL
|
Origin: BASE_URL
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!apiData || typeof apiData !== 'object') {
|
if (!apiData || typeof apiData !== 'object') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const streams = [];
|
const streams = [];
|
||||||
|
|
||||||
for (const [provider, info] of Object.entries(apiData)) {
|
for (const [provider, info] of Object.entries(apiData)) {
|
||||||
if (!info?.url) {
|
if (!info?.url) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const decryptedUrl = await decryptStreamUrl(info.url);
|
const decryptedUrl = await decryptStreamUrl(info.url);
|
||||||
|
|
||||||
if (!decryptedUrl) {
|
if (!decryptedUrl) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let url = decryptedUrl;
|
let url = decryptedUrl;
|
||||||
|
|
||||||
if (url.startsWith(PROXY_PREFIX)) {
|
if (url.startsWith(PROXY_PREFIX)) {
|
||||||
url = decodeURIComponent(
|
url = decodeURIComponent(
|
||||||
url.slice(PROXY_PREFIX.length).replace(/^\/+/, '')
|
url.slice(PROXY_PREFIX.length).replace(/^\/+/, '')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isXpass = url.includes('xpass.top');
|
const isXpass = url.includes('xpass.top');
|
||||||
|
|
||||||
streams.push({
|
streams.push({
|
||||||
name: provider,
|
name: provider,
|
||||||
url,
|
url,
|
||||||
file: url,
|
file: url,
|
||||||
stream: url,
|
stream: url,
|
||||||
src: url,
|
src: url,
|
||||||
quality: info.type || 'auto',
|
quality: info.type || 'auto',
|
||||||
type: info.type || 'hls',
|
type: info.type || 'hls',
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': USER_AGENT,
|
'User-Agent': USER_AGENT,
|
||||||
Referer: isXpass
|
Referer: isXpass
|
||||||
? 'https://play.xpass.top/'
|
? 'https://play.xpass.top/'
|
||||||
: BASE_URL,
|
: BASE_URL,
|
||||||
Origin: isXpass
|
Origin: isXpass
|
||||||
? 'https://play.xpass.top'
|
? 'https://play.xpass.top'
|
||||||
: 'https://vidrock.ru'
|
: 'https://vidrock.ru'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return streams.length ? { streams } : null;
|
return streams.length ? { streams } : null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,38 +1,38 @@
|
|||||||
import { fetchJson, fetchText, USER_AGENT, getTmdbInfo } from '../utils/helpers.js';
|
import { fetchJson, fetchText, USER_AGENT, getTmdbInfo } from '../utils/helpers.js';
|
||||||
|
|
||||||
const API_BASE = "https://enc-dec.app/api";
|
const API_BASE = "https://enc-dec.app/api";
|
||||||
const DOMAIN = "https://vidsync.xyz";
|
const DOMAIN = "https://vidsync.xyz";
|
||||||
const HEADERS = { "Accept": "*/*", "Origin": DOMAIN, "Referer": `${DOMAIN}/`, "User-Agent": USER_AGENT, "X-Requested-With": "XMLHttpRequest" };
|
const HEADERS = { "Accept": "*/*", "Origin": DOMAIN, "Referer": `${DOMAIN}/`, "User-Agent": USER_AGENT, "X-Requested-With": "XMLHttpRequest" };
|
||||||
const SERVERS = ["cinevault", "cinedub", "cinebox", "cineflix", "cinevip", "cinecloud", "cine4k"];
|
const SERVERS = ["cinevault", "cinedub", "cinebox", "cineflix", "cinevip", "cinecloud", "cine4k"];
|
||||||
|
|
||||||
export async function getStream({ id, s, e, server }) {
|
export async function getStream({ id, s, e, server }) {
|
||||||
try {
|
try {
|
||||||
const isTv = s != null && e != null;
|
const isTv = s != null && e != null;
|
||||||
const type = isTv ? 'tv' : 'movie';
|
const type = isTv ? 'tv' : 'movie';
|
||||||
const info = await getTmdbInfo(id, type);
|
const info = await getTmdbInfo(id, type);
|
||||||
if (!info?.titles?.length) return null;
|
if (!info?.titles?.length) return null;
|
||||||
const turnstileJson = await fetchJson(`${API_BASE}/enc-vidsync`, { signal: AbortSignal.timeout(5000) });
|
const turnstileJson = await fetchJson(`${API_BASE}/enc-vidsync`, { signal: AbortSignal.timeout(5000) });
|
||||||
if (turnstileJson.status !== 200 || !turnstileJson.result?.token) return null;
|
if (turnstileJson.status !== 200 || !turnstileJson.result?.token) return null;
|
||||||
const turnstileToken = turnstileJson.result.token;
|
const turnstileToken = turnstileJson.result.token;
|
||||||
let targets = SERVERS;
|
let targets = SERVERS;
|
||||||
if (server && server !== 'all') {
|
if (server && server !== 'all') {
|
||||||
const clean = server.replace('VidSync - ', '');
|
const clean = server.replace('VidSync - ', '');
|
||||||
targets = SERVERS.includes(clean) ? [clean] : SERVERS;
|
targets = SERVERS.includes(clean) ? [clean] : SERVERS;
|
||||||
}
|
}
|
||||||
const encTitle = encodeURIComponent(info.titles[0]).replace(/%20/g, '+');
|
const encTitle = encodeURIComponent(info.titles[0]).replace(/%20/g, '+');
|
||||||
const results = await Promise.allSettled(targets.map(async srv => {
|
const results = await Promise.allSettled(targets.map(async srv => {
|
||||||
let url = `${DOMAIN}/api/stream/fetch?title=${encTitle}&type=${type}&releaseYear=${info.year}&mediaId=${id}&serverName=${srv}`;
|
let url = `${DOMAIN}/api/stream/fetch?title=${encTitle}&type=${type}&releaseYear=${info.year}&mediaId=${id}&serverName=${srv}`;
|
||||||
if (isTv) url += `&season=${s}&episode=${e}`;
|
if (isTv) url += `&season=${s}&episode=${e}`;
|
||||||
const encryptedText = await fetchText(url, { headers: { ...HEADERS, "X-Cf-Turnstile": turnstileToken }, signal: AbortSignal.timeout(10000) });
|
const encryptedText = await fetchText(url, { headers: { ...HEADERS, "X-Cf-Turnstile": turnstileToken }, signal: AbortSignal.timeout(10000) });
|
||||||
const decJson = await fetchJson(`${API_BASE}/dec-vidsync`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: encryptedText, id: String(id) }), signal: AbortSignal.timeout(10000) });
|
const decJson = await fetchJson(`${API_BASE}/dec-vidsync`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: encryptedText, id: String(id) }), signal: AbortSignal.timeout(10000) });
|
||||||
if (decJson.status !== 200 || !decJson.result) throw new Error();
|
if (decJson.status !== 200 || !decJson.result) throw new Error();
|
||||||
const streams = Array.isArray(decJson.result) ? decJson.result : [decJson.result];
|
const streams = Array.isArray(decJson.result) ? decJson.result : [decJson.result];
|
||||||
return streams.map(st => ({ url: st.url || st.file, server: `VidSync - ${srv}`, quality: st.quality || "Auto", type: (st.url || st.file || "").includes(".m3u8") ? "hls" : "mp4", headers: { ...HEADERS, "Origin": DOMAIN }, skipProxy: false, skipVerify: true, skipHlsCheck: true }));
|
return streams.map(st => ({ url: st.url || st.file, server: `VidSync - ${srv}`, quality: st.quality || "Auto", type: (st.url || st.file || "").includes(".m3u8") ? "hls" : "mp4", headers: { ...HEADERS, "Origin": DOMAIN }, skipProxy: false, skipVerify: true, skipHlsCheck: true }));
|
||||||
}));
|
}));
|
||||||
const allUrls = [];
|
const allUrls = [];
|
||||||
for (const r of results) if (r.status === 'fulfilled') allUrls.push(...r.value);
|
for (const r of results) if (r.status === 'fulfilled') allUrls.push(...r.value);
|
||||||
return allUrls.length ? { allUrls } : null;
|
return allUrls.length ? { allUrls } : null;
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSources() { return SERVERS.map(s => `VidSync - ${s}`); }
|
export async function getSources() { return SERVERS.map(s => `VidSync - ${s}`); }
|
||||||
@@ -1,79 +1,79 @@
|
|||||||
import { fetchJson, fetchText, USER_AGENT } from '../utils/helpers.js';
|
import { fetchJson, fetchText, USER_AGENT } from '../utils/helpers.js';
|
||||||
|
|
||||||
const API_BASE = 'https://enc-dec.app/api';
|
const API_BASE = 'https://enc-dec.app/api';
|
||||||
const DOMAIN = 'https://vidup.to';
|
const DOMAIN = 'https://vidup.to';
|
||||||
const HEADERS = { 'User-Agent': USER_AGENT, 'Referer': `${DOMAIN}/`, 'X-Requested-With': 'XMLHttpRequest' };
|
const HEADERS = { 'User-Agent': USER_AGENT, 'Referer': `${DOMAIN}/`, 'X-Requested-With': 'XMLHttpRequest' };
|
||||||
|
|
||||||
async function getVidupMeta(id, s, e) {
|
async function getVidupMeta(id, s, e) {
|
||||||
try {
|
try {
|
||||||
const embedUrl = s != null && e != null ? `${DOMAIN}/tv/${id}/${s}/${e}/` : `${DOMAIN}/movie/${id}/`;
|
const embedUrl = s != null && e != null ? `${DOMAIN}/tv/${id}/${s}/${e}/` : `${DOMAIN}/movie/${id}/`;
|
||||||
const html = await fetchText(embedUrl, { headers: { 'User-Agent': USER_AGENT } });
|
const html = await fetchText(embedUrl, { headers: { 'User-Agent': USER_AGENT } });
|
||||||
|
|
||||||
const match = html.match(/\\"(?:en|token)\\":\\"(.*?)\\"/) || html.match(/"(?:en|token)":"(.*?)"/);
|
const match = html.match(/\\"(?:en|token)\\":\\"(.*?)\\"/) || html.match(/"(?:en|token)":"(.*?)"/);
|
||||||
if (!match?.[1]) return null;
|
if (!match?.[1]) return null;
|
||||||
|
|
||||||
const encData = await fetchJson(`${API_BASE}/enc-vidup?text=${encodeURIComponent(match[1])}`);
|
const encData = await fetchJson(`${API_BASE}/enc-vidup?text=${encodeURIComponent(match[1])}`);
|
||||||
if (encData.status !== 200 || !encData.result) return null;
|
if (encData.status !== 200 || !encData.result) return null;
|
||||||
|
|
||||||
const { servers: serversUrl, stream: streamUrl, token } = encData.result;
|
const { servers: serversUrl, stream: streamUrl, token } = encData.result;
|
||||||
const reqHeaders = { ...HEADERS, 'X-CSRF-Token': token };
|
const reqHeaders = { ...HEADERS, 'X-CSRF-Token': token };
|
||||||
|
|
||||||
const serversEncrypted = await fetchText(serversUrl, { method: 'POST', headers: reqHeaders });
|
const serversEncrypted = await fetchText(serversUrl, { method: 'POST', headers: reqHeaders });
|
||||||
const decServersData = await fetchJson(`${API_BASE}/dec-vidup`, {
|
const decServersData = await fetchJson(`${API_BASE}/dec-vidup`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ text: serversEncrypted })
|
body: JSON.stringify({ text: serversEncrypted })
|
||||||
});
|
});
|
||||||
|
|
||||||
if (decServersData.status !== 200 || !decServersData.result) return null;
|
if (decServersData.status !== 200 || !decServersData.result) return null;
|
||||||
return { servers: decServersData.result, streamUrl, reqHeaders };
|
return { servers: decServersData.result, streamUrl, reqHeaders };
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStream({ id, s, e, server }) {
|
export async function getStream({ id, s, e, server }) {
|
||||||
const meta = await getVidupMeta(id, s, e);
|
const meta = await getVidupMeta(id, s, e);
|
||||||
if (!meta?.servers?.length) return null;
|
if (!meta?.servers?.length) return null;
|
||||||
|
|
||||||
const { servers, streamUrl, reqHeaders } = meta;
|
const { servers, streamUrl, reqHeaders } = meta;
|
||||||
let targets = servers;
|
let targets = servers;
|
||||||
|
|
||||||
if (server && server !== 'all') {
|
if (server && server !== 'all') {
|
||||||
const cleanName = server.replace('VidUp - ', '');
|
const cleanName = server.replace('VidUp - ', '');
|
||||||
targets = servers.filter(srv => srv.name === cleanName);
|
targets = servers.filter(srv => srv.name === cleanName);
|
||||||
if (!targets.length) targets = servers;
|
if (!targets.length) targets = servers;
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = await Promise.allSettled(targets.map(async srv => {
|
const results = await Promise.allSettled(targets.map(async srv => {
|
||||||
const streamEncrypted = await fetchText(`${streamUrl}/${srv.data}`, { method: 'POST', headers: reqHeaders });
|
const streamEncrypted = await fetchText(`${streamUrl}/${srv.data}`, { method: 'POST', headers: reqHeaders });
|
||||||
const decStreamData = await fetchJson(`${API_BASE}/dec-vidup`, {
|
const decStreamData = await fetchJson(`${API_BASE}/dec-vidup`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ text: streamEncrypted })
|
body: JSON.stringify({ text: streamEncrypted })
|
||||||
});
|
});
|
||||||
|
|
||||||
if (decStreamData.status !== 200 || !decStreamData.result?.url) throw new Error();
|
if (decStreamData.status !== 200 || !decStreamData.result?.url) throw new Error();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url: decStreamData.result.url,
|
url: decStreamData.result.url,
|
||||||
server: `VidUp - ${srv.name || 'Server'}`,
|
server: `VidUp - ${srv.name || 'Server'}`,
|
||||||
type: decStreamData.result.url.includes('.m3u8') ? 'hls' : 'mp4',
|
type: decStreamData.result.url.includes('.m3u8') ? 'hls' : 'mp4',
|
||||||
subtitles: (decStreamData.result.captions || []).map(c => ({
|
subtitles: (decStreamData.result.captions || []).map(c => ({
|
||||||
url: c.file,
|
url: c.file,
|
||||||
lang: c.label || 'Unknown'
|
lang: c.label || 'Unknown'
|
||||||
})),
|
})),
|
||||||
headers: { ...HEADERS, 'Origin': DOMAIN }
|
headers: { ...HEADERS, 'Origin': DOMAIN }
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const allUrls = [];
|
const allUrls = [];
|
||||||
for (const r of results) {
|
for (const r of results) {
|
||||||
if (r.status === 'fulfilled') allUrls.push(r.value);
|
if (r.status === 'fulfilled') allUrls.push(r.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return allUrls.length ? { allUrls } : null;
|
return allUrls.length ? { allUrls } : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSources(args) {
|
export async function getSources(args) {
|
||||||
const meta = await getVidupMeta(args.id, args.s, args.e);
|
const meta = await getVidupMeta(args.id, args.s, args.e);
|
||||||
return meta?.servers ? meta.servers.map(srv => `VidUp - ${srv.name}`) : [];
|
return meta?.servers ? meta.servers.map(srv => `VidUp - ${srv.name}`) : [];
|
||||||
}
|
}
|
||||||
@@ -1,120 +1,120 @@
|
|||||||
import { webcrypto } from 'node:crypto';
|
import { webcrypto } from 'node:crypto';
|
||||||
import { fetchJson, USER_AGENT } from '../utils/helpers.js';
|
import { fetchJson, USER_AGENT } from '../utils/helpers.js';
|
||||||
|
|
||||||
const PLAYER_URL = 'https://player.vidzee.wtf';
|
const PLAYER_URL = 'https://player.vidzee.wtf';
|
||||||
const CORE_URL = 'https://core.vidzee.wtf';
|
const CORE_URL = 'https://core.vidzee.wtf';
|
||||||
|
|
||||||
let cachedKey = null, cachedKeyTs = 0;
|
let cachedKey = null, cachedKeyTs = 0;
|
||||||
|
|
||||||
async function getDecKey(headers) {
|
async function getDecKey(headers) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (cachedKey && now - cachedKeyTs < 300000) return cachedKey;
|
if (cachedKey && now - cachedKeyTs < 300000) return cachedKey;
|
||||||
|
|
||||||
const res = await fetch(`${CORE_URL}/api-key`, {
|
const res = await fetch(`${CORE_URL}/api-key`, {
|
||||||
headers,
|
headers,
|
||||||
signal: AbortSignal.timeout(7000)
|
signal: AbortSignal.timeout(7000)
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error(`API key fetch failed: ${res.status}`);
|
if (!res.ok) throw new Error(`API key fetch failed: ${res.status}`);
|
||||||
|
|
||||||
const e = await res.text();
|
const e = await res.text();
|
||||||
const t = Buffer.from(e.replace(/\s+/g, ''), 'base64');
|
const t = Buffer.from(e.replace(/\s+/g, ''), 'base64');
|
||||||
|
|
||||||
const s = new Uint8Array(t.length - 28 + 16);
|
const s = new Uint8Array(t.length - 28 + 16);
|
||||||
s.set(t.subarray(28), 0);
|
s.set(t.subarray(28), 0);
|
||||||
s.set(t.subarray(12, 28), t.length - 28);
|
s.set(t.subarray(12, 28), t.length - 28);
|
||||||
|
|
||||||
const keyMat = await webcrypto.subtle.digest('SHA-256', new TextEncoder().encode('c4a8f1d7e2b9a6c3d0f5e8a1b7c4d9e2'));
|
const keyMat = await webcrypto.subtle.digest('SHA-256', new TextEncoder().encode('c4a8f1d7e2b9a6c3d0f5e8a1b7c4d9e2'));
|
||||||
const key = await webcrypto.subtle.importKey('raw', keyMat, { name: 'AES-GCM' }, false, ['decrypt']);
|
const key = await webcrypto.subtle.importKey('raw', keyMat, { name: 'AES-GCM' }, false, ['decrypt']);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const decrypted = await webcrypto.subtle.decrypt({
|
const decrypted = await webcrypto.subtle.decrypt({
|
||||||
name: 'AES-GCM',
|
name: 'AES-GCM',
|
||||||
iv: t.subarray(0, 12),
|
iv: t.subarray(0, 12),
|
||||||
tagLength: 128
|
tagLength: 128
|
||||||
}, key, s);
|
}, key, s);
|
||||||
|
|
||||||
cachedKey = new TextDecoder().decode(decrypted);
|
cachedKey = new TextDecoder().decode(decrypted);
|
||||||
cachedKeyTs = now;
|
cachedKeyTs = now;
|
||||||
return cachedKey;
|
return cachedKey;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new Error('Failed to decrypt Vidzee master key');
|
throw new Error('Failed to decrypt Vidzee master key');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function decrypt(enc, key) {
|
function decrypt(enc, key) {
|
||||||
try {
|
try {
|
||||||
const dec = Buffer.from(enc, 'base64').toString('utf8');
|
const dec = Buffer.from(enc, 'base64').toString('utf8');
|
||||||
const sep = dec.indexOf(':');
|
const sep = dec.indexOf(':');
|
||||||
if (sep === -1) return '';
|
if (sep === -1) return '';
|
||||||
|
|
||||||
const ivBase64 = dec.slice(0, sep);
|
const ivBase64 = dec.slice(0, sep);
|
||||||
const ciphertext = dec.slice(sep + 1);
|
const ciphertext = dec.slice(sep + 1);
|
||||||
const keyPadded = key.padEnd(32, '\0');
|
const keyPadded = key.padEnd(32, '\0');
|
||||||
|
|
||||||
const decrypted = CryptoJS.AES.decrypt(
|
const decrypted = CryptoJS.AES.decrypt(
|
||||||
ciphertext,
|
ciphertext,
|
||||||
CryptoJS.enc.Utf8.parse(keyPadded),
|
CryptoJS.enc.Utf8.parse(keyPadded),
|
||||||
{
|
{
|
||||||
iv: CryptoJS.enc.Base64.parse(ivBase64),
|
iv: CryptoJS.enc.Base64.parse(ivBase64),
|
||||||
mode: CryptoJS.mode.CBC,
|
mode: CryptoJS.mode.CBC,
|
||||||
padding: CryptoJS.pad.Pkcs7
|
padding: CryptoJS.pad.Pkcs7
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return decrypted.toString(CryptoJS.enc.Utf8) || '';
|
return decrypted.toString(CryptoJS.enc.Utf8) || '';
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStream({ id, s, e, clientIP }) {
|
export async function getStream({ id, s, e, clientIP }) {
|
||||||
const headers = {
|
const headers = {
|
||||||
'User-Agent': USER_AGENT,
|
'User-Agent': USER_AGENT,
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
'Referer': PLAYER_URL,
|
'Referer': PLAYER_URL,
|
||||||
'Origin': PLAYER_URL,
|
'Origin': PLAYER_URL,
|
||||||
...(clientIP && { 'X-Forwarded-For': clientIP })
|
...(clientIP && { 'X-Forwarded-For': clientIP })
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const decKey = await getDecKey(headers);
|
const decKey = await getDecKey(headers);
|
||||||
|
|
||||||
const serversToTry = [3, 0, 1, 2, 4, 5, 6, 8];
|
const serversToTry = [3, 0, 1, 2, 4, 5, 6, 8];
|
||||||
|
|
||||||
for (const sr of serversToTry) {
|
for (const sr of serversToTry) {
|
||||||
try {
|
try {
|
||||||
const url = `${PLAYER_URL}/api/server?id=${id}&sr=${sr}${s ? `&ss=${s}&ep=${e || 1}` : ''}`;
|
const url = `${PLAYER_URL}/api/server?id=${id}&sr=${sr}${s ? `&ss=${s}&ep=${e || 1}` : ''}`;
|
||||||
const data = await fetchJson(url, { headers, signal: AbortSignal.timeout(5000) });
|
const data = await fetchJson(url, { headers, signal: AbortSignal.timeout(5000) });
|
||||||
|
|
||||||
if (data?.url?.length) {
|
if (data?.url?.length) {
|
||||||
for (const entry of data.url) {
|
for (const entry of data.url) {
|
||||||
if (!entry.link) continue;
|
if (!entry.link) continue;
|
||||||
|
|
||||||
const decrypted = decrypt(entry.link, decKey);
|
const decrypted = decrypt(entry.link, decKey);
|
||||||
if (decrypted?.startsWith('http')) {
|
if (decrypted?.startsWith('http')) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
allUrls: [
|
allUrls: [
|
||||||
{
|
{
|
||||||
url: decrypted,
|
url: decrypted,
|
||||||
skipHlsCheck: true,
|
skipHlsCheck: true,
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': USER_AGENT,
|
'User-Agent': USER_AGENT,
|
||||||
'Referer': PLAYER_URL,
|
'Referer': PLAYER_URL,
|
||||||
'Origin': PLAYER_URL
|
'Origin': PLAYER_URL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,98 +1,98 @@
|
|||||||
import { fetchText, USER_AGENT } from '../utils/helpers.js';
|
import { fetchText, USER_AGENT } from '../utils/helpers.js';
|
||||||
|
|
||||||
const BASE_URL = 'https://vixsrc.to';
|
const BASE_URL = 'https://vixsrc.to';
|
||||||
|
|
||||||
const HEADERS = {
|
const HEADERS = {
|
||||||
'User-Agent': USER_AGENT,
|
'User-Agent': USER_AGENT,
|
||||||
'Accept': 'application/json, text/javascript, */*; q=0.01',
|
'Accept': 'application/json, text/javascript, */*; q=0.01',
|
||||||
'Accept-Language': 'en-US,en;q=0.9',
|
'Accept-Language': 'en-US,en;q=0.9',
|
||||||
'Referer': `${BASE_URL}/`,
|
'Referer': `${BASE_URL}/`,
|
||||||
'Origin': BASE_URL
|
'Origin': BASE_URL
|
||||||
};
|
};
|
||||||
|
|
||||||
function extract(regex, text) {
|
function extract(regex, text) {
|
||||||
const m = text?.match(regex);
|
const m = text?.match(regex);
|
||||||
if (!m) return null;
|
if (!m) return null;
|
||||||
return m[1].replace(/\\/g, '');
|
return m[1].replace(/\\/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStream({ id, s, e, absoluteBase }) {
|
export async function getStream({ id, s, e, absoluteBase }) {
|
||||||
try {
|
try {
|
||||||
const apiUrl = s && e
|
const apiUrl = s && e
|
||||||
? `${BASE_URL}/api/tv/${id}/${s}/${e}`
|
? `${BASE_URL}/api/tv/${id}/${s}/${e}`
|
||||||
: `${BASE_URL}/api/movie/${id}`;
|
: `${BASE_URL}/api/movie/${id}`;
|
||||||
|
|
||||||
const apiProxy = absoluteBase
|
const apiProxy = absoluteBase
|
||||||
? `${absoluteBase.replace(/^https:\/\/(localhost|127\.0\.0\.1)/, 'http://$1')}/api?url=${encodeURIComponent(apiUrl)}&proxyHeaders=${encodeURIComponent(JSON.stringify(HEADERS))}`
|
? `${absoluteBase.replace(/^https:\/\/(localhost|127\.0\.0\.1)/, 'http://$1')}/api?url=${encodeURIComponent(apiUrl)}&proxyHeaders=${encodeURIComponent(JSON.stringify(HEADERS))}`
|
||||||
: apiUrl;
|
: apiUrl;
|
||||||
|
|
||||||
const apiText = await fetchText(apiProxy).catch(() => null);
|
const apiText = await fetchText(apiProxy).catch(() => null);
|
||||||
const apiData = JSON.parse(apiText || '{}');
|
const apiData = JSON.parse(apiText || '{}');
|
||||||
if (!apiData.src) return null;
|
if (!apiData.src) return null;
|
||||||
|
|
||||||
const embedUrl = apiData.src.startsWith('http') ? apiData.src : `${BASE_URL}${apiData.src}`;
|
const embedUrl = apiData.src.startsWith('http') ? apiData.src : `${BASE_URL}${apiData.src}`;
|
||||||
const embedProxy = absoluteBase
|
const embedProxy = absoluteBase
|
||||||
? `${absoluteBase.replace(/^https:\/\/(localhost|127\.0\.0\.1)/, 'http://$1')}/api?url=${encodeURIComponent(embedUrl)}&proxyHeaders=${encodeURIComponent(JSON.stringify(HEADERS))}`
|
? `${absoluteBase.replace(/^https:\/\/(localhost|127\.0\.0\.1)/, 'http://$1')}/api?url=${encodeURIComponent(embedUrl)}&proxyHeaders=${encodeURIComponent(JSON.stringify(HEADERS))}`
|
||||||
: embedUrl;
|
: embedUrl;
|
||||||
|
|
||||||
const html = await fetchText(embedProxy).catch(() => null);
|
const html = await fetchText(embedProxy).catch(() => null);
|
||||||
if (!html) return null;
|
if (!html) return null;
|
||||||
|
|
||||||
const token = extract(/token["']\s*:\s*["']([^"']+)/, html);
|
const token = extract(/token["']\s*:\s*["']([^"']+)/, html);
|
||||||
const expires = extract(/expires["']\s*:\s*["']([^"']+)/, html);
|
const expires = extract(/expires["']\s*:\s*["']([^"']+)/, html);
|
||||||
let playlist = extract(/url["']\s*:\s*["']([^"']+)/, html);
|
let playlist = extract(/url["']\s*:\s*["']([^"']+)/, html);
|
||||||
if (!token || !expires || !playlist) return null;
|
if (!token || !expires || !playlist) return null;
|
||||||
|
|
||||||
playlist = playlist.replace(/\\/g, '');
|
playlist = playlist.replace(/\\/g, '');
|
||||||
const lang = extract(/lang(?:uage)?["']\s*:\s*["']([a-z]{2,5})/i, html) || 'en';
|
const lang = extract(/lang(?:uage)?["']\s*:\s*["']([a-z]{2,5})/i, html) || 'en';
|
||||||
const rawMasterUrl = `${playlist}${playlist.includes('?') ? '&' : '?'}token=${token}&expires=${expires}&h=1&lang=${lang}`;
|
const rawMasterUrl = `${playlist}${playlist.includes('?') ? '&' : '?'}token=${token}&expires=${expires}&h=1&lang=${lang}`;
|
||||||
|
|
||||||
const masterProxy = absoluteBase
|
const masterProxy = absoluteBase
|
||||||
? `${absoluteBase.replace(/^https:\/\/(localhost|127\.0\.0\.1)/, 'http://$1')}/api?url=${encodeURIComponent(rawMasterUrl)}&proxyHeaders=${encodeURIComponent(JSON.stringify(HEADERS))}`
|
? `${absoluteBase.replace(/^https:\/\/(localhost|127\.0\.0\.1)/, 'http://$1')}/api?url=${encodeURIComponent(rawMasterUrl)}&proxyHeaders=${encodeURIComponent(JSON.stringify(HEADERS))}`
|
||||||
: rawMasterUrl;
|
: rawMasterUrl;
|
||||||
|
|
||||||
const playlistText = await fetchText(rawMasterUrl, { headers: HEADERS }).catch(() => null);
|
const playlistText = await fetchText(rawMasterUrl, { headers: HEADERS }).catch(() => null);
|
||||||
if (!playlistText?.includes('#EXTM3U')) return null;
|
if (!playlistText?.includes('#EXTM3U')) return null;
|
||||||
|
|
||||||
let finalUrl = null;
|
let finalUrl = null;
|
||||||
let bestHeight = 0;
|
let bestHeight = 0;
|
||||||
const lines = playlistText.split('\n');
|
const lines = playlistText.split('\n');
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
const line = lines[i];
|
const line = lines[i];
|
||||||
if (!line.startsWith('#EXT-X-STREAM-INF')) continue;
|
if (!line.startsWith('#EXT-X-STREAM-INF')) continue;
|
||||||
const height = parseInt(line.match(/RESOLUTION=\d+x(\d+)/)?.[1] || '0', 10);
|
const height = parseInt(line.match(/RESOLUTION=\d+x(\d+)/)?.[1] || '0', 10);
|
||||||
let next = lines[i + 1]?.trim();
|
let next = lines[i + 1]?.trim();
|
||||||
if (!next || next.startsWith('#')) continue;
|
if (!next || next.startsWith('#')) continue;
|
||||||
|
|
||||||
if (height > bestHeight) {
|
if (height > bestHeight) {
|
||||||
bestHeight = height;
|
bestHeight = height;
|
||||||
finalUrl = next;
|
finalUrl = next;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!finalUrl) finalUrl = rawMasterUrl;
|
if (!finalUrl) finalUrl = rawMasterUrl;
|
||||||
|
|
||||||
if (!finalUrl.startsWith('http')) {
|
if (!finalUrl.startsWith('http')) {
|
||||||
const base = rawMasterUrl.split('/playlist/')[0];
|
const base = rawMasterUrl.split('/playlist/')[0];
|
||||||
finalUrl = finalUrl.startsWith('/') ? `${base}${finalUrl}` : `${base}/${finalUrl}`;
|
finalUrl = finalUrl.startsWith('/') ? `${base}${finalUrl}` : `${base}/${finalUrl}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const alreadyProxied = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?\/api\?url=/.test(finalUrl);
|
const alreadyProxied = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?\/api\?url=/.test(finalUrl);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
allUrls: [
|
allUrls: [
|
||||||
{
|
{
|
||||||
url: finalUrl,
|
url: finalUrl,
|
||||||
skipProxy: alreadyProxied,
|
skipProxy: alreadyProxied,
|
||||||
headers: {
|
headers: {
|
||||||
...HEADERS,
|
...HEADERS,
|
||||||
'Referer': `${BASE_URL}/`
|
'Referer': `${BASE_URL}/`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,40 +1,40 @@
|
|||||||
import { fetchJson, fetchText, USER_AGENT } from '../utils/helpers.js';
|
import { fetchJson, fetchText, USER_AGENT } from '../utils/helpers.js';
|
||||||
|
|
||||||
const BASE = "https://play.xpass.top";
|
const BASE = "https://play.xpass.top";
|
||||||
const HEADERS = { "accept": "*/*", "accept-language": "en-US,en;q=0.9", "cache-control": "no-cache", "pragma": "no-cache", "User-Agent": USER_AGENT, "Origin": BASE, "Referrer": `${BASE}/`, "Cookie": "auth_token=de21073d24bca9b50f189b402ac870734cf945f2085cb7e1a4fc453fcfe4f57e" };
|
const HEADERS = { "accept": "*/*", "accept-language": "en-US,en;q=0.9", "cache-control": "no-cache", "pragma": "no-cache", "User-Agent": USER_AGENT, "Origin": BASE, "Referrer": `${BASE}/`, "Cookie": "auth_token=de21073d24bca9b50f189b402ac870734cf945f2085cb7e1a4fc453fcfe4f57e" };
|
||||||
|
|
||||||
export async function getStream({ id, s, e, server }) {
|
export async function getStream({ id, s, e, server }) {
|
||||||
const isTv = s && e && s !== 'null' && e !== 'null';
|
const isTv = s && e && s !== 'null' && e !== 'null';
|
||||||
const hdrs = { ...HEADERS, referer: `${BASE}/e/${isTv ? 'tv' : 'movie'}/${id}?autostart=true` };
|
const hdrs = { ...HEADERS, referer: `${BASE}/e/${isTv ? 'tv' : 'movie'}/${id}?autostart=true` };
|
||||||
try {
|
try {
|
||||||
let sources = [];
|
let sources = [];
|
||||||
if (!isTv) {
|
if (!isTv) {
|
||||||
const text = await fetchText(`${BASE}/e/movie/${id}?autostart=true`, { headers: hdrs, signal: AbortSignal.timeout(7000) });
|
const text = await fetchText(`${BASE}/e/movie/${id}?autostart=true`, { headers: hdrs, signal: AbortSignal.timeout(7000) });
|
||||||
const match = text.match(/var backups=(\[[\s\S]*?\])/);
|
const match = text.match(/var backups=(\[[\s\S]*?\])/);
|
||||||
if (match) sources = JSON.parse(match[1]);
|
if (match) sources = JSON.parse(match[1]);
|
||||||
} else {
|
} else {
|
||||||
sources = await fetchJson(`${BASE}/data/tv/${id}/${s}/${e}?autostart=true&force=true`, { headers: hdrs, signal: AbortSignal.timeout(7000) });
|
sources = await fetchJson(`${BASE}/data/tv/${id}/${s}/${e}?autostart=true&force=true`, { headers: hdrs, signal: AbortSignal.timeout(7000) });
|
||||||
}
|
}
|
||||||
if (!sources?.length) return null;
|
if (!sources?.length) return null;
|
||||||
if (server && server !== 'all') {
|
if (server && server !== 'all') {
|
||||||
const exact = sources.find(src => src.name === server.replace('XPass - ', ''));
|
const exact = sources.find(src => src.name === server.replace('XPass - ', ''));
|
||||||
if (exact) sources = [exact]; else return null;
|
if (exact) sources = [exact]; else return null;
|
||||||
}
|
}
|
||||||
const allUrls = [];
|
const allUrls = [];
|
||||||
for (const source of sources) {
|
for (const source of sources) {
|
||||||
if (!source.url) continue;
|
if (!source.url) continue;
|
||||||
try {
|
try {
|
||||||
const mdata = await fetchJson(`${BASE}${source.url}`, { headers: hdrs, signal: AbortSignal.timeout(5000) });
|
const mdata = await fetchJson(`${BASE}${source.url}`, { headers: hdrs, signal: AbortSignal.timeout(5000) });
|
||||||
if (!mdata?.playlist?.[0]?.sources) continue;
|
if (!mdata?.playlist?.[0]?.sources) continue;
|
||||||
const targetStream = mdata.playlist[0].sources.find(st => st.type === 'hls') || mdata.playlist[0].sources[0];
|
const targetStream = mdata.playlist[0].sources.find(st => st.type === 'hls') || mdata.playlist[0].sources[0];
|
||||||
if (targetStream?.file) allUrls.push({ server: `XPass - ${source.name}`, type: targetStream.type === 'hls' ? 'hls' : 'mp4', url: targetStream.file, headers: { "Origin": HEADERS.Origin, "Referer": HEADERS.Referrer, "User-Agent": HEADERS["User-Agent"] } });
|
if (targetStream?.file) allUrls.push({ server: `XPass - ${source.name}`, type: targetStream.type === 'hls' ? 'hls' : 'mp4', url: targetStream.file, headers: { "Origin": HEADERS.Origin, "Referer": HEADERS.Referrer, "User-Agent": HEADERS["User-Agent"] } });
|
||||||
} catch { }
|
} catch { }
|
||||||
}
|
}
|
||||||
return allUrls.length ? { allUrls } : null;
|
return allUrls.length ? { allUrls } : null;
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSources(args) {
|
export async function getSources(args) {
|
||||||
const stream = await getStream(args);
|
const stream = await getStream(args);
|
||||||
return stream?.allUrls ? stream.allUrls.map(u => u.server) : [];
|
return stream?.allUrls ? stream.allUrls.map(u => u.server) : [];
|
||||||
}
|
}
|
||||||
252
src/subtitles.js
252
src/subtitles.js
@@ -1,127 +1,127 @@
|
|||||||
import { getUA } from './utils/helpers.js';
|
import { getUA } from './utils/helpers.js';
|
||||||
|
|
||||||
const SUBTITLE_BASES = [
|
const SUBTITLE_BASES = [
|
||||||
'https://sub.vdrk.site/v1',
|
'https://sub.vdrk.site/v1',
|
||||||
'https://sub.vdrk.site/v2',
|
'https://sub.vdrk.site/v2',
|
||||||
'https://fed-subs.pstream.mov'
|
'https://fed-subs.pstream.mov'
|
||||||
];
|
];
|
||||||
const VYLIAN_MESSAGES = [
|
const VYLIAN_MESSAGES = [
|
||||||
"Thanks for using Vyla!"
|
"Thanks for using Vyla!"
|
||||||
];
|
];
|
||||||
|
|
||||||
function generateVylianVtt() {
|
function generateVylianVtt() {
|
||||||
let vttContent = 'WEBVTT\n\n';
|
let vttContent = 'WEBVTT\n\n';
|
||||||
|
|
||||||
let currentTime = 1;
|
let currentTime = 1;
|
||||||
while (currentTime <= 7200) {
|
while (currentTime <= 7200) {
|
||||||
const duration = Math.floor(Math.random() * 4) + 1;
|
const duration = Math.floor(Math.random() * 4) + 1;
|
||||||
const message = VYLIAN_MESSAGES[Math.floor(Math.random() * VYLIAN_MESSAGES.length)];
|
const message = VYLIAN_MESSAGES[Math.floor(Math.random() * VYLIAN_MESSAGES.length)];
|
||||||
|
|
||||||
const startTime = formatVttTime(currentTime);
|
const startTime = formatVttTime(currentTime);
|
||||||
const endTime = formatVttTime(currentTime + duration);
|
const endTime = formatVttTime(currentTime + duration);
|
||||||
|
|
||||||
vttContent += `${startTime} --> ${endTime}\n${message}\n\n`;
|
vttContent += `${startTime} --> ${endTime}\n${message}\n\n`;
|
||||||
|
|
||||||
currentTime += duration + Math.floor(Math.random() * 60) + 30;
|
currentTime += duration + Math.floor(Math.random() * 60) + 30;
|
||||||
}
|
}
|
||||||
|
|
||||||
return vttContent;
|
return vttContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatVttTime(seconds) {
|
function formatVttTime(seconds) {
|
||||||
const hours = Math.floor(seconds / 3600);
|
const hours = Math.floor(seconds / 3600);
|
||||||
const minutes = Math.floor((seconds % 3600) / 60);
|
const minutes = Math.floor((seconds % 3600) / 60);
|
||||||
const secs = seconds % 60;
|
const secs = seconds % 60;
|
||||||
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}.000`;
|
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}.000`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchSubtitles(paths = []) {
|
export async function fetchSubtitles(paths = []) {
|
||||||
try {
|
try {
|
||||||
const results = await Promise.all(
|
const results = await Promise.all(
|
||||||
paths.map(async ({ base, path }) => {
|
paths.map(async ({ base, path }) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${base}${path}`, {
|
const res = await fetch(`${base}${path}`, {
|
||||||
headers: { 'User-Agent': getUA() },
|
headers: { 'User-Agent': getUA() },
|
||||||
signal: AbortSignal.timeout(5000),
|
signal: AbortSignal.timeout(5000),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
res.body?.cancel();
|
res.body?.cancel();
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (base.includes('/v2')) {
|
if (base.includes('/v2')) {
|
||||||
return Array.isArray(data)
|
return Array.isArray(data)
|
||||||
? data.map(x => ({
|
? data.map(x => ({
|
||||||
label: x.label,
|
label: x.label,
|
||||||
file: x.file || x.url,
|
file: x.file || x.url,
|
||||||
type: 'vtt',
|
type: 'vtt',
|
||||||
source: 'v2'
|
source: 'v2'
|
||||||
}))
|
}))
|
||||||
: [];
|
: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (base.includes('fed-subs.pstream.mov')) {
|
if (base.includes('fed-subs.pstream.mov')) {
|
||||||
if (!data?.subtitles || typeof data.subtitles !== 'object') return [];
|
if (!data?.subtitles || typeof data.subtitles !== 'object') return [];
|
||||||
|
|
||||||
return Object.entries(data.subtitles)
|
return Object.entries(data.subtitles)
|
||||||
.map(([language, sub]) => {
|
.map(([language, sub]) => {
|
||||||
if (!sub?.subtitle_link) return null;
|
if (!sub?.subtitle_link) return null;
|
||||||
const ext = sub.subtitle_link.split('.').pop()?.toLowerCase();
|
const ext = sub.subtitle_link.split('.').pop()?.toLowerCase();
|
||||||
return {
|
return {
|
||||||
label: sub.subtitle_name || language,
|
label: sub.subtitle_name || language,
|
||||||
file: sub.subtitle_link,
|
file: sub.subtitle_link,
|
||||||
type: ext === 'vtt' ? 'vtt' : 'srt',
|
type: ext === 'vtt' ? 'vtt' : 'srt',
|
||||||
source: 'febbox'
|
source: 'febbox'
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
const v1 = Array.isArray(data) ? data : [];
|
const v1 = Array.isArray(data) ? data : [];
|
||||||
return v1.map(x => ({
|
return v1.map(x => ({
|
||||||
label: x.label,
|
label: x.label,
|
||||||
file: x.file || x.url,
|
file: x.file || x.url,
|
||||||
type: 'vtt',
|
type: 'vtt',
|
||||||
source: 'v1'
|
source: 'v1'
|
||||||
}));
|
}));
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const val = results.flat();
|
const val = results.flat();
|
||||||
|
|
||||||
val.push({
|
val.push({
|
||||||
label: 'Vylian',
|
label: 'Vylian',
|
||||||
file: 'data:text/vtt;base64,' + Buffer.from(generateVylianVtt()).toString('base64'),
|
file: 'data:text/vtt;base64,' + Buffer.from(generateVylianVtt()).toString('base64'),
|
||||||
type: 'vtt',
|
type: 'vtt',
|
||||||
source: 'easter-egg'
|
source: 'easter-egg'
|
||||||
});
|
});
|
||||||
|
|
||||||
return val;
|
return val;
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSubPathsMovie(id) {
|
export async function getSubPathsMovie(id) {
|
||||||
return [
|
return [
|
||||||
{ base: SUBTITLE_BASES[0], path: `/movie/${id}` },
|
{ base: SUBTITLE_BASES[0], path: `/movie/${id}` },
|
||||||
{ base: SUBTITLE_BASES[1], path: `/movie/${id}` },
|
{ base: SUBTITLE_BASES[1], path: `/movie/${id}` },
|
||||||
{ base: SUBTITLE_BASES[2], path: `/movie/tt${id}` }
|
{ base: SUBTITLE_BASES[2], path: `/movie/tt${id}` }
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSubPathsTv(id, season, episode) {
|
export async function getSubPathsTv(id, season, episode) {
|
||||||
return [
|
return [
|
||||||
{ base: SUBTITLE_BASES[0], path: `/tv/${id}/${season}/${episode}` },
|
{ base: SUBTITLE_BASES[0], path: `/tv/${id}/${season}/${episode}` },
|
||||||
{ base: SUBTITLE_BASES[1], path: `/tv/${id}/${season}/${episode}` },
|
{ base: SUBTITLE_BASES[1], path: `/tv/${id}/${season}/${episode}` },
|
||||||
{ base: SUBTITLE_BASES[2], path: `/tv/tt${id}/${season}/${episode}` }
|
{ base: SUBTITLE_BASES[2], path: `/tv/tt${id}/${season}/${episode}` }
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export { SUBTITLE_BASES };
|
export { SUBTITLE_BASES };
|
||||||
@@ -1,194 +1,194 @@
|
|||||||
import crypto from 'node:crypto';
|
import crypto from 'node:crypto';
|
||||||
|
|
||||||
export const UA_LIST = [
|
export const UA_LIST = [
|
||||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
||||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15',
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15',
|
||||||
'Mozilla/5.0 (X11; Linux x86_64; rv:125.0) Gecko/20100101 Firefox/125.0',
|
'Mozilla/5.0 (X11; Linux x86_64; rv:125.0) Gecko/20100101 Firefox/125.0',
|
||||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0',
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0',
|
||||||
];
|
];
|
||||||
|
|
||||||
export const getUA = () => UA_LIST[Math.floor(Math.random() * UA_LIST.length)];
|
export const getUA = () => UA_LIST[Math.floor(Math.random() * UA_LIST.length)];
|
||||||
export const USER_AGENT = UA_LIST[0];
|
export const USER_AGENT = UA_LIST[0];
|
||||||
|
|
||||||
let sharedDcKey = null;
|
let sharedDcKey = null;
|
||||||
|
|
||||||
export function decryptPartner(responseText, keyEnv) {
|
export function decryptPartner(responseText, keyEnv) {
|
||||||
try {
|
try {
|
||||||
const sep = responseText.indexOf(':');
|
const sep = responseText.indexOf(':');
|
||||||
if (sep === -1) return null;
|
if (sep === -1) return null;
|
||||||
const iv = Buffer.from(responseText.slice(0, sep), 'base64');
|
const iv = Buffer.from(responseText.slice(0, sep), 'base64');
|
||||||
const encrypted = responseText.slice(sep + 1);
|
const encrypted = responseText.slice(sep + 1);
|
||||||
if (!sharedDcKey) sharedDcKey = crypto.createHash('sha256').update(keyEnv || '').digest();
|
if (!sharedDcKey) sharedDcKey = crypto.createHash('sha256').update(keyEnv || '').digest();
|
||||||
const decipher = crypto.createDecipheriv('aes-256-cbc', sharedDcKey, iv);
|
const decipher = crypto.createDecipheriv('aes-256-cbc', sharedDcKey, iv);
|
||||||
return JSON.parse(decipher.update(encrypted, 'base64', 'utf8') + decipher.final('utf8'));
|
return JSON.parse(decipher.update(encrypted, 'base64', 'utf8') + decipher.final('utf8'));
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchJson(url, options) {
|
export async function fetchJson(url, options) {
|
||||||
const res = await fetch(url, options);
|
const res = await fetch(url, options);
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchText(url, options) {
|
export async function fetchText(url, options) {
|
||||||
const res = await fetch(url, options);
|
const res = await fetch(url, options);
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
return res.text();
|
return res.text();
|
||||||
}
|
}
|
||||||
|
|
||||||
const tmdbInfoCache = new Map();
|
const tmdbInfoCache = new Map();
|
||||||
const anilistCache = new Map();
|
const anilistCache = new Map();
|
||||||
const tmdbValidationCache = new Map();
|
const tmdbValidationCache = new Map();
|
||||||
|
|
||||||
function pruneHelperCache(cache) {
|
function pruneHelperCache(cache) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
for (const [k, v] of cache) if (now - v.ts >= v.ttl) cache.delete(k);
|
for (const [k, v] of cache) if (now - v.ts >= v.ttl) cache.delete(k);
|
||||||
if (cache.size > 3000) {
|
if (cache.size > 3000) {
|
||||||
const overflow = cache.size - 3000;
|
const overflow = cache.size - 3000;
|
||||||
const it = cache.keys();
|
const it = cache.keys();
|
||||||
for (let i = 0; i < overflow; i++) cache.delete(it.next().value);
|
for (let i = 0; i < overflow; i++) cache.delete(it.next().value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
pruneHelperCache(tmdbValidationCache);
|
pruneHelperCache(tmdbValidationCache);
|
||||||
pruneHelperCache(tmdbInfoCache);
|
pruneHelperCache(tmdbInfoCache);
|
||||||
pruneHelperCache(anilistCache);
|
pruneHelperCache(anilistCache);
|
||||||
}, 60000).unref();
|
}, 60000).unref();
|
||||||
|
|
||||||
function cacheGet(cache, key) {
|
function cacheGet(cache, key) {
|
||||||
const entry = cache.get(key);
|
const entry = cache.get(key);
|
||||||
if (!entry) return undefined;
|
if (!entry) return undefined;
|
||||||
if (Date.now() - entry.ts >= entry.ttl) { cache.delete(key); return undefined; }
|
if (Date.now() - entry.ts >= entry.ttl) { cache.delete(key); return undefined; }
|
||||||
return entry.val;
|
return entry.val;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cacheSet(cache, key, val, ttl) { cache.set(key, { val, ts: Date.now(), ttl }); }
|
function cacheSet(cache, key, val, ttl) { cache.set(key, { val, ts: Date.now(), ttl }); }
|
||||||
|
|
||||||
export async function createStreamArgs(source, tmdbApiKey, id, s, e, clientIP=null) {
|
export async function createStreamArgs(source, tmdbApiKey, id, s, e, clientIP=null) {
|
||||||
const getAbsoluteBase = host => (host.startsWith('localhost') || host.startsWith('127.0.0.1')) ? `http://${host}` : `https://${host}`;
|
const getAbsoluteBase = host => (host.startsWith('localhost') || host.startsWith('127.0.0.1')) ? `http://${host}` : `https://${host}`;
|
||||||
const audio = /dub$/.test(source.key) ? 'dub' : 'sub';
|
const audio = /dub$/.test(source.key) ? 'dub' : 'sub';
|
||||||
return {
|
return {
|
||||||
id: id,
|
id: id,
|
||||||
s: s,
|
s: s,
|
||||||
e: e,
|
e: e,
|
||||||
clientIP: clientIP,
|
clientIP: clientIP,
|
||||||
absoluteBase: '/',
|
absoluteBase: '/',
|
||||||
audio,
|
audio,
|
||||||
tmdbApiKey,
|
tmdbApiKey,
|
||||||
config: source,
|
config: source,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function validateTmdbId(tmdbApiKey, tmdbId, mediaType = 'movie') {
|
export async function validateTmdbId(tmdbApiKey, tmdbId, mediaType = 'movie') {
|
||||||
const key = `${tmdbId}-${mediaType}`;
|
const key = `${tmdbId}-${mediaType}`;
|
||||||
const cached = cacheGet(tmdbValidationCache, key);
|
const cached = cacheGet(tmdbValidationCache, key);
|
||||||
if (cached !== undefined) return cached;
|
if (cached !== undefined) return cached;
|
||||||
const k = tmdbApiKey;
|
const k = tmdbApiKey;
|
||||||
if (!k) return { valid: false, error: 'TMDB API key not configured' };
|
if (!k) return { valid: false, error: 'TMDB API key not configured' };
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`https://api.themoviedb.org/3/${mediaType}/${tmdbId}?api_key=${k}`, { signal: AbortSignal.timeout(5000) });
|
const res = await fetch(`https://api.themoviedb.org/3/${mediaType}/${tmdbId}?api_key=${k}`, { signal: AbortSignal.timeout(5000) });
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const isValid = !!(data?.id && (data?.title || data?.name));
|
const isValid = !!(data?.id && (data?.title || data?.name));
|
||||||
const result = { valid: isValid, error: isValid ? null : 'Invalid TMDB ID' };
|
const result = { valid: isValid, error: isValid ? null : 'Invalid TMDB ID' };
|
||||||
cacheSet(tmdbValidationCache, key, result, 300000);
|
cacheSet(tmdbValidationCache, key, result, 300000);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
const result = { valid: false, error: `TMDB API error: ${res.status}` };
|
const result = { valid: false, error: `TMDB API error: ${res.status}` };
|
||||||
cacheSet(tmdbValidationCache, key, result, 60000);
|
cacheSet(tmdbValidationCache, key, result, 60000);
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const result = { valid: false, error: `Failed to validate TMDB ID: ${error.message}` };
|
const result = { valid: false, error: `Failed to validate TMDB ID: ${error.message}` };
|
||||||
cacheSet(tmdbValidationCache, key, result, 60000);
|
cacheSet(tmdbValidationCache, key, result, 60000);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTmdbInfo(tmdbApiKey, tmdbId, mediaType, season) {
|
export async function getTmdbInfo(tmdbApiKey, tmdbId, mediaType, season) {
|
||||||
const key = `${tmdbId}-${mediaType}-${season || ''}`;
|
const key = `${tmdbId}-${mediaType}-${season || ''}`;
|
||||||
const cached = cacheGet(tmdbInfoCache, key);
|
const cached = cacheGet(tmdbInfoCache, key);
|
||||||
if (cached !== undefined) return cached;
|
if (cached !== undefined) return cached;
|
||||||
const k = tmdbApiKey;
|
const k = tmdbApiKey;
|
||||||
if (!k) return { isAnime: false, titles: [], year: null, imdbId: null };
|
if (!k) return { isAnime: false, titles: [], year: null, imdbId: null };
|
||||||
try {
|
try {
|
||||||
const [mainRes, seasonRes] = await Promise.all([
|
const [mainRes, seasonRes] = await Promise.all([
|
||||||
fetch(`https://api.themoviedb.org/3/${mediaType}/${tmdbId}?api_key=${k}&append_to_response=external_ids`, { signal: AbortSignal.timeout(5000) }),
|
fetch(`https://api.themoviedb.org/3/${mediaType}/${tmdbId}?api_key=${k}&append_to_response=external_ids`, { signal: AbortSignal.timeout(5000) }),
|
||||||
season && mediaType === 'tv' ? fetch(`https://api.themoviedb.org/3/tv/${tmdbId}/season/${season}?api_key=${k}`, { signal: AbortSignal.timeout(5000) }) : Promise.resolve(null),
|
season && mediaType === 'tv' ? fetch(`https://api.themoviedb.org/3/tv/${tmdbId}/season/${season}?api_key=${k}`, { signal: AbortSignal.timeout(5000) }) : Promise.resolve(null),
|
||||||
]);
|
]);
|
||||||
let mainData = null, seasonData = null;
|
let mainData = null, seasonData = null;
|
||||||
if (mainRes.ok) mainData = await mainRes.json(); else mainRes.body?.cancel();
|
if (mainRes.ok) mainData = await mainRes.json(); else mainRes.body?.cancel();
|
||||||
if (seasonRes) { if (seasonRes.ok) seasonData = await seasonRes.json(); else seasonRes.body?.cancel(); }
|
if (seasonRes) { if (seasonRes.ok) seasonData = await seasonRes.json(); else seasonRes.body?.cancel(); }
|
||||||
const genres = mainData?.genres || [];
|
const genres = mainData?.genres || [];
|
||||||
const isAnime = genres.some(g => g.id === 16) && ((mainData?.origin_country || []).includes('JP') || mainData?.original_language === 'ja');
|
const isAnime = genres.some(g => g.id === 16) && ((mainData?.origin_country || []).includes('JP') || mainData?.original_language === 'ja');
|
||||||
const titles = [];
|
const titles = [];
|
||||||
if (seasonData?.name) titles.push(seasonData.name);
|
if (seasonData?.name) titles.push(seasonData.name);
|
||||||
const t = mainData?.title || mainData?.name || '';
|
const t = mainData?.title || mainData?.name || '';
|
||||||
const ot = mainData?.original_title || mainData?.original_name || '';
|
const ot = mainData?.original_title || mainData?.original_name || '';
|
||||||
if (t) titles.push(t);
|
if (t) titles.push(t);
|
||||||
if (ot && ot !== t) titles.push(ot);
|
if (ot && ot !== t) titles.push(ot);
|
||||||
const dateStr = seasonData?.air_date || mainData?.release_date || mainData?.first_air_date || '';
|
const dateStr = seasonData?.air_date || mainData?.release_date || mainData?.first_air_date || '';
|
||||||
const result = { isAnime, titles: [...new Set(titles.filter(Boolean))], year: dateStr ? parseInt(dateStr.slice(0, 4), 10) : null, imdbId: mainData?.imdb_id || mainData?.external_ids?.imdb_id || null };
|
const result = { isAnime, titles: [...new Set(titles.filter(Boolean))], year: dateStr ? parseInt(dateStr.slice(0, 4), 10) : null, imdbId: mainData?.imdb_id || mainData?.external_ids?.imdb_id || null };
|
||||||
cacheSet(tmdbInfoCache, key, result, 600000);
|
cacheSet(tmdbInfoCache, key, result, 600000);
|
||||||
return result;
|
return result;
|
||||||
} catch { return { isAnime: false, titles: [], year: null, imdbId: null }; }
|
} catch { return { isAnime: false, titles: [], year: null, imdbId: null }; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function tmdbToAnilist(tmdbId, mediaType, season, titles = [], year = null) {
|
export async function tmdbToAnilist(tmdbId, mediaType, season, titles = [], year = null) {
|
||||||
const key = `${tmdbId}-${mediaType}-${season || ''}`;
|
const key = `${tmdbId}-${mediaType}-${season || ''}`;
|
||||||
const cached = cacheGet(anilistCache, key);
|
const cached = cacheGet(anilistCache, key);
|
||||||
if (cached !== undefined) return cached;
|
if (cached !== undefined) return cached;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`https://api.ani.zip/mappings?tmdb_id=${tmdbId}&type=${mediaType}&season=${season || 1}`, { signal: AbortSignal.timeout(6000) });
|
const res = await fetch(`https://api.ani.zip/mappings?tmdb_id=${tmdbId}&type=${mediaType}&season=${season || 1}`, { signal: AbortSignal.timeout(6000) });
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const id = (await res.json())?.mappings?.[0]?.anilist_id;
|
const id = (await res.json())?.mappings?.[0]?.anilist_id;
|
||||||
if (id) { cacheSet(anilistCache, key, id, 600000); return id; }
|
if (id) { cacheSet(anilistCache, key, id, 600000); return id; }
|
||||||
} else res.body?.cancel();
|
} else res.body?.cancel();
|
||||||
} catch { }
|
} catch { }
|
||||||
if (!titles.length) return null;
|
if (!titles.length) return null;
|
||||||
const query = `query ($s: String) { Page(page:1,perPage:10) { media(search:$s,type:ANIME) { id title { romaji english native } startDate { year } format } } }`;
|
const query = `query ($s: String) { Page(page:1,perPage:10) { media(search:$s,type:ANIME) { id title { romaji english native } startDate { year } format } } }`;
|
||||||
let bestId = null, bestScore = -1;
|
let bestId = null, bestScore = -1;
|
||||||
for (const searchTitle of titles) {
|
for (const searchTitle of titles) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('https://graphql.anilist.co', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ query, variables: { s: searchTitle } }), signal: AbortSignal.timeout(6000) });
|
const res = await fetch('https://graphql.anilist.co', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ query, variables: { s: searchTitle } }), signal: AbortSignal.timeout(6000) });
|
||||||
if (!res.ok) { res.body?.cancel(); continue; }
|
if (!res.ok) { res.body?.cancel(); continue; }
|
||||||
for (const entry of (await res.json())?.data?.Page?.media || []) {
|
for (const entry of (await res.json())?.data?.Page?.media || []) {
|
||||||
const entryTitles = [entry.title?.romaji, entry.title?.english, entry.title?.native].filter(Boolean).map(t => t.toLowerCase());
|
const entryTitles = [entry.title?.romaji, entry.title?.english, entry.title?.native].filter(Boolean).map(t => t.toLowerCase());
|
||||||
const sl = searchTitle.toLowerCase();
|
const sl = searchTitle.toLowerCase();
|
||||||
let score = 0;
|
let score = 0;
|
||||||
if (entryTitles.some(t => t === sl)) score += 5;
|
if (entryTitles.some(t => t === sl)) score += 5;
|
||||||
else if (entryTitles.some(t => t.includes(sl) || sl.includes(t))) score += 3;
|
else if (entryTitles.some(t => t.includes(sl) || sl.includes(t))) score += 3;
|
||||||
else if (entryTitles.some(t => { const w = sl.split(/\s+/).filter(x => x.length > 3); return w.length > 0 && w.every(x => t.includes(x)); })) score += 2;
|
else if (entryTitles.some(t => { const w = sl.split(/\s+/).filter(x => x.length > 3); return w.length > 0 && w.every(x => t.includes(x)); })) score += 2;
|
||||||
else continue;
|
else continue;
|
||||||
if (year && entry.startDate?.year) {
|
if (year && entry.startDate?.year) {
|
||||||
const diff = Math.abs(entry.startDate.year - year);
|
const diff = Math.abs(entry.startDate.year - year);
|
||||||
if (diff === 0) score += 3; else if (diff === 1) score += 1; else if (diff > 2) score -= 3;
|
if (diff === 0) score += 3; else if (diff === 1) score += 1; else if (diff > 2) score -= 3;
|
||||||
}
|
}
|
||||||
if (mediaType === 'tv' && ['TV', 'TV_SHORT', 'ONA', 'OVA'].includes(entry.format)) score += 1;
|
if (mediaType === 'tv' && ['TV', 'TV_SHORT', 'ONA', 'OVA'].includes(entry.format)) score += 1;
|
||||||
if (mediaType === 'movie' && entry.format === 'MOVIE') score += 1;
|
if (mediaType === 'movie' && entry.format === 'MOVIE') score += 1;
|
||||||
if (score > bestScore) { bestScore = score; bestId = entry.id; }
|
if (score > bestScore) { bestScore = score; bestId = entry.id; }
|
||||||
}
|
}
|
||||||
if (bestScore >= 8) break;
|
if (bestScore >= 8) break;
|
||||||
} catch { }
|
} catch { }
|
||||||
}
|
}
|
||||||
if (bestId) cacheSet(anilistCache, key, bestId, 600000);
|
if (bestId) cacheSet(anilistCache, key, bestId, 600000);
|
||||||
return bestId;
|
return bestId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function imdbToTmdb(tmdbApiKey, imdbId, mediaType = 'movie') {
|
export async function imdbToTmdb(tmdbApiKey, imdbId, mediaType = 'movie') {
|
||||||
const key = `imdb-${imdbId}`;
|
const key = `imdb-${imdbId}`;
|
||||||
const cached = cacheGet(tmdbInfoCache, key);
|
const cached = cacheGet(tmdbInfoCache, key);
|
||||||
if (cached !== undefined) return cached;
|
if (cached !== undefined) return cached;
|
||||||
const k = tmdbApiKey;
|
const k = tmdbApiKey;
|
||||||
if (!k) return null;
|
if (!k) return null;
|
||||||
try {
|
try {
|
||||||
const data = await fetchJson(`https://api.themoviedb.org/3/find/${imdbId}?api_key=${k}&external_source=imdb_id`, { signal: AbortSignal.timeout(5000) });
|
const data = await fetchJson(`https://api.themoviedb.org/3/find/${imdbId}?api_key=${k}&external_source=imdb_id`, { signal: AbortSignal.timeout(5000) });
|
||||||
let result = null;
|
let result = null;
|
||||||
if (mediaType === 'movie' && data.movie_results?.length) result = data.movie_results[0].id;
|
if (mediaType === 'movie' && data.movie_results?.length) result = data.movie_results[0].id;
|
||||||
else if (mediaType === 'tv' && data.tv_results?.length) result = data.tv_results[0].id;
|
else if (mediaType === 'tv' && data.tv_results?.length) result = data.tv_results[0].id;
|
||||||
if (result) cacheSet(tmdbInfoCache, key, result, 600000);
|
if (result) cacheSet(tmdbInfoCache, key, result, 600000);
|
||||||
return result;
|
return result;
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
106
test/index.js
106
test/index.js
@@ -1,54 +1,54 @@
|
|||||||
import { HEALTH_PROBE_ID } from "../src/config.js";
|
import { HEALTH_PROBE_ID } from "../src/config.js";
|
||||||
import VylaSDK from "../src/sdk.js";
|
import VylaSDK from "../src/sdk.js";
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
const BACKROOMS = '1083381';
|
const BACKROOMS = '1083381';
|
||||||
|
|
||||||
const sdk = new VylaSDK({
|
const sdk = new VylaSDK({
|
||||||
tmdbApiKey: process.env.TMDB_API_KEY
|
tmdbApiKey: process.env.TMDB_API_KEY
|
||||||
});
|
});
|
||||||
|
|
||||||
// 'true' is excludeDisabled
|
// 'true' is excludeDisabled
|
||||||
// const sources = sdk.getSources(true);
|
// const sources = sdk.getSources(true);
|
||||||
// const source = sources.find(s => s.key === 'lookmovie');
|
// const source = sources.find(s => s.key === 'lookmovie');
|
||||||
|
|
||||||
// const result = await sdk.getStream(source.key, BACKROOMS);
|
// const result = await sdk.getStream(source.key, BACKROOMS);
|
||||||
// console.log("Result:", result.allUrls[0].url);
|
// console.log("Result:", result.allUrls[0].url);
|
||||||
|
|
||||||
// const tests = {
|
// const tests = {
|
||||||
// probeAll(log) {
|
// probeAll(log) {
|
||||||
// const sources = sdk.getSources(true);
|
// const sources = sdk.getSources(true);
|
||||||
// let index = 0;
|
// let index = 0;
|
||||||
|
|
||||||
// Promise.all(sources.map(async (source, idx) => {
|
// Promise.all(sources.map(async (source, idx) => {
|
||||||
// try {
|
// try {
|
||||||
// const result = await sdk.probeSource(source.key);
|
// const result = await sdk.probeSource(source.key);
|
||||||
// index++;
|
// index++;
|
||||||
// log(`(${index}/${sources.length}) source #${idx + 1} (${source.label}):`, result);
|
// log(`(${index}/${sources.length}) source #${idx + 1} (${source.label}):`, result);
|
||||||
// } catch (err) {
|
// } catch (err) {
|
||||||
// log(`Error probing source ${source.key}:`, err);
|
// log(`Error probing source ${source.key}:`, err);
|
||||||
// }
|
// }
|
||||||
// })).then(() => {
|
// })).then(() => {
|
||||||
// log("All sources probed.");
|
// log("All sources probed.");
|
||||||
// });
|
// });
|
||||||
// },
|
// },
|
||||||
// tryHealthProbe(log) {
|
// tryHealthProbe(log) {
|
||||||
// const sources = sdk.getSources(true);
|
// const sources = sdk.getSources(true);
|
||||||
// let index = 0;
|
// let index = 0;
|
||||||
|
|
||||||
// Promise.all(sources.map(async (source, idx) => {
|
// Promise.all(sources.map(async (source, idx) => {
|
||||||
// try {
|
// try {
|
||||||
// const result = await sdk.getStream(source.key, HEALTH_PROBE_ID, null, null, null);
|
// const result = await sdk.getStream(source.key, HEALTH_PROBE_ID, null, null, null);
|
||||||
// index++;
|
// index++;
|
||||||
// log(`(${index}/${sources.length}) source #${idx + 1} (${source.label}):`, result?.allUrls?.[0]?.url || result?.url || "no sources :(");
|
// log(`(${index}/${sources.length}) source #${idx + 1} (${source.label}):`, result?.allUrls?.[0]?.url || result?.url || "no sources :(");
|
||||||
// } catch (err) {
|
// } catch (err) {
|
||||||
// log(`Error probing source ${source.key}:`, err);
|
// log(`Error probing source ${source.key}:`, err);
|
||||||
// }
|
// }
|
||||||
// })).then(() => {
|
// })).then(() => {
|
||||||
// log("All sources probed.");
|
// log("All sources probed.");
|
||||||
// });
|
// });
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// tests.tryHealthProbe(console.log);
|
// tests.tryHealthProbe(console.log);
|
||||||
Reference in New Issue
Block a user