From 3071aeb29f43ca5d749be6fb14b769ba587bcb19 Mon Sep 17 00:00:00 2001 From: tapframe Date: Wed, 27 Aug 2025 21:55:06 +0530 Subject: [PATCH] Add nuvio-providers as submodule --- .gitmodules | 3 + local-scrapers-repo | 1 + local-scrapers-repo/LICENSE | 674 ---------- local-scrapers-repo/README.md | 1136 ----------------- local-scrapers-repo/manifest.json | 150 --- local-scrapers-repo/providers/4khdhub.js | 774 ----------- local-scrapers-repo/providers/dahmermovies.js | 329 ----- local-scrapers-repo/providers/hdrezka.js | 437 ------- local-scrapers-repo/providers/moviesmod.js | 857 ------------- .../providers/myflixer-extractor.js | 471 ------- local-scrapers-repo/providers/netmirror.js | 740 ----------- local-scrapers-repo/providers/showbox.js | 506 -------- local-scrapers-repo/providers/uhdmovies.js | 1081 ---------------- local-scrapers-repo/providers/watch32.js | 547 -------- local-scrapers-repo/providers/xprime.js | 800 ------------ 15 files changed, 4 insertions(+), 8502 deletions(-) create mode 100644 .gitmodules create mode 160000 local-scrapers-repo delete mode 100644 local-scrapers-repo/LICENSE delete mode 100644 local-scrapers-repo/README.md delete mode 100644 local-scrapers-repo/manifest.json delete mode 100644 local-scrapers-repo/providers/4khdhub.js delete mode 100644 local-scrapers-repo/providers/dahmermovies.js delete mode 100644 local-scrapers-repo/providers/hdrezka.js delete mode 100644 local-scrapers-repo/providers/moviesmod.js delete mode 100644 local-scrapers-repo/providers/myflixer-extractor.js delete mode 100644 local-scrapers-repo/providers/netmirror.js delete mode 100644 local-scrapers-repo/providers/showbox.js delete mode 100644 local-scrapers-repo/providers/uhdmovies.js delete mode 100644 local-scrapers-repo/providers/watch32.js delete mode 100644 local-scrapers-repo/providers/xprime.js diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..2b46623 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "local-scrapers-repo"] + path = local-scrapers-repo + url = https://github.com/tapframe/nuvio-providers.git diff --git a/local-scrapers-repo b/local-scrapers-repo new file mode 160000 index 0000000..e48fd1a --- /dev/null +++ b/local-scrapers-repo @@ -0,0 +1 @@ +Subproject commit e48fd1a255b90cac10cd60fdddab8410783434a1 diff --git a/local-scrapers-repo/LICENSE b/local-scrapers-repo/LICENSE deleted file mode 100644 index f288702..0000000 --- a/local-scrapers-repo/LICENSE +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/local-scrapers-repo/README.md b/local-scrapers-repo/README.md deleted file mode 100644 index ff3d242..0000000 --- a/local-scrapers-repo/README.md +++ /dev/null @@ -1,1136 +0,0 @@ -# Nuvio Local Scrapers Repository - -This repository contains local scrapers for the Nuvio streaming application. These scrapers allow you to fetch streams from various sources directly within the app. - -## Repository Structure - -``` -├── manifest.json # Repository manifest with scraper definitions -├── uhdmovies.js # UHD Movies scraper -├── moviesmod.js # MoviesMod scraper -├── hdrezka.js # HDRezka scraper -├── dahmermovies.js # Dahmer Movies scraper -├── myflixer.js # MyFlixer scraper -├── test_uhdmovies.js # Test file for UHD Movies scraper -├── test_moviesmod.js # Test file for MoviesMod scraper -├── test_hdrezka.js # Test file for HDRezka scraper -├── test_dahmermovies.js # Test file for Dahmer Movies scraper -├── test_myflixer.js # Test file for MyFlixer scraper -└── README.md # This file -``` - -## How to Use - -1. **Add Repository to Nuvio:** - - Open Nuvio app - - Go to Settings → Local Scrapers - - Add this repository URL - - Enable the scrapers you want to use - -2. **GitHub Repository URL:** - ``` - https://raw.githubusercontent.com/tapframe/nuvio-local-scrapers/main/ - ``` - -## Available Scrapers - -### UHD Movies -- **Source:** UHD Movies website -- **Content:** High-quality movies and TV shows -- **Formats:** Various qualities (480p to 4K) -- **Features:** Episode-specific extraction, multiple download servers -- **Status:** Active - -### MoviesMod -- **Source:** MoviesMod website -- **Content:** Movies and TV shows with multiple quality options -- **Formats:** 720p, 1080p, 4K with various encodings (x264, x265/HEVC, 10-bit) -- **Features:** Dynamic domain fetching, SID link resolution, multiple tech domains -- **Status:** Active - -### HDRezka -- **Source:** HDRezka website -- **Content:** High-quality movies and TV shows with subtitle support -- **Formats:** 360p, 480p, 720p, 1080p Ultra (premium content requires login) -- **Features:** Multi-language subtitles (Russian, Ukrainian, English), episode-specific streaming -- **Status:** Active - -### Dahmer Movies -- **Source:** Dahmer Movies website (a.111477.xyz) -- **Content:** High-quality movies and TV shows -- **Formats:** 1080p, 2160p (4K) with various encodings -- **Features:** Direct file access, episode-specific extraction, quality filtering -- **Status:** Active - -### MyFlixer -- **Source:** MyFlixer website (watch32.sx) -- **Content:** High-quality movies and TV shows with M3U8 streaming -- **Formats:** 360p, 720p, 1080p with adaptive bitrate streaming -- **Features:** M3U8 playlist extraction, Videostr decryption, multiple quality options, episode-specific streaming -- **Status:** Active - -## Scraper Development - -### Prerequisites - -- Basic knowledge of JavaScript and web scraping -- Understanding of HTML/CSS selectors -- Familiarity with HTTP requests and responses -- Knowledge of React Native compatibility requirements - -### Creating a New Scraper - -#### 1. Create the scraper file (e.g., `newscraper.js`): - -```javascript -// Import cheerio for HTML parsing (React Native compatible) -const cheerio = require('cheerio-without-node-native'); - -// Constants -const TMDB_API_KEY = "your_tmdb_api_key_here"; -const BASE_URL = 'https://example-site.com'; - -// Helper function for HTTP requests -async function makeRequest(url, options = {}) { - const defaultHeaders = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', - 'Accept-Language': 'en-US,en;q=0.5', - 'Connection': 'keep-alive' - }; - - const response = await fetch(url, { - ...options, - headers: { - ...defaultHeaders, - ...options.headers - } - }); - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - - return response; -} - -// Main function that Nuvio will call -async function getStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) { - console.log(`[YourScraper] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}`); - - try { - // 1. Get TMDB info - const tmdbUrl = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY}`; - const tmdbResponse = await makeRequest(tmdbUrl); - const tmdbData = await tmdbResponse.json(); - - const title = mediaType === 'tv' ? tmdbData.name : tmdbData.title; - const year = mediaType === 'tv' ? tmdbData.first_air_date?.substring(0, 4) : tmdbData.release_date?.substring(0, 4); - - if (!title) { - throw new Error('Could not extract title from TMDB response'); - } - - console.log(`[YourScraper] TMDB Info: "${title}" (${year})`); - - // 2. Search for content - const searchResults = await searchContent(title, year, mediaType); - if (searchResults.length === 0) { - console.log(`[YourScraper] No search results found`); - return []; - } - - // 3. Extract download links - const selectedResult = findBestMatch(title, searchResults); - const downloadLinks = await extractDownloadLinks(selectedResult.url); - - // 4. Process links to get final streams - const streamPromises = downloadLinks.map(link => processDownloadLink(link, mediaType, episodeNum)); - const streams = (await Promise.all(streamPromises)).filter(Boolean); - - // 5. Sort by quality (highest first) - streams.sort((a, b) => { - const qualityA = parseQualityForSort(a.quality); - const qualityB = parseQualityForSort(b.quality); - return qualityB - qualityA; - }); - - console.log(`[YourScraper] Successfully processed ${streams.length} streams`); - return streams; - - } catch (error) { - console.error(`[YourScraper] Error in getStreams: ${error.message}`); - return []; - } -} - -// Helper functions -async function searchContent(title, year, mediaType) { - // Implement search logic - const searchUrl = `${BASE_URL}/search?q=${encodeURIComponent(title)}`; - const response = await makeRequest(searchUrl); - const html = await response.text(); - const $ = cheerio.load(html); - - const results = []; - $('.search-result').each((i, element) => { - const linkElement = $(element).find('a'); - const resultTitle = linkElement.text().trim(); - const url = linkElement.attr('href'); - if (resultTitle && url) { - results.push({ title: resultTitle, url }); - } - }); - - return results; -} - -async function extractDownloadLinks(pageUrl) { - // Implement link extraction logic - const response = await makeRequest(pageUrl); - const html = await response.text(); - const $ = cheerio.load(html); - - const links = []; - $('.download-link').each((i, element) => { - const quality = $(element).find('.quality').text().trim(); - const url = $(element).attr('href'); - if (quality && url) { - links.push({ quality, url }); - } - }); - - return links; -} - -async function processDownloadLink(link, mediaType, episodeNum) { - try { - // Process individual download link - // This might involve resolving intermediate URLs, handling captchas, etc. - const finalUrl = await resolveFinalUrl(link.url); - - if (!finalUrl) return null; - - return { - name: "YourScraper", - title: `${link.quality} Stream`, - url: finalUrl, - quality: extractQuality(link.quality), - size: extractSize(link.quality), - type: 'direct', - headers: { // Include headers if your stream requires them - "Referer": "https://your-source-site.com", - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" - } - }; - } catch (error) { - console.error(`[YourScraper] Error processing link: ${error.message}`); - return null; - } -} - -// Utility functions -function findBestMatch(title, results) { - // Implement string similarity matching - return results[0]; // Simple fallback -} - -function parseQualityForSort(qualityString) { - const match = qualityString.match(/(\d{3,4})p/i); - return match ? parseInt(match[1], 10) : 0; -} - -function extractQuality(text) { - const match = text.match(/(480p|720p|1080p|2160p|4k)/i); - return match ? match[1] : 'Unknown'; -} - -function extractSize(text) { - const match = text.match(/\[([^\]]+)\]/); - return match ? match[1] : null; -} - -async function resolveFinalUrl(url) { - // Implement URL resolution logic - return url; // Simple fallback -} - -// Export for React Native -if (typeof module !== 'undefined' && module.exports) { - module.exports = { getStreams }; -} else { - global.getStreams = getStreams; -} -``` - -#### 2. Update manifest.json: - -```json -{ - "version": "1.0.0", - "scrapers": [ - { - "id": "newscraper", - "name": "New Scraper", - "description": "Description of your scraper functionality", - "version": "1.0.0", - "author": "Your Name", - "types": ["movie", "tv"], - "file": "newscraper.js", - "enabled": true - } - ] -} -``` - -#### 3. Create a test file (e.g., `test_newscraper.js`): - -```javascript -const { getStreams } = require('./newscraper.js'); - -async function testScraper() { - console.log('=== New Scraper Test ===\n'); - - const testCases = [ - { name: 'Popular Movie', tmdbId: '550', type: 'movie' }, - { name: 'TV Show Episode', tmdbId: '1396', type: 'tv', season: 1, episode: 1 } - ]; - - for (const testCase of testCases) { - console.log(`--- Testing: ${testCase.name} ---`); - console.log(`TMDB ID: ${testCase.tmdbId}, Type: ${testCase.type}`); - - const startTime = Date.now(); - const streams = await getStreams(testCase.tmdbId, testCase.type, testCase.season, testCase.episode); - const endTime = Date.now(); - - console.log(`Test completed in ${((endTime - startTime) / 1000).toFixed(3)}s`); - console.log(`Found ${streams.length} streams:\n`); - - streams.forEach((stream, index) => { - console.log(`${index + 1}. ${stream.name}`); - console.log(` Title: ${stream.title}`); - console.log(` Quality: ${stream.quality}`); - console.log(` Size: ${stream.size || 'Unknown'}`); - console.log(` Type: ${stream.type}`); - console.log(` URL: ${stream.url.substring(0, 80)}...`); - console.log(''); - }); - - console.log('==================================================\n'); - } -} - -testScraper().catch(console.error); -``` - -### Scraper Function Parameters - -- `tmdbId` (string): The Movie Database ID -- `mediaType` (string): Either "movie" or "tv" -- `seasonNum` (number|null): Season number (for TV shows, null for movies) -- `episodeNum` (number|null): Episode number (for TV shows, null for movies) - -### Stream Object Format - -```javascript -{ - name: "Provider name (e.g., 'UHDMovies', 'MoviesMod')", - title: "Descriptive title with quality and technical details", - url: "Direct stream URL or magnet link", - quality: "Video quality (e.g., '1080p', '720p', '4K')", - size: "File size (e.g., '2.5GB', '1.18GB')", - fileName: "Original filename (optional)", - type: "direct", // or "torrent" for magnet links - headers: { // Optional: Custom headers for video player requests - "Referer": "https://example.com", - "User-Agent": "Custom User Agent", - "Origin": "https://example.com" - } -} -``` - -#### Headers Support - -If your stream requires specific headers for playback (e.g., Referer, User-Agent, Authorization), you can include them in the `headers` object. These headers will be automatically passed to the video player when streaming the content. - -**Common use cases:** -- **Referer**: Required by some CDNs to verify the request origin -- **User-Agent**: Some servers block requests without proper user agents -- **Authorization**: For streams requiring authentication tokens -- **Origin**: CORS-related headers for cross-origin requests - -**Example with headers:** -```javascript -{ - name: "XPrime", - title: "Movie Title 2024 1080p", - url: "https://cdn.example.com/stream.m3u8", - quality: "1080p", - type: "direct", - headers: { - "Referer": "https://xprime.tv", - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" - } -} -``` - -**Note:** Headers are optional. If not provided, the video player will use default headers. - -### Advanced Stream Object Example - -```javascript -{ - name: "MoviesMod", - title: "Movie Title 2024 1080p WEB-DL English MSubs\n2.75GB • HEVC • 10-bit", - url: "https://example.com/download/movie.mp4", - quality: "1080p", - size: "2.75GB", - fileName: "Movie.Title.2024.1080p.WEB-DL.x265.mkv", - type: "direct", - headers: { - "Referer": "https://moviesmod.com", - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" - } -} -``` - -### Best Practices - -#### 1. Error Handling -```javascript -try { - const result = await riskyOperation(); - return result; -} catch (error) { - console.error(`[YourScraper] Error in operation: ${error.message}`); - return []; -} -``` - -#### 2. Logging -```javascript -console.log(`[YourScraper] Starting search for: ${title}`); -console.log(`[YourScraper] Found ${results.length} results`); -console.error(`[YourScraper] Failed to process link: ${error.message}`); -``` - -#### 3. Rate Limiting -```javascript -// Add delays between requests -await new Promise(resolve => setTimeout(resolve, 1000)); - -// Use request queues for multiple operations -const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms)); -``` - -#### 4. User Agents and Headers -```javascript -const defaultHeaders = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', - 'Accept-Language': 'en-US,en;q=0.5', - 'Accept-Encoding': 'gzip, deflate', - 'Connection': 'keep-alive', - 'Upgrade-Insecure-Requests': '1' -}; -``` - -### Implementing Headers for Video Playback - -Many streaming sources require specific headers for video playback. Here's how to implement headers in your scraper: - -#### Basic Header Implementation -```javascript -// Define headers that will be passed to the video player -const WORKING_HEADERS = { - 'Referer': 'https://your-source-site.com', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', - 'Origin': 'https://your-source-site.com' -}; - -// Include headers in your stream object -function createStreamObject(url, quality, title) { - return { - name: "YourScraper", - title: title, - url: url, - quality: quality, - type: 'direct', - headers: WORKING_HEADERS // These headers will be passed to the video player - }; -} -``` - -#### Real-World Example (XPrime Pattern) -```javascript -// Headers for XPrime-style sources -const XPRIME_HEADERS = { - 'Referer': 'https://xprime.tv', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' -}; - -// Apply headers to all streams from this source -function processXPrimeStream(streamData) { - return { - name: "XPrime", - title: `${streamData.title} - ${streamData.quality}`, - url: streamData.url, - quality: streamData.quality, - type: 'direct', - headers: XPRIME_HEADERS // Video player will use these headers - }; -} -``` - -#### Conditional Headers Based on Source -```javascript -function createStreamWithHeaders(url, quality, title, sourceType) { - let headers = {}; - - // Apply different headers based on the source - switch (sourceType) { - case 'xprime': - headers = { - 'Referer': 'https://xprime.tv', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' - }; - break; - case 'moviesmod': - headers = { - 'Referer': 'https://moviesmod.com', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' - }; - break; - case 'videostr': - headers = { - 'Referer': 'https://videostr.net/', - 'Origin': 'https://videostr.net/' - }; - break; - default: - // No special headers needed - headers = undefined; - } - - const streamObject = { - name: "YourScraper", - title: title, - url: url, - quality: quality, - type: 'direct' - }; - - // Only include headers if they're needed - if (headers && Object.keys(headers).length > 0) { - streamObject.headers = headers; - } - - return streamObject; -} -``` - -#### Headers for Different Stream Types -```javascript -// For M3U8 streams that require referer -function createM3U8Stream(m3u8Url, quality, refererUrl) { - return { - name: "YourScraper", - title: `${quality} Stream`, - url: m3u8Url, - quality: quality, - type: 'direct', - headers: { - 'Referer': refererUrl, - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' - } - }; -} - -// For direct MP4 streams with authentication -function createAuthenticatedStream(mp4Url, quality, authToken) { - return { - name: "YourScraper", - title: `${quality} Stream`, - url: mp4Url, - quality: quality, - type: 'direct', - headers: { - 'Authorization': `Bearer ${authToken}`, - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' - } - }; -} -``` - -#### Important Notes About Headers - -1. **Headers are automatically passed to video players**: When you include a `headers` object in your stream, Nuvio will automatically pass these headers to both VLCPlayer and AndroidVideoPlayer. - -2. **Common header types**: - - **Referer**: Most important for CDN validation - - **User-Agent**: Prevents bot detection - - **Origin**: Required for CORS compliance - - **Authorization**: For authenticated streams - -3. **Testing headers**: Always test your streams with the headers to ensure they work: -```javascript -// Test if headers are working -async function testStreamWithHeaders(streamUrl, headers) { - try { - const response = await fetch(streamUrl, { - method: 'HEAD', - headers: headers - }); - return response.ok || response.status === 206; // 206 for partial content - } catch (error) { - console.error('Stream test failed:', error.message); - return false; - } -} -``` - -4. **Header inheritance**: Headers are only applied to video playback, not to scraping requests. Use separate headers for scraping vs. playback. - -5. **Performance**: Headers don't impact performance - they're just additional HTTP headers sent with video requests. - -#### 5. Caching -```javascript -// Global cache for domain/session data -let domainCache = null; -let cacheTimestamp = 0; -const CACHE_TTL = 4 * 60 * 60 * 1000; // 4 hours - -function getCachedDomain() { - const now = Date.now(); - if (domainCache && (now - cacheTimestamp < CACHE_TTL)) { - return domainCache; - } - return null; -} -``` - -#### 6. String Similarity Matching -```javascript -function findBestMatch(target, candidates) { - let bestMatch = null; - let bestScore = 0; - - for (const candidate of candidates) { - const score = calculateSimilarity(target.toLowerCase(), candidate.title.toLowerCase()); - if (score > bestScore) { - bestScore = score; - bestMatch = candidate; - } - } - - return bestMatch; -} - -function calculateSimilarity(str1, str2) { - // Simple word-based similarity - const words1 = str1.split(/\s+/); - const words2 = str2.split(/\s+/); - - let matches = 0; - for (const word of words1) { - if (word.length > 2 && words2.some(w => w.includes(word) || word.includes(w))) { - matches++; - } - } - - return matches / Math.max(words1.length, words2.length); -} -``` - -#### 7. Quality Parsing and Sorting -```javascript -function parseQualityForSort(qualityString) { - if (!qualityString) return 0; - const match = qualityString.match(/(\d{3,4})p/i); - return match ? parseInt(match[1], 10) : 0; -} - -function extractQuality(text) { - if (!text) return 'Unknown'; - const qualityMatch = text.match(/(480p|720p|1080p|2160p|4k)/i); - return qualityMatch ? qualityMatch[1] : 'Unknown'; -} - -function getTechDetails(qualityString) { - if (!qualityString) return []; - const details = []; - const lowerText = qualityString.toLowerCase(); - if (lowerText.includes('10bit')) details.push('10-bit'); - if (lowerText.includes('hevc') || lowerText.includes('x265')) details.push('HEVC'); - if (lowerText.includes('hdr')) details.push('HDR'); - return details; -} -``` - -### React Native Compatibility - -#### Required Dependencies -```javascript -// Use React Native compatible cheerio -const cheerio = require('cheerio-without-node-native'); - -// Avoid Node.js modules -// ❌ Don't use: require('fs'), require('path'), require('crypto') -// ✅ Use: global variables, fetch(), built-in JavaScript functions -``` - -#### HTTP Requests -```javascript -// ✅ Use fetch() - React Native compatible -const response = await fetch(url, { - method: 'GET', - headers: { 'User-Agent': '...' } -}); - -// ❌ Avoid: axios, request, http modules -``` - -#### Async/Await Usage -**IMPORTANT:** Do not use `async/await` syntax in your scrapers for React Native compatibility. - -```javascript -// ❌ Don't use async/await -async function getStreams(title, year) { - const response = await fetch(url); - const data = await response.json(); - return data; -} - -// ✅ Use Promises with .then()/.catch() -function getStreams(title, year) { - return fetch(url) - .then(response => response.json()) - .then(data => { - // Process data - return processedStreams; - }) - .catch(error => { - console.error(`[YourScraper] Error: ${error.message}`); - return []; - }); -} - -// ✅ Or use Promise constructor for complex flows -function getStreams(title, year) { - return new Promise((resolve, reject) => { - fetch(url) - .then(response => response.json()) - .then(data => { - // Process data - const streams = processData(data); - resolve(streams); - }) - .catch(error => { - console.error(`[YourScraper] Error: ${error.message}`); - resolve([]); // Return empty array instead of rejecting - }); - }); -} -``` - -#### Data Storage -```javascript -// ✅ Use global variables for caching -let globalCache = {}; - -// ❌ Avoid: file system operations -// Don't use: fs.writeFileSync(), localStorage (not available) -``` - -#### URL Handling -```javascript -// ✅ Use built-in URL constructor -const urlObject = new URL(link); -const params = urlObject.searchParams.get('param'); - -// ✅ Use URLSearchParams for form data -const formData = new URLSearchParams(); -formData.append('key', 'value'); -``` - -#### Base64 Operations -```javascript -// ✅ Use built-in functions -const decoded = atob(encodedString); // Base64 decode -const encoded = btoa(plainString); // Base64 encode -``` - -### Testing Your Scraper - -#### 1. Unit Testing -```bash -# Run your test file -node test_newscraper.js -``` - -#### 2. Integration Testing -- Test with various TMDB IDs -- Test both movies and TV shows -- Test edge cases (missing content, network errors) -- Verify stream URLs are accessible - -#### 3. Performance Testing -- Monitor response times -- Check memory usage -- Test with multiple concurrent requests - -### Common Patterns - -#### 1. Dynamic Domain Fetching -```javascript -async function getLatestDomain() { - try { - const response = await fetch('https://api.example.com/domains'); - const data = await response.json(); - return data.currentDomain; - } catch (error) { - console.error('Failed to fetch domain, using fallback'); - return 'https://fallback-domain.com'; - } -} -``` - -#### 2. SID Link Resolution -```javascript -async function resolveSidLink(sidUrl) { - // Multi-step process common in many scrapers - const step1Response = await fetch(sidUrl); - const step1Html = await step1Response.text(); - - // Extract form data - const $ = cheerio.load(step1Html); - const formData = new URLSearchParams(); - formData.append('token', $('input[name="token"]').val()); - - // Submit form - const step2Response = await fetch(actionUrl, { - method: 'POST', - body: formData, - headers: { 'Content-Type': 'application/x-www-form-urlencoded' } - }); - - // Extract final URL - const finalHtml = await step2Response.text(); - const finalUrl = extractFinalUrl(finalHtml); - - return finalUrl; -} -``` - -#### 3. Multiple Download Options -```javascript -async function processDownloadLink(link) { - const downloadOptions = await getDownloadOptions(link.url); - - // Try options in order of preference - for (const option of downloadOptions.sort((a, b) => a.priority - b.priority)) { - try { - const finalUrl = await resolveDownloadOption(option); - if (await validateUrl(finalUrl)) { - return createStreamObject(finalUrl, link); - } - } catch (error) { - console.log(`Option ${option.name} failed: ${error.message}`); - } - } - - return null; -} -``` - -## Publishing to GitHub - -1. **Create a new repository on GitHub:** - - Go to github.com - - Click "New repository" - - Name it `nuvio-local-scrapers` - - Make it public - - Don't initialize with README (we already have one) - -2. **Upload files:** - ```bash - cd /path/to/local-scrapers-repo - git init - git add . - git commit -m "Initial commit with UHD Movies scraper" - git branch -M main - git remote add origin https://github.com/tapframe/nuvio-local-scrapers.git - git push -u origin main - ``` - -3. **Get the raw URL:** - ``` - https://raw.githubusercontent.com/tapframe/nuvio-local-scrapers/main/ - ``` - -## Contributing - -### Development Workflow - -1. **Fork this repository** - ```bash - # Clone your fork - git clone https://github.com/tapframe/nuvio-local-scrapers.git - cd nuvio-local-scrapers - ``` - -2. **Create a new branch** - ```bash - git checkout -b add-newscraper - ``` - -3. **Develop your scraper** - - Create `newscraper.js` - - Update `manifest.json` - - Create `test_newscraper.js` - - Test thoroughly - -4. **Test your scraper** - ```bash - # Run tests - node test_newscraper.js - - # Test with different content types - # Verify stream URLs work - # Check error handling - ``` - -5. **Commit and push** - ```bash - git add . - git commit -m "Add NewScraper with support for movies and TV shows" - git push origin add-newscraper - ``` - -6. **Submit a pull request** - - Include description of the scraper - - List supported features - - Provide test results - - Mention any limitations - -### Code Review Checklist - -Before submitting, ensure your scraper: - -- [ ] **Follows naming conventions** (camelCase, descriptive names) -- [ ] **Has proper error handling** (try-catch blocks, graceful failures) -- [ ] **Includes comprehensive logging** (with scraper name prefix) -- [ ] **Is React Native compatible** (no Node.js modules, uses fetch()) -- [ ] **Has a working test file** (tests movies and TV shows) -- [ ] **Updates manifest.json** (correct metadata and version) -- [ ] **Respects rate limits** (reasonable delays between requests) -- [ ] **Handles edge cases** (missing content, network errors) -- [ ] **Returns proper stream objects** (correct format and required fields) -- [ ] **Is well-documented** (comments explaining complex logic) - -### Scraper Quality Standards - -#### Performance -- Response time < 15 seconds for most requests -- Handles concurrent requests gracefully -- Minimal memory usage -- Efficient DOM parsing - -#### Reliability -- Success rate > 80% for popular content -- Graceful degradation when source is unavailable -- Proper timeout handling -- Retry logic for transient failures - -#### User Experience -- Clear, descriptive stream titles -- Accurate quality and size information -- Sorted results (highest quality first) -- Consistent naming conventions - -### Debugging Tips - -#### 1. Network Issues -```javascript -// Add request/response logging -console.log(`[YourScraper] Requesting: ${url}`); -console.log(`[YourScraper] Response status: ${response.status}`); -console.log(`[YourScraper] Response headers:`, response.headers); -``` - -#### 2. HTML Parsing Issues -```javascript -// Log HTML content for inspection -console.log(`[YourScraper] HTML length: ${html.length}`); -console.log(`[YourScraper] Page title: ${$('title').text()}`); -console.log(`[YourScraper] Found ${$('.target-selector').length} elements`); -``` - -#### 3. URL Resolution Issues -```javascript -// Validate URLs before returning -async function validateUrl(url) { - try { - const response = await fetch(url, { method: 'HEAD' }); - return response.ok || response.status === 206; // 206 for partial content - } catch (error) { - return false; - } -} -``` - -### Real-World Examples - -#### UHDMovies Scraper Features -- **Episode-specific extraction** for TV shows -- **Multiple tech domains** (tech.unblockedgames.world, tech.examzculture.in, etc.) -- **SID link resolution** with multi-step form submission -- **Driveleech URL processing** with multiple download methods -- **Quality parsing** with technical details (10-bit, HEVC, HDR) - -#### MoviesMod Scraper Features -- **Dynamic domain fetching** from GitHub repository -- **String similarity matching** for content selection -- **Intermediate link resolution** (modrefer.in decoding) -- **Multiple download servers** (Resume Cloud, Worker Bot, Instant Download) -- **Broken link filtering** (report pages, invalid URLs) -- **Parallel processing** of multiple quality options - -### Advanced Techniques - -#### 1. Multi-Domain Support -```javascript -const TECH_DOMAINS = [ - 'tech.unblockedgames.world', - 'tech.examzculture.in', - 'tech.creativeexpressionsblog.com', - 'tech.examdegree.site' -]; - -function isTechDomain(url) { - return TECH_DOMAINS.some(domain => url.includes(domain)); -} -``` - -#### 2. Form-Based Authentication -```javascript -async function submitVerificationForm(formUrl, formData) { - const response = await fetch(formUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Referer': previousUrl - }, - body: new URLSearchParams(formData).toString() - }); - return response; -} -``` - -#### 3. JavaScript Execution Simulation -```javascript -// Extract dynamic values from JavaScript code -function extractFromJavaScript(html) { - const cookieMatch = html.match(/s_343\('([^']+)',\s*'([^']+)'/); - const linkMatch = html.match(/c\.setAttribute\("href",\s*"([^"]+)"\)/); - - return { - cookieName: cookieMatch?.[1], - cookieValue: cookieMatch?.[2], - linkPath: linkMatch?.[1] - }; -} -``` - -### Maintenance - -#### Updating Existing Scrapers -- Monitor source website changes -- Update selectors and logic as needed -- Test after updates -- Increment version number in manifest - -#### Handling Source Changes -- Implement fallback mechanisms -- Use multiple extraction methods -- Add domain rotation support -- Monitor for breaking changes - -### Troubleshooting - -#### Common Issues - -1. **CORS Errors** - - Use appropriate headers - - Consider proxy solutions - - Check source website restrictions - -2. **Rate Limiting** - - Add delays between requests - - Implement exponential backoff - - Use different user agents - -3. **Captcha/Bot Detection** - - Rotate user agents - - Add realistic delays - - Implement session management - -4. **Dynamic Content** - - Look for API endpoints - - Parse JavaScript for data - - Use multiple extraction methods - -#### Getting Help - -- Check existing scraper implementations -- Review error logs carefully -- Test with different content types -- Ask for help in community discussions - ---- - -## 🧰 Tools & Technologies - -

- - - -

- ---- - - - -## 📄 License - -[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html) - -These scrapers are **free software**: you can use, study, share, and modify them as you wish. - -They are distributed under the terms of the [GNU General Public License](https://www.gnu.org/licenses/gpl.html) version 3 or later, published by the Free Software Foundation. - ---- - -## ⚖️ DMCA Disclaimer - -We hereby issue this notice to clarify that these scrapers function similarly to a standard web browser by fetching video files from the internet. - -- **No content is hosted by this repository or the Nuvio application.** -- Any content accessed is hosted by third-party websites. -- Users are solely responsible for their usage and must comply with their local laws. - -If you believe content is violating copyright laws, please contact the **actual file hosts**, **not** the developers of this repository or the Nuvio app. - ---- - -## Support - -For issues or questions: -- Open an issue on GitHub -- Check the Nuvio app documentation -- Join the community discussions - ---- - -**Thank You for using Nuvio Local Scrapers!** diff --git a/local-scrapers-repo/manifest.json b/local-scrapers-repo/manifest.json deleted file mode 100644 index 8e0c50b..0000000 --- a/local-scrapers-repo/manifest.json +++ /dev/null @@ -1,150 +0,0 @@ -{ - "name": "Tapframe's Repo", - "version": "1.0.0", - "scrapers": [ - { - - - "id": "4khdhub", - "name": "4KHDHub", - "description": "4KHDHub direct links", - "version": "1.0.1", - "author": "Nuvio Team", - "supportedTypes": [ - "movie", - "tv" - ], - "filename": "providers/4khdhub.js", - "enabled": true, - "formats": ["mkv"], - "logo": "https://4khdhub.fans/favicon.ico", - "contentLanguage": [ - "en" - ] - }, - { - "id": "uhdmovies", - "name": "UHDMovies", - "description": "High-quality movie and TV show streams from UHD Movies", - "version": "1.0.1", - "author": "Nuvio Team", - "supportedTypes": [ - "movie", - "tv" - ], - "filename": "providers/uhdmovies.js", - "enabled": true, - "formats": ["mkv"], - "logo": "https://uhdmovies.cat/wp-content/uploads/2021/03/cropped-output-onlinepngtools-1-32x32.png", - "contentLanguage": [ - "en" - ] - }, - { - "id": "moviesmod", - "name": "MoviesMod", - "description": "Movie and TV show streams from MoviesMod with multiple quality options", - "version": "1.0.1", - "author": "Nuvio Team", - "supportedTypes": [ - "movie", - "tv" - ], - "filename": "providers/moviesmod.js", - "enabled": true, - "formats": ["mkv"], - "logo": "https://moviesmod.tube/wp-content/uploads/2022/10/moviesmod.png", - "contentLanguage": [ - "en" - ] - }, - { - "id": "hdrezka", - "name": "HDRezka", - "description": "High-quality movie and TV show streams", - "version": "1.0.0", - "author": "Nuvio Team", - "supportedTypes": [ - "movie", - "tv" - ], - "filename": "providers/hdrezka.js", - "enabled": true, - "logo": "https://hdrezka.ag/templates/hdrezka/images/favicon.ico", - "contentLanguage": [ - "ru", - "en" - ] - }, - { - "id": "dahmermovies", - "name": "DahmerMovies", - "description": "High-quality movie and TV show streams from Dahmer Movies, Can be slow at times to fetch and stream. Only use if needed.", - "version": "1.0.0", - "author": "Nuvio Team", - "supportedTypes": [ - "movie", - "tv" - ], - "filename": "providers/dahmermovies.js", - "enabled": true, - "formats": ["mkv"], - "logo": "https://image.similarpng.com/file/similarpng/very-thumbnail/2021/05/Letter-D-logo-design-template-with-geometric-shape-style-on-transparent-background-PNG.png", - "contentLanguage": [ - "en" - ] - }, - { - "id": "watch32", - "name": "Watch32", - "description": "Lightweight M3U8 Links", - "version": "1.0.1", - "author": "Nuvio Team", - "supportedTypes": [ - "movie", - "tv" - ], - "filename": "providers/watch32.js", - "enabled": true, - "logo": "https://img.watch32.sx/xxrz/400x400/100/a9/5e/a95e15a880a9df3c045f6a5224daf576/a95e15a880a9df3c045f6a5224daf576.png", - "contentLanguage": [ - "en" - ] - }, - { - "id": "xprime", - "name": "Xprime", - "description": "Faster Streaming links from Xprime.tv", - "version": "1.0.0", - "author": "Nuvio Team", - "supportedTypes": [ - "movie", - "tv" - ], - "filename": "providers/xprime.js", - "enabled": true, - "logo": "https://xprime.tv/favicon/favicon.ico", - "contentLanguage": [ - "en" - ] - }, - { - "id": "netmirror", - "name": "NetMirror", - "description": "Streaming links from netmirror. Automatically disabled on iOS due to compatibility issues.", - "version": "1.0.1", - "author": "Nuvio Team", - "supportedTypes": [ - "movie", - "tv" - ], - "filename": "providers/netmirror.js", - "enabled": true, - "logo": "https://net2025.cc/favicon.ico", - "contentLanguage": [ - "en" - ], - "disabledPlatforms": ["ios"] - } - ] -} \ No newline at end of file diff --git a/local-scrapers-repo/providers/4khdhub.js b/local-scrapers-repo/providers/4khdhub.js deleted file mode 100644 index cbdb4bf..0000000 --- a/local-scrapers-repo/providers/4khdhub.js +++ /dev/null @@ -1,774 +0,0 @@ -// 4KHDHub Scraper for Nuvio Local Scrapers -// React Native compatible – no Node core modules, no async/await - -const cheerio = require('cheerio-without-node-native'); -console.log('[4KHDHub] Using cheerio-without-node-native for DOM parsing'); - -// Constants -const TMDB_API_KEY = '439c478a771f35c05022f9feabcca01c'; -const DOMAINS_URL = 'https://raw.githubusercontent.com/phisher98/TVVVV/refs/heads/main/domains.json'; - -// Caches (in-memory only) -let domainsCache = null; -let resolvedUrlsCache = {}; // key -> array of resolved file-host URLs - -// Headers -const DEFAULT_HEADERS = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36', - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', - 'Accept-Language': 'en-US,en;q=0.5', - 'Connection': 'keep-alive' -}; - -// Polyfill atob/btoa for Node test environments if missing (kept lightweight, no imports) -if (typeof atob === 'undefined') { - try { - // eslint-disable-next-line no-undef - global.atob = function (b64) { return Buffer.from(b64, 'base64').toString('binary'); }; - } catch (e) { - // ignore for RN - } -} -if (typeof btoa === 'undefined') { - try { - // eslint-disable-next-line no-undef - global.btoa = function (str) { return Buffer.from(str, 'binary').toString('base64'); }; - } catch (e) { - // ignore for RN - } -} - -// Helper: HTTP -function makeRequest(url, options = {}) { - return fetch(url, { - ...options, - headers: { - ...DEFAULT_HEADERS, - ...(options.headers || {}) - } - }).then(function (response) { - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - return response; - }); -} - -// Base64 and misc helpers (RN-safe) -function base64Decode(str) { - try { - // Convert base64 -> binary string -> UTF-8 - // escape/unescape is deprecated but works in RN environments for this use case - // eslint-disable-next-line no-undef - return decodeURIComponent(escape(atob(str))); - } catch (e) { - return ''; - } -} - -function base64Encode(str) { - try { - // eslint-disable-next-line no-undef - return btoa(unescape(encodeURIComponent(str))); - } catch (e) { - return ''; - } -} - -function rot13(str) { - return (str || '').replace(/[A-Za-z]/g, function (char) { - var start = char <= 'Z' ? 65 : 97; - return String.fromCharCode(((char.charCodeAt(0) - start + 13) % 26) + start); - }); -} - -function decodeFilename(filename) { - if (!filename) return filename; - try { - var decoded = filename; - if (decoded.indexOf('UTF-8') === 0) { - decoded = decoded.substring(5); - } - return decodeURIComponent(decoded); - } catch (e) { - return filename; - } -} - -function getIndexQuality(str) { - var match = (str || '').match(/(\d{3,4})[pP]/); - return match ? parseInt(match[1], 10) : 2160; -} - -function cleanTitle(title) { - var decodedTitle = decodeFilename(title || ''); - var parts = decodedTitle.split(/[.\-_]/); - var qualityTags = ['WEBRip','WEB-DL','WEB','BluRay','HDRip','DVDRip','HDTV','CAM','TS','R5','DVDScr','BRRip','BDRip','DVD','PDTV','HD']; - var audioTags = ['AAC','AC3','DTS','MP3','FLAC','DD5','EAC3','Atmos']; - var subTags = ['ESub','ESubs','Subs','MultiSub','NoSub','EnglishSub','HindiSub']; - var codecTags = ['x264','x265','H264','HEVC','AVC']; - - var startIndex = parts.findIndex(function (part) { - return qualityTags.some(function (tag) { return part.toLowerCase().indexOf(tag.toLowerCase()) !== -1; }); - }); - - var endIndex = parts.map(function (part, index) { - var hasTag = subTags.concat(audioTags).concat(codecTags).some(function (tag) { - return part.toLowerCase().indexOf(tag.toLowerCase()) !== -1; - }); - return hasTag ? index : -1; - }).filter(function (i) { return i !== -1; }).pop() || -1; - - if (startIndex !== -1 && endIndex !== -1 && endIndex >= startIndex) { - return parts.slice(startIndex, endIndex + 1).join('.'); - } else if (startIndex !== -1) { - return parts.slice(startIndex).join('.'); - } else { - return parts.slice(-3).join('.'); - } -} - -function normalizeTitle(title) { - return (title || '') - .toLowerCase() - .replace(/[^a-z0-9\s]/g, ' ') - .replace(/\s+/g, ' ') - .trim(); -} - -function calculateSimilarity(str1, str2) { - var s1 = normalizeTitle(str1); - var s2 = normalizeTitle(str2); - if (s1 === s2) return 1.0; - var len1 = s1.length; - var len2 = s2.length; - if (len1 === 0) return len2 === 0 ? 1.0 : 0.0; - if (len2 === 0) return 0.0; - var matrix = Array(len1 + 1).fill(null).map(function () { return Array(len2 + 1).fill(0); }); - for (var i = 0; i <= len1; i++) matrix[i][0] = i; - for (var j = 0; j <= len2; j++) matrix[0][j] = j; - for (i = 1; i <= len1; i++) { - for (j = 1; j <= len2; j++) { - var cost = s1[i - 1] === s2[j - 1] ? 0 : 1; - matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost); - } - } - var maxLen = Math.max(len1, len2); - return (maxLen - matrix[len1][len2]) / maxLen; -} - -function findBestMatch(results, query) { - if (!results || results.length === 0) return null; - if (results.length === 1) return results[0]; - var scored = results.map(function (r) { - var score = 0; - if (normalizeTitle(r.title) === normalizeTitle(query)) score += 100; - var sim = calculateSimilarity(r.title, query); score += sim * 50; - if (normalizeTitle(r.title).indexOf(normalizeTitle(query)) !== -1) score += 15; // quick containment bonus - var lengthDiff = Math.abs(r.title.length - query.length); - score += Math.max(0, 10 - lengthDiff / 5); - if (/(19|20)\d{2}/.test(r.title)) score += 5; - return { item: r, score: score }; - }); - scored.sort(function (a, b) { return b.score - a.score; }); - return scored[0].item; -} - -// URL utils – replicate UHDMovies validation style -function validateVideoUrl(url, timeout) { - console.log('[4KHDHub] Validating URL: ' + (url.substring(0, 100)) + '...'); - return fetch(url, { - method: 'HEAD', - headers: { - 'Range': 'bytes=0-1', - 'User-Agent': DEFAULT_HEADERS['User-Agent'] - } - }).then(function (response) { - if (response.ok || response.status === 206) { - console.log('[4KHDHub] ✓ URL validation successful (' + response.status + ')'); - return true; - } else { - console.log('[4KHDHub] ✗ URL validation failed with status: ' + response.status); - return false; - } - }).catch(function (error) { - console.log('[4KHDHub] ✗ URL validation failed: ' + (error && error.message)); - return false; - }); -} - -function getFilenameFromUrl(url) { - try { - return fetch(url, { - method: 'HEAD', - headers: { 'User-Agent': DEFAULT_HEADERS['User-Agent'] } - }).then(function (res) { - var cd = res.headers.get('content-disposition'); - var filename = null; - if (cd) { - var match = cd.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/i); - if (match && match[1]) filename = (match[1] || '').replace(/["']/g, ''); - } - if (!filename) { - try { - var uo = new URL(url); - filename = uo.pathname.split('/').pop() || ''; - if (filename && filename.indexOf('.') !== -1) filename = filename.replace(/\.[^.]+$/, ''); - } catch (e) { /* ignore */ } - } - var decoded = decodeFilename(filename || ''); - return decoded || null; - }).catch(function () { return null; }); - } catch (e) { - return Promise.resolve(null); - } -} - -// Domains -function getDomains() { - if (domainsCache) return Promise.resolve(domainsCache); - return makeRequest(DOMAINS_URL).then(function (res) { return res.json(); }).then(function (data) { - domainsCache = data; - return domainsCache; - }).catch(function () { return null; }); -} - -// Resolve redirect link style used by 4KHDHub -function getRedirectLinks(url) { - return makeRequest(url).then(function (res) { return res.text(); }).then(function (html) { - var regex = /s\('o','([A-Za-z0-9+/=]+)'|ck\('_wp_http_\d+','([^']+)'/g; - var combined = ''; - var m; - while ((m = regex.exec(html)) !== null) { - var val = m[1] || m[2]; - if (val) combined += val; - } - try { - var decoded = base64Decode(rot13(base64Decode(base64Decode(combined)))); - var obj = JSON.parse(decoded); - var encodedurl = base64Decode(obj.o || '').trim(); - var data = base64Decode(obj.data || '').trim(); - var blog = (obj.blog_url || '').trim(); - if (encodedurl) return encodedurl; - if (blog && data) { - return makeRequest(blog + '?re=' + data).then(function (r) { return r.text(); }).then(function (txt) { return (txt || '').trim(); }).catch(function () { return ''; }); - } - return ''; - } catch (e) { - return ''; - } - }).catch(function () { return ''; }); -} - -// Search content -function searchContent(query) { - return getDomains().then(function (domains) { - if (!domains || !domains['4khdhub']) throw new Error('Failed to get domain information'); - var baseUrl = domains['4khdhub']; - var searchUrl = baseUrl + '/?s=' + encodeURIComponent(query); - return makeRequest(searchUrl).then(function (res) { return res.text(); }).then(function (html) { - var $ = cheerio.load(html); - var results = []; - - // Primary parsing for new movie-card structure - $('a').each(function (i, el) { - var $el = $(el); - var title = $el.find('h3.movie-card-title').text().trim(); - var href = $el.attr('href'); - var poster = $el.find('img').attr('src') || ''; - var year = $el.find('p.movie-card-meta').text().trim(); - - if (title && href) { - var absoluteUrl = href.indexOf('http') === 0 ? href : (baseUrl + (href.indexOf('/') === 0 ? '' : '/') + href); - results.push({ title: title, url: absoluteUrl, poster: poster, year: year }); - } - }); - - // Fallback parsing for legacy card-grid structure - if (results.length === 0) { - $('div.card-grid a').each(function (i, el) { - var $el = $(el); - var title = $el.find('h3').text().trim(); - var href = $el.attr('href'); - var poster = $el.find('img').attr('src') || ''; - if (title && href) { - var absoluteUrl = href.indexOf('http') === 0 ? href : (baseUrl + (href.indexOf('/') === 0 ? '' : '/') + href); - results.push({ title: title, url: absoluteUrl, poster: poster }); - } - }); - } - - // Final fallback for general anchors - if (results.length === 0) { - $('a[href]').each(function (i, el) { - var $el2 = $(el); - var h = $el2.attr('href') || ''; - var t = ($el2.text() || '').trim(); - if (t && h && /\/\d{4}\//.test(h)) { - var abs = h.indexOf('http') === 0 ? h : (baseUrl + (h.indexOf('/') === 0 ? '' : '/') + h); - results.push({ title: t, url: abs, poster: '' }); - } - }); - } - return results; - }); - }); -} - -// Load content page and collect download links (and episodes for TV) -function loadContent(url) { - return makeRequest(url).then(function (res) { return res.text(); }).then(function (html) { - var $ = cheerio.load(html); - var title = ($('h1.page-title').text() || '').split('(')[0].trim(); - var poster = $('meta[property="og:image"]').attr('content') || ''; - var tags = []; - $('div.mt-2 span.badge').each(function (i, el) { tags.push($(el).text()); }); - var year = parseInt(($('div.mt-2 span').first().text() || '').replace(/[^0-9]/g, ''), 10) || null; - var description = $('div.content-section p.mt-4').text().trim() || ''; - var trailer = $('#trailer-btn').attr('data-trailer-url') || ''; - var isMovie = tags.indexOf('Movies') !== -1; - - // Collect all relevant links across multiple selectors (do not stop at first) - var hrefsSet = new Set(); - var selectors = [ - 'div.download-item a', - '.download-item a', - 'a[href*="hubdrive"]', - 'a[href*="hubcloud"]', - 'a[href*="pixeldrain"]', - 'a[href*="buzz"]', - 'a[href*="10gbps"]', - 'a[href*="drive"]', - 'a.btn[href]', - 'a.btn', - 'a[href]' - ]; - for (var s = 0; s < selectors.length; s++) { - $(selectors[s]).each(function (i, el) { - var h = ($(el).attr('href') || '').trim(); - if (!h) return; - // Keep only plausible download/intermediate links - var keep = /hubdrive|hubcloud|pixeldrain|buzz|10gbps|workers\.dev|r2\.dev|id=|download|s3|fsl/i.test(h); - if (keep) hrefsSet.add(h); - }); - } - var hrefs = Array.from(hrefsSet); - - var content = { title: title, poster: poster, tags: tags, year: year, description: description, trailer: trailer, type: isMovie ? 'movie' : 'series' }; - if (isMovie) { - content.downloadLinks = hrefs; - return content; - } - - // Series handling (best-effort; falls back to general links) - var episodesMap = {}; - $('div.episodes-list div.season-item').each(function (i, seasonEl) { - var $season = $(seasonEl); - var seasonText = $season.find('div.episode-number').text() || ''; - var seasonMatch = seasonText.match(/S?([1-9][0-9]*)/); - var seasonNum = seasonMatch ? parseInt(seasonMatch[1], 10) : null; - $season.find('div.episode-download-item').each(function (j, epEl) { - var $ep = $(epEl); - var epText = $ep.find('div.episode-file-info span.badge-psa').text() || ''; - var epMatch = epText.match(/Episode-0*([1-9][0-9]*)/); - var episodeNum = epMatch ? parseInt(epMatch[1], 10) : null; - var epLinks = []; - $ep.find('a').each(function (k, a) { - var h = $(a).attr('href'); if (h && h.trim()) epLinks.push(h); - }); - if (seasonNum && episodeNum && epLinks.length > 0) { - var key = seasonNum + '-' + episodeNum; - if (!episodesMap[key]) episodesMap[key] = { season: seasonNum, episode: episodeNum, downloadLinks: [] }; - episodesMap[key].downloadLinks = episodesMap[key].downloadLinks.concat(epLinks); - } - }); - }); - - var episodes = Object.keys(episodesMap).map(function (k) { - var ep = episodesMap[k]; - ep.downloadLinks = Array.from(new Set(ep.downloadLinks)); - return ep; - }); - - if (episodes.length === 0 && hrefs.length > 0) { - content.episodes = [{ season: 1, episode: 1, downloadLinks: hrefs }]; - } else { - content.episodes = episodes; - } - return content; - }); -} - -// Extract HubCloud links -> [{name,title,url,quality}] -function extractHubCloudLinks(url, referer) { - var origin; - try { origin = new URL(url).origin; } catch (e) { origin = ''; } - - function toAbsolute(href, base) { - try { return new URL(href, base).href; } catch (e) { return href; } - } - - function resolveBuzzServer(buttonLink) { - var baseOrigin = (function () { try { return new URL(buttonLink).origin; } catch (e) { return origin; } })(); - var dlUrl = buttonLink.replace(/\/?$/, '') + '/download'; - return fetch(dlUrl, { headers: { 'Referer': buttonLink, 'User-Agent': DEFAULT_HEADERS['User-Agent'] }, redirect: 'manual' }) - .then(function (res) { - var hx = res.headers.get('hx-redirect') || res.headers.get('location'); - if (hx) return toAbsolute(hx, baseOrigin); - // Fallback: if manual redirect unsupported, use final response URL - return res.url || buttonLink; - }).catch(function () { return buttonLink; }); - } - - function resolveTenGbps(initialLink, headerDetails, size, qualityLabel, quality) { - var current = initialLink; - var baseOrigin = (function () { try { return new URL(initialLink).origin; } catch (e) { return origin; } })(); - var maxHops = 6; - function step() { - return fetch(current, { redirect: 'manual', headers: { 'User-Agent': DEFAULT_HEADERS['User-Agent'] } }) - .then(function (res) { - var loc = res.headers.get('location'); - if (!loc) { - // Try current as final - return null; - } - if (loc.indexOf('id=') !== -1) { - var linkParam = (loc.split('link=')[1] || '').trim(); - if (linkParam) { - try { linkParam = decodeURIComponent(linkParam); } catch (e) { /* ignore */ } - return linkParam; - } - return null; - } else { - current = toAbsolute(loc, baseOrigin); - return step(); - } - }); - } - return step().then(function (finalUrl) { - if (!finalUrl) return null; - return getFilenameFromUrl(finalUrl).then(function (actualFilename) { - var displayFilename = actualFilename || headerDetails || 'Unknown'; - var titleParts = []; - if (displayFilename) titleParts.push(displayFilename); - if (size) titleParts.push(size); - var finalTitle = titleParts.join('\n'); - return { name: '4KHDHub - 10Gbps Server' + qualityLabel, title: finalTitle, url: finalUrl, quality: quality }; - }).catch(function () { - var displayFilename = headerDetails || 'Unknown'; - var titleParts = []; - if (displayFilename) titleParts.push(displayFilename); - if (size) titleParts.push(size); - var finalTitle = titleParts.join('\n'); - return { name: '4KHDHub - 10Gbps Server' + qualityLabel, title: finalTitle, url: finalUrl, quality: quality }; - }); - }).catch(function () { return null; }); - } - - return makeRequest(url).then(function (res) { return res.text(); }).then(function (html) { - var $ = cheerio.load(html); - var href = url; - if (url.indexOf('hubcloud.php') === -1) { - var rawHref = $('#download').attr('href') || $('a[href*="hubcloud.php"]').attr('href') || $('.download-btn').attr('href') || $('a[href*="download"]').attr('href'); - if (!rawHref) throw new Error('Download element not found'); - href = toAbsolute(rawHref, origin); - } - return makeRequest(href).then(function (res2) { return res2.text(); }).then(function (html2) { - var $$ = cheerio.load(html2); - - function buildTask(buttonText, buttonLink, headerDetails, size, quality) { - var qualityLabel = quality ? (' - ' + quality + 'p') : ''; - // Pixeldrain normalization - var pd = buttonLink.match(/pixeldrain\.(?:net|dev)\/u\/([a-zA-Z0-9]+)/); - if (pd && pd[1]) buttonLink = 'https://pixeldrain.net/api/file/' + pd[1]; - - if (buttonText.indexOf('BuzzServer') !== -1) { - return resolveBuzzServer(buttonLink).then(function (finalUrl) { - return getFilenameFromUrl(finalUrl).then(function (actualFilename) { - var displayFilename = actualFilename || headerDetails || 'Unknown'; - var titleParts = []; - if (displayFilename) titleParts.push(displayFilename); - if (size) titleParts.push(size); - var finalTitle = titleParts.join('\n'); - return { name: '4KHDHub - BuzzServer' + qualityLabel, title: finalTitle, url: finalUrl, quality: quality, size: size || null, fileName: actualFilename || null }; - }).catch(function () { - var displayFilename = headerDetails || 'Unknown'; - var titleParts = []; - if (displayFilename) titleParts.push(displayFilename); - if (size) titleParts.push(size); - var finalTitle = titleParts.join('\n'); - return { name: '4KHDHub - BuzzServer' + qualityLabel, title: finalTitle, url: finalUrl, quality: quality, size: size || null, fileName: null }; - }); - }).catch(function () { return null; }); - } - if (buttonText.indexOf('10Gbps') !== -1) { - return resolveTenGbps(buttonLink, headerDetails, size, qualityLabel, quality); - } - return getFilenameFromUrl(buttonLink).then(function (actualFilename) { - var displayFilename = actualFilename || headerDetails || 'Unknown'; - var titleParts = []; - if (displayFilename) titleParts.push(displayFilename); - if (size) titleParts.push(size); - var finalTitle = titleParts.join('\n'); - var name; - if (buttonText.indexOf('FSL Server') !== -1) name = '4KHDHub - FSL Server' + qualityLabel; - else if (buttonText.indexOf('S3 Server') !== -1) name = '4KHDHub - S3 Server' + qualityLabel; - else if (/pixeldra/i.test(buttonText) || /pixeldra/i.test(buttonLink)) name = '4KHDHub - Pixeldrain' + qualityLabel; - else if (buttonText.indexOf('Download File') !== -1) name = '4KHDHub - HubCloud' + qualityLabel; - else name = '4KHDHub - HubCloud' + qualityLabel; - return { name: name, title: finalTitle, url: buttonLink, quality: quality, size: size || null, fileName: actualFilename || null }; - }).catch(function () { - var displayFilename = headerDetails || 'Unknown'; - var titleParts = []; - if (displayFilename) titleParts.push(displayFilename); - if (size) titleParts.push(size); - var finalTitle = titleParts.join('\n'); - var name = '4KHDHub - HubCloud' + qualityLabel; - return { name: name, title: finalTitle, url: buttonLink, quality: quality, size: size || null, fileName: null }; - }); - } - - // Iterate per card to capture per-quality sections - var tasks = []; - var cards = $$('.card'); - if (cards.length > 0) { - cards.each(function (ci, card) { - var $card = $$(card); - var header = $card.find('div.card-header').text() || $$('div.card-header').first().text() || ''; - var size = $card.find('i#size').text() || $$('i#size').first().text() || ''; - var quality = getIndexQuality(header); - var headerDetails = cleanTitle(header); - var localBtns = $card.find('div.card-body h2 a.btn'); - if (localBtns.length === 0) localBtns = $card.find('a.btn, .btn, a[href]'); - localBtns.each(function (i, el) { - var $btn = $$(el); - var text = ($btn.text() || '').trim(); - var link = $btn.attr('href'); - if (!link) return; - link = toAbsolute(link, href); - // Only consider plausible buttons - if (!/(hubcloud|hubdrive|pixeldrain|buzz|10gbps|workers\.dev|r2\.dev|download|api\/file)/i.test(link) && text.toLowerCase().indexOf('download') === -1) return; - tasks.push(buildTask(text, link, headerDetails, size, quality)); - }); - }); - } - - // Fallback: whole page buttons - if (tasks.length === 0) { - var buttons = $$.root().find('div.card-body h2 a.btn'); - if (buttons.length === 0) { - var altSelectors = ['a.btn', '.btn', 'a[href]']; - for (var si = 0; si < altSelectors.length && buttons.length === 0; si++) { - buttons = $$.root().find(altSelectors[si]); - } - } - var size = $$('i#size').first().text() || ''; - var header = $$('div.card-header').first().text() || ''; - var quality = getIndexQuality(header); - var headerDetails = cleanTitle(header); - buttons.each(function (i, el) { - var $btn = $$(el); - var text = ($btn.text() || '').trim(); - var link = $btn.attr('href'); - if (!link) return; - link = toAbsolute(link, href); - tasks.push(buildTask(text, link, headerDetails, size, quality)); - }); - } - - if (tasks.length === 0) return []; - return Promise.all(tasks).then(function (arr) { return (arr || []).filter(function (x) { return !!x; }); }); - }); - }).catch(function () { return []; }); -} - -// Extract HubDrive links (wrapper around HubCloud if needed) -function extractHubDriveLinks(url, referer) { - return makeRequest(url).then(function (res) { return res.text(); }).then(function (html) { - var $ = cheerio.load(html); - var size = $('i#size').text() || ''; - var header = $('div.card-header').text() || ''; - var quality = getIndexQuality(header); - var headerDetails = cleanTitle(header); - var filename = (headerDetails || header || 'Unknown').replace(/^4kHDHub\.com\s*[-_]?\s*/i, '').replace(/\.[a-z0-9]{2,4}$/i, '').replace(/[._]/g, ' ').trim(); - var primaryBtn = $('.btn.btn-primary.btn-user.btn-success1.m-1').attr('href') || $('a.btn.btn-primary').attr('href') || $('a[href*="download"]').attr('href') || $('a.btn').attr('href'); - if (!primaryBtn) return []; - if ((primaryBtn || '').toLowerCase().indexOf('hubcloud') !== -1) { - return extractHubCloudLinks(primaryBtn, '4KHDHub'); - } - var qualityLabel = quality ? (' - ' + quality + 'p') : ''; - return getFilenameFromUrl(primaryBtn).then(function (actualFilename) { - var displayFilename = actualFilename || filename || 'Unknown'; - var titleParts = []; - if (displayFilename) titleParts.push(displayFilename); - if (size) titleParts.push(size); - var finalTitle = titleParts.join('\n'); - return [{ name: '4KHDHub - HubDrive' + qualityLabel, title: finalTitle, url: primaryBtn, quality: quality }]; - }).catch(function () { - var displayFilename = filename || 'Unknown'; - var titleParts = []; - if (displayFilename) titleParts.push(displayFilename); - if (size) titleParts.push(size); - var finalTitle = titleParts.join('\n'); - return [{ name: '4KHDHub - HubDrive' + qualityLabel, title: finalTitle, url: primaryBtn, quality: quality }]; - }); - }).catch(function () { return []; }); -} - -// Dispatcher for a single link to final streams -function processExtractorLink(link) { - var lower = (link || '').toLowerCase(); - if (lower.indexOf('hubdrive') !== -1) { - return extractHubDriveLinks(link, '4KHDHub'); - } else if (lower.indexOf('hubcloud') !== -1) { - return extractHubCloudLinks(link, '4KHDHub'); - } else if (lower.indexOf('workers.dev') !== -1 || lower.indexOf('r2.dev') !== -1) { - // Cloudflare Workers / R2 links – treat as HubCloud direct files - return getFilenameFromUrl(link).then(function (actualFilename) { - var displayFilename = actualFilename || 'HubCloud File'; - var titleParts = []; - if (displayFilename) titleParts.push(displayFilename); - var finalTitle = titleParts.join('\n'); - return [{ name: '4KHDHub - HubCloud - 1080p', title: finalTitle, url: link, quality: 1080, size: null, fileName: actualFilename || null }]; - }).catch(function () { - return [{ name: '4KHDHub - HubCloud - 1080p', title: 'HubCloud File', url: link, quality: 1080, size: null, fileName: null }]; - }); - } else if (lower.indexOf('pixeldrain') !== -1) { - // Normalize pixeldrain URLs to API endpoint - var converted = link; - var m = link.match(/pixeldrain\.(?:net|dev)\/u\/([a-zA-Z0-9]+)/); - if (m && m[1]) converted = 'https://pixeldrain.net/api/file/' + m[1]; - return getFilenameFromUrl(converted).then(function (actualFilename) { - var displayFilename = actualFilename || 'Pixeldrain File'; - var title = displayFilename + '\nPixeldrain'; - return [{ name: '4KHDHub - Pixeldrain - 1080p', title: title, url: converted, quality: 1080, size: null, fileName: actualFilename || null }]; - }).catch(function () { - var title = 'Pixeldrain File\nPixeldrain'; - return [{ name: '4KHDHub - Pixeldrain - 1080p', title: title, url: converted, quality: 1080, size: null, fileName: null }]; - }); - } else if (/\.m(ov|p4|kv)$|\.avi$/i.test(link)) { - // Direct video link - var filename = (function () { - try { return decodeFilename(new URL(link).pathname.split('/').pop().replace(/\.[^/.]+$/, '').replace(/[._]/g, ' ')); } catch (e) { return 'Direct Link'; } - })(); - return getFilenameFromUrl(link).then(function (actualFilename) { - var displayFilename = actualFilename || filename || 'Unknown'; - return [{ name: '4KHDHub Direct Link', title: displayFilename + '\n[Direct Link]', url: link, quality: 1080, size: null, fileName: actualFilename || null }]; - }).catch(function () { - return [{ name: '4KHDHub Direct Link', title: filename + '\n[Direct Link]', url: link, quality: 1080, size: null, fileName: null }]; - }); - } - return Promise.resolve([]); -} - -// Convert a list of resolved hosting URLs to final stream entries -function extractStreamingLinks(downloadLinks) { - var tasks = (downloadLinks || []).map(function (lnk) { - return processExtractorLink(lnk).then(function (res) { return res || []; }).catch(function () { return []; }); - }); - return Promise.all(tasks).then(function (arrs) { - var flat = [].concat.apply([], arrs).filter(function (x) { return x && x.url; }); - // Filter out .zip and suspicious AMP - var suspicious = ['www-google-com.cdn.ampproject.org', 'bloggingvector.shop', 'cdn.ampproject.org']; - flat = flat.filter(function (x) { - var u = (x.url || '').toLowerCase(); - if (u.endsWith('.zip')) return false; - return !suspicious.some(function (p) { return u.indexOf(p) !== -1; }); - }); - // Deduplicate by URL - var seen = {}; - var unique = []; - flat.forEach(function (x) { if (!seen[x.url]) { seen[x.url] = 1; unique.push(x); } }); - return unique; - }); -} - -// TMDB helper -function getTMDBDetails(tmdbId, mediaType) { - var url = 'https://api.themoviedb.org/3/' + mediaType + '/' + tmdbId + '?api_key=' + TMDB_API_KEY; - return makeRequest(url).then(function (res) { return res.json(); }).then(function (data) { - if (mediaType === 'movie') { - return { title: data.title, original_title: data.original_title, year: data.release_date ? data.release_date.split('-')[0] : null }; - } else { - return { title: data.name, original_title: data.original_name, year: data.first_air_date ? data.first_air_date.split('-')[0] : null }; - } - }).catch(function () { return null; }); -} - -// Main entry – Promise-based, no async/await -function getStreams(tmdbId, type, season, episode) { - type = type || 'movie'; - var cacheKey = '4khdhub_resolved_urls_v1_' + tmdbId + '_' + type + (season ? ('_s' + season + 'e' + (episode || '')) : ''); - var disableValidation = ((typeof URL_VALIDATION_ENABLED !== 'undefined') && (URL_VALIDATION_ENABLED === false)) || - ((typeof DISABLE_4KHDHUB_URL_VALIDATION !== 'undefined') && (DISABLE_4KHDHUB_URL_VALIDATION === true)); - - function finalizeToStreams(links) { - var tasks = links.map(function (link) { return disableValidation ? Promise.resolve(true) : validateVideoUrl(link.url); }); - return Promise.all(tasks).then(function (vals) { - var validated = links.filter(function (l, idx) { return !!vals[idx]; }); - return validated.map(function (l) { - return { - name: l.name, - title: l.title || l.name, - url: l.url, - quality: (l.quality ? (l.quality + 'p') : '1080p'), - size: l.size || null, - fileName: l.fileName || null, - type: 'direct', - behaviorHints: { bingeGroup: '4khdhub-streams' } - }; - }); - }); - } - - var cached = resolvedUrlsCache[cacheKey]; - if (cached && cached.length > 0) { - return extractStreamingLinks(cached).then(function (streams) { return finalizeToStreams(streams); }); - } - - var tmdbType = (type === 'series' ? 'tv' : type); - return getTMDBDetails(tmdbId, tmdbType).then(function (tmdb) { - if (!tmdb || !tmdb.title) return []; - return searchContent(tmdb.title).then(function (results) { - if (!results || results.length === 0) return []; - var best = findBestMatch(results, tmdb.title) || results[0]; - return loadContent(best.url).then(function (content) { - var downloadLinks = []; - if (type === 'movie') { - downloadLinks = content.downloadLinks || []; - } else if ((type === 'series' || type === 'tv') && season && episode) { - var target = (content.episodes || []).find(function (ep) { return ep.season === parseInt(season, 10) && ep.episode === parseInt(episode, 10); }); - downloadLinks = target ? (target.downloadLinks || []) : []; - } - if (!downloadLinks || downloadLinks.length === 0) return []; - - // Resolve redirect-style links to file hosts - var resolverTasks = downloadLinks.map(function (lnk) { - var needs = (lnk || '').toLowerCase().indexOf('id=') !== -1; - if (needs) { - return getRedirectLinks(lnk).then(function (r) { return r && r.trim() ? r : null; }).catch(function () { return null; }); - } - return Promise.resolve(lnk); - }); - - return Promise.all(resolverTasks).then(function (resolvedArr) { - var resolved = resolvedArr.filter(function (x) { return x && x.trim(); }); - if (resolved.length === 0) return []; - resolvedUrlsCache[cacheKey] = resolved; // cache in-memory - return extractStreamingLinks(resolved).then(function (links) { return finalizeToStreams(links); }); - }); - }); - }); - }).catch(function () { return []; }); -} - -// Export -if (typeof module !== 'undefined' && module.exports) { - module.exports = { getStreams }; -} else { - // RN global - // eslint-disable-next-line no-undef - global.getStreams = getStreams; -} - - diff --git a/local-scrapers-repo/providers/dahmermovies.js b/local-scrapers-repo/providers/dahmermovies.js deleted file mode 100644 index bef7581..0000000 --- a/local-scrapers-repo/providers/dahmermovies.js +++ /dev/null @@ -1,329 +0,0 @@ -// Dahmer Movies Scraper for Nuvio Local Scrapers -// React Native compatible version - -console.log('[DahmerMovies] Initializing Dahmer Movies scraper'); - -// Constants -const TMDB_API_KEY = "439c478a771f35c05022f9feabcca01c"; -const DAHMER_MOVIES_API = 'https://a.111477.xyz'; -const TIMEOUT = 60000; // 60 seconds - -// Quality mapping -const Qualities = { - Unknown: 0, - P144: 144, - P240: 240, - P360: 360, - P480: 480, - P720: 720, - P1080: 1080, - P1440: 1440, - P2160: 2160 -}; - -// Helper function to make HTTP requests -function makeRequest(url, options = {}) { - const requestOptions = { - timeout: TIMEOUT, - headers: { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', - 'Accept-Language': 'en-US,en;q=0.5', - 'Connection': 'keep-alive', - ...options.headers - }, - ...options - }; - - return fetch(url, requestOptions).then(function(response) { - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - return response; - }); -} - -// Utility functions -function getEpisodeSlug(season = null, episode = null) { - if (season === null && episode === null) { - return ['', '']; - } - const seasonSlug = season < 10 ? `0${season}` : `${season}`; - const episodeSlug = episode < 10 ? `0${episode}` : `${episode}`; - return [seasonSlug, episodeSlug]; -} - -function getIndexQuality(str) { - if (!str) return Qualities.Unknown; - const match = str.match(/(\d{3,4})[pP]/); - return match ? parseInt(match[1]) : Qualities.Unknown; -} - -// Extract quality with codec information -function getQualityWithCodecs(str) { - if (!str) return 'Unknown'; - - // Extract base quality (resolution) - const qualityMatch = str.match(/(\d{3,4})[pP]/); - const baseQuality = qualityMatch ? `${qualityMatch[1]}p` : 'Unknown'; - - // Extract codec information (excluding HEVC and bit depth) - const codecs = []; - const lowerStr = str.toLowerCase(); - - // HDR formats - if (lowerStr.includes('dv') || lowerStr.includes('dolby vision')) codecs.push('DV'); - if (lowerStr.includes('hdr10+')) codecs.push('HDR10+'); - else if (lowerStr.includes('hdr10') || lowerStr.includes('hdr')) codecs.push('HDR'); - - // Special formats - if (lowerStr.includes('remux')) codecs.push('REMUX'); - if (lowerStr.includes('imax')) codecs.push('IMAX'); - - // Combine quality with codecs using pipeline separator - if (codecs.length > 0) { - return `${baseQuality} | ${codecs.join(' | ')}`; - } - - return baseQuality; -} - -function getIndexQualityTags(str, fullTag = false) { - if (!str) return ''; - - if (fullTag) { - const match = str.match(/(.*)\.(?:mkv|mp4|avi)/i); - return match ? match[1].trim() : str; - } else { - const match = str.match(/\d{3,4}[pP]\.?(.*?)\.(mkv|mp4|avi)/i); - return match ? match[1].replace(/\./g, ' ').trim() : str; - } -} - -function encodeUrl(url) { - try { - return encodeURI(url); - } catch (e) { - return url; - } -} - -function decode(input) { - try { - return decodeURIComponent(input); - } catch (e) { - return input; - } -} - -// Format file size from bytes to human readable format -function formatFileSize(sizeText) { - if (!sizeText) return null; - - // If it's already formatted (contains GB, MB, etc.), return as is - if (/\d+(\.\d+)?\s*(GB|MB|KB|TB)/i.test(sizeText)) { - return sizeText; - } - - // If it's a number (bytes), convert to human readable - const bytes = parseInt(sizeText); - if (isNaN(bytes)) return sizeText; - - const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; - if (bytes === 0) return '0 Bytes'; - - const i = Math.floor(Math.log(bytes) / Math.log(1024)); - const size = (bytes / Math.pow(1024, i)).toFixed(2); - - return `${size} ${sizes[i]}`; -} - -// Parse HTML using basic string manipulation (React Native compatible) -function parseLinks(html) { - const links = []; - - // Parse table rows to get both links and file sizes - const rowRegex = /]*>(.*?)<\/tr>/gis; - let rowMatch; - - while ((rowMatch = rowRegex.exec(html)) !== null) { - const rowContent = rowMatch[1]; - - // Extract link from the row - const linkMatch = rowContent.match(/]*href=["']([^"']*)["'][^>]*>([^<]*)<\/a>/i); - if (!linkMatch) continue; - - const href = linkMatch[1]; - const text = linkMatch[2].trim(); - - // Skip parent directory and empty links - if (!text || href === '../' || text === '../') continue; - - // Extract file size from the same row - let size = null; - const sizeMatch = rowContent.match(/]*class=["']filesize["'][^>]*[^>]*>([^<]+)<\/td>/i); - if (sizeMatch) { - size = sizeMatch[1].trim(); - } - - links.push({ text, href, size }); - } - - // Fallback to simple link parsing if table parsing fails - if (links.length === 0) { - const linkRegex = /]*href=["']([^"']*)["'][^>]*>([^<]*)<\/a>/gi; - let match; - - while ((match = linkRegex.exec(html)) !== null) { - const href = match[1]; - const text = match[2].trim(); - if (text && href && href !== '../' && text !== '../') { - links.push({ text, href, size: null }); - } - } - } - - return links; -} - -// Main Dahmer Movies fetcher function -function invokeDahmerMovies(title, year, season = null, episode = null) { - console.log(`[DahmerMovies] Searching for: ${title} (${year})${season ? ` Season ${season}` : ''}${episode ? ` Episode ${episode}` : ''}`); - - // Construct URL based on content type (with proper encoding) - const encodedUrl = season === null - ? `${DAHMER_MOVIES_API}/movies/${encodeURIComponent(title.replace(/:/g, '') + ' (' + year + ')')}/` - : `${DAHMER_MOVIES_API}/tvs/${encodeURIComponent(title.replace(/:/g, ' -'))}/Season ${season}/`; - - console.log(`[DahmerMovies] Fetching from: ${encodedUrl}`); - - return makeRequest(encodedUrl).then(function(response) { - return response.text(); - }).then(function(html) { - console.log(`[DahmerMovies] Response length: ${html.length}`); - - // Parse HTML to extract links - const paths = parseLinks(html); - console.log(`[DahmerMovies] Found ${paths.length} total links`); - - // Filter based on content type - let filteredPaths; - if (season === null) { - // For movies, filter by quality (1080p or 2160p) - filteredPaths = paths.filter(path => - /(1080p|2160p)/i.test(path.text) - ); - console.log(`[DahmerMovies] Filtered to ${filteredPaths.length} movie links (1080p/2160p only)`); - } else { - // For TV shows, filter by season and episode - const [seasonSlug, episodeSlug] = getEpisodeSlug(season, episode); - const episodePattern = new RegExp(`S${seasonSlug}E${episodeSlug}`, 'i'); - filteredPaths = paths.filter(path => - episodePattern.test(path.text) - ); - console.log(`[DahmerMovies] Filtered to ${filteredPaths.length} TV episode links (S${seasonSlug}E${episodeSlug})`); - } - - if (filteredPaths.length === 0) { - console.log('[DahmerMovies] No matching content found'); - return []; - } - - // Process and return results - const results = filteredPaths.map(path => { - const quality = getIndexQuality(path.text); - const qualityWithCodecs = getQualityWithCodecs(path.text); - const tags = getIndexQualityTags(path.text); - - // Construct proper URL - handle relative paths correctly - let fullUrl; - if (path.href.startsWith('http')) { - // Already a full URL - need to encode it properly - try { - // Parse the URL and let the URL constructor handle encoding - const url = new URL(path.href); - // Reconstruct the URL with properly encoded pathname - fullUrl = `${url.protocol}//${url.host}${url.pathname}`; - } catch (error) { - // Fallback: manually encode if URL parsing fails - console.log(`[DahmerMovies] URL parsing failed, manually encoding: ${path.href}`); - fullUrl = path.href.replace(/ /g, '%20'); - } - } else { - // Relative path - combine with encoded base URL - const baseUrl = encodedUrl.endsWith('/') ? encodedUrl : encodedUrl + '/'; - const relativePath = path.href.startsWith('/') ? path.href.substring(1) : path.href; - const encodedFilename = encodeURIComponent(relativePath); - fullUrl = baseUrl + encodedFilename; - } - - return { - name: "DahmerMovies", - title: `DahmerMovies ${tags || path.text}`, - url: fullUrl, - quality: qualityWithCodecs, // Use enhanced quality with codecs - size: formatFileSize(path.size), // Format file size - type: 'direct', - filename: path.text - }; - }); - - // Sort by quality (highest first) - results.sort((a, b) => { - const qualityA = getIndexQuality(a.filename); - const qualityB = getIndexQuality(b.filename); - return qualityB - qualityA; - }); - - console.log(`[DahmerMovies] Successfully processed ${results.length} streams`); - return results; - - }).catch(function(error) { - if (error.name === 'AbortError') { - console.log('[DahmerMovies] Request timeout - server took too long to respond'); - } else { - console.log(`[DahmerMovies] Error: ${error.message}`); - } - return []; - }); -} - -// Main function to get streams for TMDB content -function getStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) { - console.log(`[DahmerMovies] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}${seasonNum ? `, S${seasonNum}E${episodeNum}` : ''}`); - - // Get TMDB info - const tmdbUrl = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY}`; - return makeRequest(tmdbUrl).then(function(tmdbResponse) { - return tmdbResponse.json(); - }).then(function(tmdbData) { - const title = mediaType === 'tv' ? tmdbData.name : tmdbData.title; - const year = mediaType === 'tv' ? tmdbData.first_air_date?.substring(0, 4) : tmdbData.release_date?.substring(0, 4); - - if (!title) { - throw new Error('Could not extract title from TMDB response'); - } - - console.log(`[DahmerMovies] TMDB Info: "${title}" (${year})`); - - // Call the main scraper function - return invokeDahmerMovies( - title, - year ? parseInt(year) : null, - seasonNum, - episodeNum - ); - - }).catch(function(error) { - console.error(`[DahmerMovies] Error in getStreams: ${error.message}`); - return []; - }); -} - -// Export the main function -if (typeof module !== 'undefined' && module.exports) { - module.exports = { getStreams }; -} else { - // For React Native environment - global.getStreams = getStreams; -} \ No newline at end of file diff --git a/local-scrapers-repo/providers/hdrezka.js b/local-scrapers-repo/providers/hdrezka.js deleted file mode 100644 index 8d9ea29..0000000 --- a/local-scrapers-repo/providers/hdrezka.js +++ /dev/null @@ -1,437 +0,0 @@ -// HDRezka Scraper for Nuvio Local Scrapers -// React Native compatible version - No async/await for sandbox compatibility - -// Import cheerio for HTML parsing (React Native compatible) -const cheerio = require('cheerio-without-node-native'); - -console.log('[HDRezka] Using cheerio-without-node-native for DOM parsing'); - -// Constants -const TMDB_API_KEY = "439c478a771f35c05022f9feabcca01c"; -const REZKA_BASE = 'https://hdrezka.ag/'; -const BASE_HEADERS = { - 'X-Hdrezka-Android-App': '1', - 'X-Hdrezka-Android-App-Version': '2.2.0', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', - 'Accept-Language': 'en-US,en;q=0.5', - 'Connection': 'keep-alive' -}; - -// Helper function to make HTTP requests -function makeRequest(url, options = {}) { - return fetch(url, { - ...options, - headers: { - ...BASE_HEADERS, - ...options.headers - } - }).then(function (response) { - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - return response; - }); -} - -// Generate random favs parameter -function generateRandomFavs() { - const randomHex = () => Math.floor(Math.random() * 16).toString(16); - const generateSegment = (length) => Array.from({ length }, randomHex).join(''); - - return `${generateSegment(8)}-${generateSegment(4)}-${generateSegment(4)}-${generateSegment(4)}-${generateSegment(12)}`; -} - -// Extract title and year from search result -function extractTitleAndYear(input) { - const regex = /^(.*?),.*?(\d{4})/; - const match = input.match(regex); - - if (match) { - const title = match[1]; - const year = match[2]; - return { title: title.trim(), year: year ? parseInt(year, 10) : null }; - } - return null; -} - -// Parse video links from HDRezka response (optimized) -function parseVideoLinks(inputString) { - if (!inputString) { - console.log('[HDRezka] No video links found'); - return {}; - } - - console.log(`[HDRezka] Parsing video links from stream URL data`); - const linksArray = inputString.split(','); - const result = {}; - - // Pre-compile regex patterns for better performance - const simplePattern = /\[([^<\]]+)\](https?:\/\/[^\s,]+\.mp4|null)/; - const qualityPattern = /\[]*>([^<]+)/; - const urlPattern = /\][^[]*?(https?:\/\/[^\s,]+\.mp4|null)/; - - for (const link of linksArray) { - // Try simple format first (non-HTML) - let match = link.match(simplePattern); - - // If not found, try HTML format with more flexible pattern - if (!match) { - const qualityMatch = link.match(qualityPattern); - const urlMatch = link.match(urlPattern); - - if (qualityMatch && urlMatch) { - match = [null, qualityMatch[1].trim(), urlMatch[1]]; - } - } - - if (match) { - const qualityText = match[1].trim(); - const mp4Url = match[2]; - - // Skip null URLs (premium content that requires login) - if (mp4Url !== 'null') { - result[qualityText] = { type: 'mp4', url: mp4Url }; - console.log(`[HDRezka] Found ${qualityText}: ${mp4Url.substring(0, 50)}...`); - } else { - console.log(`[HDRezka] Premium quality ${qualityText} requires login (null URL)`); - } - } else { - console.log(`[HDRezka] Could not parse quality from: ${link.substring(0, 100)}...`); - } - } - - console.log(`[HDRezka] Found ${Object.keys(result).length} valid qualities: ${Object.keys(result).join(', ')}`); - return result; -} - -// Parse subtitles from HDRezka response (optimized) -function parseSubtitles(inputString) { - if (!inputString) { - console.log('[HDRezka] No subtitles found'); - return []; - } - - console.log(`[HDRezka] Parsing subtitles data`); - const linksArray = inputString.split(','); - const captions = []; - - // Pre-compile regex pattern for better performance - const subtitlePattern = /\[([^\]]+)\](https?:\/\/\S+?)(?=,\[|$)/; - - for (const link of linksArray) { - const match = link.match(subtitlePattern); - - if (match) { - const language = match[1]; - const url = match[2]; - - captions.push({ - id: url, - language, - hasCorsRestrictions: false, - type: 'vtt', - url: url, - }); - console.log(`[HDRezka] Found subtitle ${language}: ${url.substring(0, 50)}...`); - } - } - - console.log(`[HDRezka] Found ${captions.length} subtitles`); - return captions; -} - -// Search for content and find media ID -function searchAndFindMediaId(media) { - console.log(`[HDRezka] Searching for title: ${media.title}, type: ${media.type}, year: ${media.releaseYear || 'any'}`); - - const itemRegexPattern = /([^<]+)<\/span> \(([^)]+)\)/g; - const idRegexPattern = /\/(\d+)-[^/]+\.html$/; - - const fullUrl = new URL('/engine/ajax/search.php', REZKA_BASE); - fullUrl.searchParams.append('q', media.title); - - console.log(`[HDRezka] Making search request to: ${fullUrl.toString()}`); - return makeRequest(fullUrl.toString()).then(function (response) { - return response.text(); - }).then(function (searchData) { - console.log(`[HDRezka] Search response length: ${searchData.length}`); - - const movieData = []; - let match; - - while ((match = itemRegexPattern.exec(searchData)) !== null) { - const url = match[1]; - const titleAndYear = match[3]; - - const result = extractTitleAndYear(titleAndYear); - if (result !== null) { - const id = url.match(idRegexPattern)?.[1] || null; - const isMovie = url.includes('/films/'); - const isShow = url.includes('/series/'); - const type = isMovie ? 'movie' : isShow ? 'tv' : 'unknown'; - - movieData.push({ - id: id ?? '', - year: result.year ?? 0, - type, - url, - title: match[2] - }); - console.log(`[HDRezka] Found: id=${id}, title=${match[2]}, type=${type}, year=${result.year}`); - } - } - - // Filter by year if provided - let filteredItems = movieData; - if (media.releaseYear) { - filteredItems = movieData.filter(item => item.year === media.releaseYear); - console.log(`[HDRezka] Items filtered by year ${media.releaseYear}: ${filteredItems.length}`); - } - - // Filter by type if provided - if (media.type) { - filteredItems = filteredItems.filter(item => item.type === media.type); - console.log(`[HDRezka] Items filtered by type ${media.type}: ${filteredItems.length}`); - } - - if (filteredItems.length === 0 && movieData.length > 0) { - console.log(`[HDRezka] No exact match found, using first result: ${movieData[0].title}`); - return movieData[0]; - } - - if (filteredItems.length > 0) { - console.log(`[HDRezka] Selected item: id=${filteredItems[0].id}, title=${filteredItems[0].title}`); - return filteredItems[0]; - } else { - console.log(`[HDRezka] No matching items found`); - return null; - } - }); -} - -// Get translator ID from media page -function getTranslatorId(url, id, media) { - console.log(`[HDRezka] Getting translator ID for url=${url}, id=${id}`); - - // Make sure the URL is absolute - const fullUrl = url.startsWith('http') ? url : `${REZKA_BASE}${url.startsWith('/') ? url.substring(1) : url}`; - console.log(`[HDRezka] Making request to: ${fullUrl}`); - - return makeRequest(fullUrl).then(function (response) { - return response.text(); - }).then(function (responseText) { - console.log(`[HDRezka] Translator page response length: ${responseText.length}`); - - // Translator ID 238 represents the Original + subtitles player. - if (responseText.includes(`data-translator_id="238"`)) { - console.log(`[HDRezka] Found translator ID 238 (Original + subtitles)`); - return '238'; - } - - const functionName = media.type === 'movie' ? 'initCDNMoviesEvents' : 'initCDNSeriesEvents'; - const regexPattern = new RegExp(`sof\.tv\.${functionName}\\(${id}, ([^,]+)`, 'i'); - const match = responseText.match(regexPattern); - const translatorId = match ? match[1] : null; - - console.log(`[HDRezka] Extracted translator ID: ${translatorId}`); - return translatorId; - }); -} - -// Get stream data from HDRezka -function getStreamData(id, translatorId, media) { - console.log(`[HDRezka] Getting stream for id=${id}, translatorId=${translatorId}`); - - const searchParams = new URLSearchParams(); - searchParams.append('id', id); - searchParams.append('translator_id', translatorId); - - if (media.type === 'tv') { - searchParams.append('season', media.season.number.toString()); - searchParams.append('episode', media.episode.number.toString()); - console.log(`[HDRezka] TV params: season=${media.season.number}, episode=${media.episode.number}`); - } - - const randomFavs = generateRandomFavs(); - searchParams.append('favs', randomFavs); - searchParams.append('action', media.type === 'tv' ? 'get_stream' : 'get_movie'); - - const fullUrl = `${REZKA_BASE}ajax/get_cdn_series/`; - console.log(`[HDRezka] Making stream request with action=${media.type === 'tv' ? 'get_stream' : 'get_movie'}`); - - return makeRequest(fullUrl, { - method: 'POST', - body: searchParams, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - } - }).then(function (response) { - return response.text(); - }).then(function (rawText) { - console.log(`[HDRezka] Stream response length: ${rawText.length}`); - - try { - const parsedResponse = JSON.parse(rawText); - console.log(`[HDRezka] Parsed response successfully`); - - // Process video qualities and subtitles synchronously - const qualities = parseVideoLinks(parsedResponse.url); - const captions = parseSubtitles(parsedResponse.subtitle); - - return { qualities, captions }; - } catch (e) { - console.error(`[HDRezka] Failed to parse JSON response: ${e.message}`); - console.log(`[HDRezka] Raw response: ${rawText.substring(0, 200)}...`); - return null; - } - }); -} - -// Get file size using HEAD request -function getFileSize(url) { - console.log(`[HDRezka] Getting file size for: ${url.substring(0, 60)}...`); - - return fetch(url, { - method: 'HEAD', - headers: { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' - } - }).then(function (response) { - if (response.ok) { - const contentLength = response.headers.get('content-length'); - if (contentLength) { - const bytes = parseInt(contentLength, 10); - const sizeFormatted = formatFileSize(bytes); - console.log(`[HDRezka] File size: ${sizeFormatted}`); - return sizeFormatted; - } - } - - console.log(`[HDRezka] Could not determine file size`); - return null; - }).catch(function (error) { - console.log(`[HDRezka] Error getting file size: ${error.message}`); - return null; - }); -} - -// Format file size in human readable format -function formatFileSize(bytes) { - if (bytes === 0) return '0 Bytes'; - - const k = 1024; - const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; -} - -// Parse quality for sorting -function parseQualityForSort(qualityString) { - if (!qualityString) return 0; - const match = qualityString.match(/(\d{3,4})p/i); - return match ? parseInt(match[1], 10) : 0; -} - -// Main function to get streams for TMDB content -function getStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) { - console.log(`[HDRezka] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}${seasonNum ? `, S${seasonNum}E${episodeNum}` : ''}`); - - // Get TMDB info - const tmdbUrl = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY}`; - return makeRequest(tmdbUrl).then(function (tmdbResponse) { - return tmdbResponse.json(); - }).then(function (tmdbData) { - const title = mediaType === 'tv' ? tmdbData.name : tmdbData.title; - const year = mediaType === 'tv' ? tmdbData.first_air_date?.substring(0, 4) : tmdbData.release_date?.substring(0, 4); - - if (!title) { - throw new Error('Could not extract title from TMDB response'); - } - - console.log(`[HDRezka] TMDB Info: "${title}" (${year})`); - - // Create media object - const media = { - type: mediaType === 'tv' ? 'tv' : 'movie', - title: title, - releaseYear: year ? parseInt(year) : null - }; - - // Add season/episode for TV shows - if (mediaType === 'tv') { - media.season = { number: seasonNum || 1 }; - media.episode = { number: episodeNum || 1 }; - } - - // Step 1: Search and find media ID - return searchAndFindMediaId(media).then(function (searchResult) { - if (!searchResult || !searchResult.id) { - console.log('[HDRezka] No search result found'); - return []; - } - - // Step 2: Get translator ID - return getTranslatorId(searchResult.url, searchResult.id, media).then(function (translatorId) { - if (!translatorId) { - console.log('[HDRezka] No translator ID found'); - return []; - } - - // Step 3: Get stream data - return getStreamData(searchResult.id, translatorId, media).then(function (streamData) { - if (!streamData || !streamData.qualities) { - console.log('[HDRezka] No stream data found'); - return []; - } - - // Convert to Nuvio stream format with size detection - const streamEntries = Object.entries(streamData.qualities); - const streamPromises = streamEntries - .filter(([quality, data]) => data.url && data.url !== 'null') - .map(([quality, data]) => { - const cleanQuality = quality.replace(/p.*$/, 'p'); // "1080p Ultra" -> "1080p" - - // Get file size using Promise chain - return getFileSize(data.url).then(function (fileSize) { - return { - name: "HDRezka", - title: `${title} ${year ? `(${year})` : ''} ${quality}${mediaType === 'tv' ? ` S${seasonNum}E${episodeNum}` : ''}`, - url: data.url, - quality: cleanQuality, - size: fileSize, - type: 'direct' - }; - }); - }); - - return Promise.all(streamPromises).then(function (streams) { - // Sort by quality (highest first) - optimized - if (streams.length > 1) { - streams.sort(function (a, b) { - const qualityA = parseQualityForSort(a.quality); - const qualityB = parseQualityForSort(b.quality); - return qualityB - qualityA; - }); - } - - console.log(`[HDRezka] Successfully processed ${streams.length} streams`); - return streams; - }); - }); - }); - }); - }).catch(function (error) { - console.error(`[HDRezka] Error in getStreams: ${error.message}`); - return []; - }); -} - -// Export the main function -if (typeof module !== 'undefined' && module.exports) { - module.exports = { getStreams }; -} else { - // For React Native environment - global.getStreams = getStreams; -} \ No newline at end of file diff --git a/local-scrapers-repo/providers/moviesmod.js b/local-scrapers-repo/providers/moviesmod.js deleted file mode 100644 index 973558a..0000000 --- a/local-scrapers-repo/providers/moviesmod.js +++ /dev/null @@ -1,857 +0,0 @@ -// MoviesMod Scraper for Nuvio Local Scrapers -// React Native compatible version with Cheerio support - -// Import cheerio-without-node-native for React Native -const cheerio = require('cheerio-without-node-native'); - -console.log('[MoviesMod] Using cheerio-without-node-native for DOM parsing'); - -// Escape regex special characters -function escapeRegExp(string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); -} - -// Constants -const TMDB_API_KEY = "439c478a771f35c05022f9feabcca01c"; -const FALLBACK_DOMAIN = 'https://moviesmod.chat'; -const DOMAIN_CACHE_TTL = 4 * 60 * 60 * 1000; // 4 hours - -// Global variables for domain caching -let moviesModDomain = FALLBACK_DOMAIN; -let domainCacheTimestamp = 0; - -// Fetch latest domain from GitHub -async function getMoviesModDomain() { - const now = Date.now(); - if (now - domainCacheTimestamp < DOMAIN_CACHE_TTL) { - return moviesModDomain; - } - - try { - console.log('[MoviesMod] Fetching latest domain...'); - const response = await fetch('https://raw.githubusercontent.com/phisher98/TVVVV/refs/heads/main/domains.json', { - method: 'GET', - headers: { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' - } - }); - - if (response.ok) { - const data = await response.json(); - if (data && data.moviesmod) { - moviesModDomain = data.moviesmod; - domainCacheTimestamp = now; - console.log(`[MoviesMod] Updated domain to: ${moviesModDomain}`); - } - } - } catch (error) { - console.error(`[MoviesMod] Failed to fetch latest domain: ${error.message}`); - } - - return moviesModDomain; -} - -// Helper function to make HTTP requests -async function makeRequest(url, options = {}) { - const defaultHeaders = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', - 'Accept-Language': 'en-US,en;q=0.5', - 'Accept-Encoding': 'gzip, deflate', - 'Connection': 'keep-alive', - 'Upgrade-Insecure-Requests': '1' - }; - - const response = await fetch(url, { - ...options, - headers: { - ...defaultHeaders, - ...options.headers - } - }); - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - - return response; -} - -// Helper function to extract quality from text -function extractQuality(text) { - if (!text) return 'Unknown'; - - const qualityMatch = text.match(/(480p|720p|1080p|2160p|4k)/i); - if (qualityMatch) { - return qualityMatch[1]; - } - - const cleanMatch = text.match(/(480p|720p|1080p|2160p|4k)[^)]*\)/i); - if (cleanMatch) { - return cleanMatch[0]; - } - - return 'Unknown'; -} - -// Parse quality for sorting -function parseQualityForSort(qualityString) { - if (!qualityString) return 0; - const match = qualityString.match(/(\d{3,4})p/i); - return match ? parseInt(match[1], 10) : 0; -} - -// Get technical details from quality string -function getTechDetails(qualityString) { - if (!qualityString) return []; - const details = []; - const lowerText = qualityString.toLowerCase(); - if (lowerText.includes('10bit')) details.push('10-bit'); - if (lowerText.includes('hevc') || lowerText.includes('x265')) details.push('HEVC'); - if (lowerText.includes('hdr')) details.push('HDR'); - return details; -} - -// Simple string similarity function -function findBestMatch(mainString, targetStrings) { - if (!targetStrings || targetStrings.length === 0) { - return { bestMatch: { target: '', rating: 0 }, bestMatchIndex: -1 }; - } - - const ratings = targetStrings.map(target => { - if (!target) return 0; - - const main = mainString.toLowerCase(); - const targ = target.toLowerCase(); - - if (main === targ) return 1; - if (targ.includes(main) || main.includes(targ)) return 0.8; - - // Simple word matching - const mainWords = main.split(/\s+/); - const targWords = targ.split(/\s+/); - let matches = 0; - - for (const word of mainWords) { - if (word.length > 2 && targWords.some(tw => tw.includes(word) || word.includes(tw))) { - matches++; - } - } - - return matches / Math.max(mainWords.length, targWords.length); - }); - - const bestRating = Math.max(...ratings); - const bestIndex = ratings.indexOf(bestRating); - - return { - bestMatch: { target: targetStrings[bestIndex], rating: bestRating }, - bestMatchIndex: bestIndex - }; -} - -// Search for content on MoviesMod -async function searchMoviesMod(query) { - try { - const baseUrl = await getMoviesModDomain(); - const searchUrl = `${baseUrl}/?s=${encodeURIComponent(query)}`; - console.log(`[MoviesMod] Searching: ${searchUrl}`); - - const response = await makeRequest(searchUrl); - const html = await response.text(); - const $ = cheerio.load(html); - - const results = []; - $('.latestPost').each((i, element) => { - const linkElement = $(element).find('a'); - const title = linkElement.attr('title'); - const url = linkElement.attr('href'); - if (title && url) { - results.push({ title, url }); - } - }); - - console.log(`[MoviesMod] Found ${results.length} search results`); - return results; - } catch (error) { - console.error(`[MoviesMod] Error searching: ${error.message}`); - return []; - } -} - -// Extract download links from a movie/series page -async function extractDownloadLinks(moviePageUrl) { - try { - const response = await makeRequest(moviePageUrl); - const html = await response.text(); - const $ = cheerio.load(html); - const links = []; - const contentBox = $('.thecontent'); - - // Get all relevant headers (for movies and TV shows) in document order - const headers = contentBox.find('h3:contains("Season"), h4'); - - headers.each((i, el) => { - const header = $(el); - const headerText = header.text().trim(); - - // Define the content block for this header - const blockContent = header.nextUntil('h3, h4'); - - if (header.is('h3') && headerText.toLowerCase().includes('season')) { - // TV Show Logic - const linkElements = blockContent.find('a.maxbutton-episode-links, a.maxbutton-batch-zip'); - linkElements.each((j, linkEl) => { - const buttonText = $(linkEl).text().trim(); - const linkUrl = $(linkEl).attr('href'); - if (linkUrl && !buttonText.toLowerCase().includes('batch')) { - links.push({ - quality: `${headerText} - ${buttonText}`, - url: linkUrl - }); - } - }); - } else if (header.is('h4')) { - // Movie Logic - const linkElement = blockContent.find('a[href*="modrefer.in"]').first(); - if (linkElement.length > 0) { - const link = linkElement.attr('href'); - const cleanQuality = extractQuality(headerText); - links.push({ - quality: cleanQuality, - url: link - }); - } - } - }); - - console.log(`[MoviesMod] Extracted ${links.length} download links`); - return links; - } catch (error) { - console.error(`[MoviesMod] Error extracting download links: ${error.message}`); - return []; - } -} - -// Resolve intermediate links -async function resolveIntermediateLink(initialUrl, refererUrl, quality) { - try { - const urlObject = new URL(initialUrl); - - if (urlObject.hostname.includes('modrefer.in')) { - const encodedUrl = urlObject.searchParams.get('url'); - if (!encodedUrl) { - console.error('[MoviesMod] Could not find encoded URL in modrefer.in link.'); - return []; - } - - // Use atob for base64 decoding (React Native compatible) - const decodedUrl = atob(encodedUrl); - console.log(`[MoviesMod] Decoded modrefer URL: ${decodedUrl}`); - - const response = await makeRequest(decodedUrl, { - headers: { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', - 'Referer': refererUrl, - } - }); - const html = await response.text(); - const $ = cheerio.load(html); - const finalLinks = []; - - // Debug: Check what content is available on the page - console.log(`[MoviesMod] Page title: ${$('title').text()}`); - console.log(`[MoviesMod] Total links on page: ${$('a').length}`); - console.log(`[MoviesMod] HTML length: ${html.length} characters`); - - // Look for timed content links (this is the key part from OG) - $('.timed-content-client_show_0_5_0 a').each((i, el) => { - const link = $(el).attr('href'); - const text = $(el).text().trim(); - if (link) { - finalLinks.push({ - server: text, - url: link, - }); - } - }); - - // If no timed content found, look for any driveseed or tech links - if (finalLinks.length === 0) { - console.log(`[MoviesMod] No timed content found, looking for direct links...`); - $('a').each((i, el) => { - const link = $(el).attr('href'); - const text = $(el).text().trim(); - if (link && (link.includes('driveseed.org') || link.includes('tech.unblockedgames.world') || link.includes('tech.examzculture.in') || link.includes('tech.creativeexpressionsblog.com') || link.includes('tech.examdegree.site'))) { - console.log(`[MoviesMod] Found direct link: ${text} -> ${link}`); - finalLinks.push({ - server: text || 'Download Link', - url: link, - }); - } - }); - } - - // Also look for any additional download buttons or links that might be hidden - if (finalLinks.length === 0) { - console.log(`[MoviesMod] Looking for alternative download patterns...`); - $('button, .download-btn, .btn, [class*="download"], [class*="btn"]').each((i, el) => { - const $el = $(el); - const link = $el.attr('href') || $el.attr('data-href') || $el.find('a').attr('href'); - const text = $el.text().trim(); - if (link && (link.includes('driveseed.org') || link.includes('tech.unblockedgames.world') || link.includes('tech.examzculture.in') || link.includes('tech.creativeexpressionsblog.com') || link.includes('tech.examdegree.site'))) { - console.log(`[MoviesMod] Found alternative link: ${text} -> ${link}`); - finalLinks.push({ - server: text || 'Alternative Download', - url: link, - }); - } - }); - } - - console.log(`[MoviesMod] Found ${finalLinks.length} total links`); - return finalLinks; - } - - return []; - } catch (error) { - console.error(`[MoviesMod] Error resolving intermediate link: ${error.message}`); - return []; - } -} - -// Resolve tech.unblockedgames.world SID links to driveleech URLs -async function resolveTechUnblockedLink(sidUrl) { - console.log(`[MoviesMod] Resolving SID link: ${sidUrl}`); - - try { - // Step 1: Get the initial page - const response = await makeRequest(sidUrl); - const html = await response.text(); - const $ = cheerio.load(html); - - const initialForm = $('#landing'); - const wp_http_step1 = initialForm.find('input[name="_wp_http"]').val(); - const action_url_step1 = initialForm.attr('action'); - - if (!wp_http_step1 || !action_url_step1) { - console.error(" [SID] Error: Could not find _wp_http in initial form."); - return null; - } - - // Step 2: POST to the first form's action URL - const step1Data = new URLSearchParams({ '_wp_http': wp_http_step1 }); - const responseStep1 = await makeRequest(action_url_step1, { - method: 'POST', - headers: { - 'Referer': sidUrl, - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: step1Data.toString() - }); - - // Step 3: Parse verification page for second form - const html2 = await responseStep1.text(); - const $2 = cheerio.load(html2); - const verificationForm = $2('#landing'); - const action_url_step2 = verificationForm.attr('action'); - const wp_http2 = verificationForm.find('input[name="_wp_http2"]').val(); - const token = verificationForm.find('input[name="token"]').val(); - - if (!action_url_step2) { - console.error(" [SID] Error: Could not find verification form."); - return null; - } - - // Step 4: POST to the verification URL - const step2Data = new URLSearchParams({ '_wp_http2': wp_http2, 'token': token }); - const responseStep2 = await makeRequest(action_url_step2, { - method: 'POST', - headers: { - 'Referer': responseStep1.url, - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: step2Data.toString() - }); - - // Step 5: Find dynamic cookie and link from JavaScript - const finalHtml = await responseStep2.text(); - let finalLinkPath = null; - let cookieName = null; - let cookieValue = null; - - const cookieMatch = finalHtml.match(/s_343\('([^']+)',\s*'([^']+)'/); - const linkMatch = finalHtml.match(/c\.setAttribute\("href",\s*"([^"]+)"\)/); - - if (cookieMatch) { - cookieName = cookieMatch[1].trim(); - cookieValue = cookieMatch[2].trim(); - } - if (linkMatch) { - finalLinkPath = linkMatch[1].trim(); - } - - if (!finalLinkPath || !cookieName || !cookieValue) { - console.error(" [SID] Error: Could not extract dynamic cookie/link from JS."); - return null; - } - - const { origin } = new URL(sidUrl); - const finalUrl = new URL(finalLinkPath, origin).href; - - // Step 6: Make final request with cookie - const finalResponse = await makeRequest(finalUrl, { - headers: { - 'Referer': responseStep2.url, - 'Cookie': `${cookieName}=${cookieValue}` - } - }); - - // Step 7: Extract driveleech URL from meta refresh tag - const metaHtml = await finalResponse.text(); - const $3 = cheerio.load(metaHtml); - const metaRefresh = $3('meta[http-equiv="refresh"]'); - - if (metaRefresh.length > 0) { - const content = metaRefresh.attr('content'); - const urlMatch = content.match(/url=(.*)/i); - if (urlMatch && urlMatch[1]) { - const driveleechUrl = urlMatch[1].replace(/"/g, "").replace(/'/g, ""); - console.log(` [SID] SUCCESS! Resolved Driveleech URL: ${driveleechUrl}`); - return driveleechUrl; - } - } - - console.error(" [SID] Error: Could not find meta refresh tag with Driveleech URL."); - return null; - - } catch (error) { - console.error(` [SID] Error during SID resolution: ${error.message}`); - return null; - } -} - -// Resolve driveseed.org links to get download options -async function resolveDriveseedLink(driveseedUrl) { - try { - const response = await makeRequest(driveseedUrl, { - headers: { - 'Referer': 'https://links.modpro.blog/', - } - }); - const html = await response.text(); - - const redirectMatch = html.match(/window\.location\.replace\("([^"]+)"\)/); - - if (redirectMatch && redirectMatch[1]) { - const finalPath = redirectMatch[1]; - const finalUrl = `https://driveseed.org${finalPath}`; - - const finalResponse = await makeRequest(finalUrl, { - headers: { - 'Referer': driveseedUrl, - } - }); - const finalHtml = await finalResponse.text(); - const $ = cheerio.load(finalHtml); - - const downloadOptions = []; - let size = null; - let fileName = null; - - // Extract size and filename from the list - $('ul.list-group li').each((i, el) => { - const text = $(el).text(); - if (text.includes('Size :')) { - size = text.split(':')[1].trim(); - } else if (text.includes('Name :')) { - fileName = text.split(':')[1].trim(); - } - }); - - // Find Resume Cloud button (primary) - const resumeCloudLink = $('a:contains("Resume Cloud")').attr('href'); - if (resumeCloudLink) { - downloadOptions.push({ - title: 'Resume Cloud', - type: 'resume', - url: `https://driveseed.org${resumeCloudLink}`, - priority: 1 - }); - } - - // Find Resume Worker Bot (fallback) - const workerSeedLink = $('a:contains("Resume Worker Bot")').attr('href'); - if (workerSeedLink) { - downloadOptions.push({ - title: 'Resume Worker Bot', - type: 'worker', - url: workerSeedLink, - priority: 2 - }); - } - - // Find any other download links as additional fallbacks - $('a[href*="/download/"]').each((i, el) => { - const href = $(el).attr('href'); - const text = $(el).text().trim(); - if (href && text && !downloadOptions.some(opt => opt.url === href)) { - downloadOptions.push({ - title: text, - type: 'generic', - url: href.startsWith('http') ? href : `https://driveseed.org${href}`, - priority: 4 - }); - } - }); - - // Find Instant Download (final fallback) - const instantDownloadLink = $('a:contains("Instant Download")').attr('href'); - if (instantDownloadLink) { - downloadOptions.push({ - title: 'Instant Download', - type: 'instant', - url: instantDownloadLink, - priority: 3 - }); - } - - // Sort by priority - downloadOptions.sort((a, b) => a.priority - b.priority); - return { downloadOptions, size, fileName }; - } - return { downloadOptions: [], size: null, fileName: null }; - } catch (error) { - console.error(`[MoviesMod] Error resolving Driveseed link: ${error.message}`); - return { downloadOptions: [], size: null, fileName: null }; - } -} - -// Resolve Resume Cloud link to final download URL -async function resolveResumeCloudLink(resumeUrl) { - try { - const response = await makeRequest(resumeUrl, { - headers: { - 'Referer': 'https://driveseed.org/', - } - }); - const html = await response.text(); - const $ = cheerio.load(html); - const downloadLink = $('a:contains("Cloud Resume Download")').attr('href'); - return downloadLink || null; - } catch (error) { - console.error(`[MoviesMod] Error resolving Resume Cloud link: ${error.message}`); - return null; - } -} - -// Resolve Video Seed (Instant Download) link -async function resolveVideoSeedLink(videoSeedUrl) { - try { - const urlParams = new URLSearchParams(new URL(videoSeedUrl).search); - const keys = urlParams.get('url'); - - if (keys) { - const apiUrl = `${new URL(videoSeedUrl).origin}/api`; - const formData = new URLSearchParams(); - formData.append('keys', keys); - - const apiResponse = await fetch(apiUrl, { - method: 'POST', - body: formData, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'x-token': new URL(videoSeedUrl).hostname, - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' - } - }); - - if (apiResponse.ok) { - const responseData = await apiResponse.json(); - if (responseData && responseData.url) { - return responseData.url; - } - } - } - return null; - } catch (error) { - console.error(`[MoviesMod] Error resolving VideoSeed link: ${error.message}`); - return null; - } -} - -// Validate if a video URL is working -async function validateVideoUrl(url, timeout = 10000) { - try { - console.log(`[MoviesMod] Validating URL: ${url.substring(0, 100)}...`); - const response = await fetch(url, { - method: 'HEAD', - headers: { - 'Range': 'bytes=0-1', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' - } - }); - - if (response.ok || response.status === 206) { - console.log(`[MoviesMod] ✓ URL validation successful (${response.status})`); - return true; - } else { - console.log(`[MoviesMod] ✗ URL validation failed with status: ${response.status}`); - return false; - } - } catch (error) { - console.log(`[MoviesMod] ✗ URL validation failed: ${error.message}`); - return false; - } -} - -// Process a single download link to get final stream -async function processDownloadLink(link, selectedResult, mediaType, episodeNum) { - try { - console.log(`[MoviesMod] Processing quality: ${link.quality}`); - - const finalLinks = await resolveIntermediateLink(link.url, selectedResult.url, link.quality); - if (!finalLinks || finalLinks.length === 0) { - console.log(`[MoviesMod] No final links found for ${link.quality}`); - return null; - } - - // Filter for specific episode if needed - let targetLinks = finalLinks; - if ((mediaType === 'tv' || mediaType === 'series') && episodeNum !== null) { - targetLinks = finalLinks.filter(targetLink => { - const serverName = targetLink.server.toLowerCase(); - const episodePatterns = [ - new RegExp(`episode\\s+${episodeNum}\\b`, 'i'), - new RegExp(`ep\\s+${episodeNum}\\b`, 'i'), - new RegExp(`e${episodeNum}\\b`, 'i'), - new RegExp(`\\b${episodeNum}\\b`) - ]; - - return episodePatterns.some(pattern => pattern.test(serverName)); - }); - - if (targetLinks.length === 0) { - console.log(`[MoviesMod] No episode ${episodeNum} found for ${link.quality}`); - return null; - } - } - - // Process each target link - for (const targetLink of targetLinks) { - try { - let currentUrl = targetLink.url; - - // Handle SID links if they appear - if (currentUrl && (currentUrl.includes('tech.unblockedgames.world') || currentUrl.includes('tech.creativeexpressionsblog.com') || currentUrl.includes('tech.examzculture.in') || currentUrl.includes('tech.examdegree.site'))) { - console.log(`[MoviesMod] Resolving SID link: ${targetLink.server}`); - const resolvedUrl = await resolveTechUnblockedLink(currentUrl); - if (!resolvedUrl) { - console.log(`[MoviesMod] Failed to resolve SID link for ${targetLink.server}`); - continue; - } - - // Skip broken link report pages - if (resolvedUrl.includes('report-broken-links') || resolvedUrl.includes('moviesmod.wiki')) { - console.log(`[MoviesMod] Skipping broken link report page for ${targetLink.server}`); - continue; - } - - currentUrl = resolvedUrl; - } - - if (currentUrl && currentUrl.includes('driveseed.org')) { - const { downloadOptions, size, fileName } = await resolveDriveseedLink(currentUrl); - - if (!downloadOptions || downloadOptions.length === 0) { - console.log(`[MoviesMod] No download options found for ${targetLink.server} - ${currentUrl}`); - continue; - } - - // Try download methods in order of priority - let finalDownloadUrl = null; - let usedMethod = null; - - for (const option of downloadOptions) { - try { - console.log(`[MoviesMod] Trying ${option.title} for ${link.quality}...`); - - if (option.type === 'resume') { - finalDownloadUrl = await resolveResumeCloudLink(option.url); - } else if (option.type === 'instant') { - finalDownloadUrl = await resolveVideoSeedLink(option.url); - } - - if (finalDownloadUrl) { - // Check if URL validation is enabled - if (typeof URL_VALIDATION_ENABLED !== 'undefined' && !URL_VALIDATION_ENABLED) { - usedMethod = option.title; - console.log(`[MoviesMod] ✓ URL validation disabled, accepting ${usedMethod} result`); - break; - } - - const isValid = await validateVideoUrl(finalDownloadUrl); - if (isValid) { - usedMethod = option.title; - console.log(`[MoviesMod] ✓ Successfully resolved using ${usedMethod}`); - break; - } else { - console.log(`[MoviesMod] ✗ ${option.title} returned invalid URL`); - finalDownloadUrl = null; - } - } - } catch (error) { - console.log(`[MoviesMod] ✗ ${option.title} failed: ${error.message}`); - } - } - - if (finalDownloadUrl) { - const actualQuality = extractQuality(link.quality); - const sizeInfo = size || link.quality.match(/\[([^\]]+)\]/)?.[1]; - const cleanFileName = fileName ? fileName.replace(/\.[^/.]+$/, "").replace(/[._]/g, ' ') : `Stream from ${link.quality}`; - const techDetails = getTechDetails(link.quality); - const techDetailsString = techDetails.length > 0 ? ` • ${techDetails.join(' • ')}` : ''; - - return { - name: `MoviesMod`, - title: `${cleanFileName}\n${sizeInfo || ''}${techDetailsString}`, - url: finalDownloadUrl, - quality: actualQuality, - size: sizeInfo, - fileName: fileName, - type: 'direct' - }; - } - } - } catch (error) { - console.error(`[MoviesMod] Error processing target link: ${error.message}`); - } - } - return null; - } catch (error) { - console.error(`[MoviesMod] Error processing quality ${link.quality}: ${error.message}`); - return null; - } -} - -// Main function to get streams for TMDB content -async function getStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) { - console.log(`[MoviesMod] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}${seasonNum ? `, S${seasonNum}E${episodeNum}` : ''}`); - - try { - // Get TMDB info - const tmdbUrl = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY}`; - const tmdbResponse = await makeRequest(tmdbUrl); - const tmdbData = await tmdbResponse.json(); - - const title = mediaType === 'tv' ? tmdbData.name : tmdbData.title; - const year = mediaType === 'tv' ? tmdbData.first_air_date?.substring(0, 4) : tmdbData.release_date?.substring(0, 4); - - if (!title) { - throw new Error('Could not extract title from TMDB response'); - } - - console.log(`[MoviesMod] TMDB Info: "${title}" (${year})`); - - // Search for the media - const searchResults = await searchMoviesMod(title); - if (searchResults.length === 0) { - console.log(`[MoviesMod] No search results found`); - return []; - } - - // Use string similarity to find the best match - const titles = searchResults.map(r => r.title); - const bestMatch = findBestMatch(title, titles); - - console.log(`[MoviesMod] Best match for "${title}" is "${bestMatch.bestMatch.target}" with a rating of ${bestMatch.bestMatch.rating.toFixed(2)}`); - - let selectedResult = null; - if (bestMatch.bestMatch.rating > 0.3) { - selectedResult = searchResults[bestMatch.bestMatchIndex]; - - // Additional check for year if it's a movie - if (mediaType === 'movie' && year) { - if (!selectedResult.title.includes(year)) { - console.warn(`[MoviesMod] Title match found, but year mismatch. Matched: "${selectedResult.title}", Expected year: ${year}. Discarding match.`); - selectedResult = null; - } - } - } - - if (!selectedResult) { - // Try stricter search - console.log('[MoviesMod] Similarity match failed. Trying stricter search...'); - const titleRegex = new RegExp(`\\b${escapeRegExp(title.toLowerCase())}\\b`); - - if (mediaType === 'movie') { - selectedResult = searchResults.find(r => - titleRegex.test(r.title.toLowerCase()) && - (!year || r.title.includes(year)) - ); - } else { - selectedResult = searchResults.find(r => - titleRegex.test(r.title.toLowerCase()) && - r.title.toLowerCase().includes('season') - ); - } - } - - if (!selectedResult) { - console.log(`[MoviesMod] No suitable search result found for "${title} (${year})"`); - return []; - } - - console.log(`[MoviesMod] Selected: ${selectedResult.title}`); - - // Extract download links - const downloadLinks = await extractDownloadLinks(selectedResult.url); - if (downloadLinks.length === 0) { - console.log(`[MoviesMod] No download links found`); - return []; - } - - let relevantLinks = downloadLinks; - if ((mediaType === 'tv' || mediaType === 'series') && seasonNum !== null) { - relevantLinks = downloadLinks.filter(link => - link.quality.toLowerCase().includes(`season ${seasonNum}`) || - link.quality.toLowerCase().includes(`s${seasonNum}`) - ); - } - - // Filter out 480p links - relevantLinks = relevantLinks.filter(link => !link.quality.toLowerCase().includes('480p')); - console.log(`[MoviesMod] ${relevantLinks.length} links remaining after 480p filter.`); - - if (relevantLinks.length === 0) { - console.log(`[MoviesMod] No relevant links found after filtering`); - return []; - } - - // Process links to get final streams - using Promise.all like UHDMovies - const streamPromises = relevantLinks.map(link => processDownloadLink(link, selectedResult, mediaType, episodeNum)); - const streams = (await Promise.all(streamPromises)).filter(Boolean); - - // Sort by quality descending - streams.sort((a, b) => { - const qualityA = parseQualityForSort(a.quality); - const qualityB = parseQualityForSort(b.quality); - return qualityB - qualityA; - }); - - console.log(`[MoviesMod] Successfully processed ${streams.length} streams`); - return streams; - - } catch (error) { - console.error(`[MoviesMod] Error in getStreams: ${error.message}`); - return []; - } -} - -// Export the main function -if (typeof module !== 'undefined' && module.exports) { - module.exports = { getStreams }; -} else { - // For React Native environment - global.getStreams = getStreams; -} \ No newline at end of file diff --git a/local-scrapers-repo/providers/myflixer-extractor.js b/local-scrapers-repo/providers/myflixer-extractor.js deleted file mode 100644 index 5494a98..0000000 --- a/local-scrapers-repo/providers/myflixer-extractor.js +++ /dev/null @@ -1,471 +0,0 @@ -#!/usr/bin/env node - -const axios = require('axios'); -const cheerio = require('cheerio'); -const { URL } = require('url'); - -class MyFlixerExtractor { - constructor() { - this.mainUrl = 'https://watch32.sx'; - this.videostrUrl = 'https://videostr.net'; - } - - async search(query) { - try { - const searchUrl = `${this.mainUrl}/search/${query.replace(/\s+/g, '-')}`; - console.log(`Searching: ${searchUrl}`); - - const response = await axios.get(searchUrl); - const $ = cheerio.load(response.data); - - const results = []; - $('.flw-item').each((i, element) => { - const title = $(element).find('h2.film-name > a').attr('title'); - const link = $(element).find('h2.film-name > a').attr('href'); - const poster = $(element).find('img.film-poster-img').attr('data-src'); - - if (title && link) { - results.push({ - title, - url: link.startsWith('http') ? link : `${this.mainUrl}${link}`, - poster - }); - } - }); - - console.log('Search results found:'); - results.forEach((result, index) => { - console.log(`${index + 1}. ${result.title}`); - }); - - return results; - } catch (error) { - console.error('Search error:', error.message); - return []; - } - } - - async getContentDetails(url) { - try { - console.log(`Getting content details: ${url}`); - const response = await axios.get(url); - const $ = cheerio.load(response.data); - - const contentId = $('.detail_page-watch').attr('data-id'); - const name = $('.detail_page-infor h2.heading-name > a').text(); - const isMovie = url.includes('movie'); - - if (isMovie) { - return { - type: 'movie', - name, - data: `list/${contentId}` - }; - } else { - // Get TV series episodes - const episodes = []; - const seasonsResponse = await axios.get(`${this.mainUrl}/ajax/season/list/${contentId}`); - const $seasons = cheerio.load(seasonsResponse.data); - - for (const season of $seasons('a.ss-item').toArray()) { - const seasonId = $(season).attr('data-id'); - const seasonNum = $(season).text().replace('Season ', ''); - - const episodesResponse = await axios.get(`${this.mainUrl}/ajax/season/episodes/${seasonId}`); - const $episodes = cheerio.load(episodesResponse.data); - - $episodes('a.eps-item').each((i, episode) => { - const epId = $(episode).attr('data-id'); - const title = $(episode).attr('title'); - const match = title.match(/Eps (\d+): (.+)/); - - if (match) { - episodes.push({ - id: epId, - episode: parseInt(match[1]), - name: match[2], - season: parseInt(seasonNum.replace('Series', '').trim()), - data: `servers/${epId}` - }); - } - }); - } - - return { - type: 'series', - name, - episodes - }; - } - } catch (error) { - console.error('Content details error:', error.message); - return null; - } - } - - async getServerLinks(data) { - try { - console.log(`Getting server links: ${data}`); - const response = await axios.get(`${this.mainUrl}/ajax/episode/${data}`); - const $ = cheerio.load(response.data); - - const servers = []; - $('a.link-item').each((i, element) => { - const linkId = $(element).attr('data-linkid') || $(element).attr('data-id'); - if (linkId) { - servers.push(linkId); - } - }); - - return servers; - } catch (error) { - console.error('Server links error:', error.message); - return []; - } - } - - async getSourceUrl(linkId) { - try { - console.log(`Getting source URL for linkId: ${linkId}`); - const response = await axios.get(`${this.mainUrl}/ajax/episode/sources/${linkId}`); - return response.data.link; - } catch (error) { - console.error('Source URL error:', error.message); - return null; - } - } - - async extractVideostrM3u8(url) { - try { - console.log(`Extracting from Videostr: ${url}`); - - const headers = { - 'Accept': '*/*', - 'X-Requested-With': 'XMLHttpRequest', - 'Referer': this.videostrUrl, - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' - }; - - // Extract ID from URL - const id = url.split('/').pop().split('?')[0]; - - // Get nonce from embed page - const embedResponse = await axios.get(url, { headers }); - const embedHtml = embedResponse.data; - - // Try to find 48-character nonce - let nonce = embedHtml.match(/\b[a-zA-Z0-9]{48}\b/); - if (nonce) { - nonce = nonce[0]; - } else { - // Try to find three 16-character segments - const matches = embedHtml.match(/\b([a-zA-Z0-9]{16})\b.*?\b([a-zA-Z0-9]{16})\b.*?\b([a-zA-Z0-9]{16})\b/); - if (matches) { - nonce = matches[1] + matches[2] + matches[3]; - } - } - - if (!nonce) { - throw new Error('Could not extract nonce'); - } - - console.log(`Extracted nonce: ${nonce}`); - - // Get sources from API - const apiUrl = `${this.videostrUrl}/embed-1/v3/e-1/getSources?id=${id}&_k=${nonce}`; - console.log(`API URL: ${apiUrl}`); - - const sourcesResponse = await axios.get(apiUrl, { headers }); - const sourcesData = sourcesResponse.data; - - if (!sourcesData.sources) { - throw new Error('No sources found in response'); - } - - let m3u8Url = sourcesData.sources; - - // Check if sources is already an M3U8 URL - if (!m3u8Url.includes('.m3u8')) { - console.log('Sources are encrypted, attempting to decrypt...'); - - // Get decryption key - const keyResponse = await axios.get('https://raw.githubusercontent.com/yogesh-hacker/MegacloudKeys/refs/heads/main/keys.json'); - const key = keyResponse.data.vidstr; - - if (!key) { - throw new Error('Could not get decryption key'); - } - - // Decrypt using Google Apps Script - const decodeUrl = 'https://script.google.com/macros/s/AKfycbx-yHTwupis_JD0lNzoOnxYcEYeXmJZrg7JeMxYnEZnLBy5V0--UxEvP-y9txHyy1TX9Q/exec'; - const fullUrl = `${decodeUrl}?encrypted_data=${encodeURIComponent(m3u8Url)}&nonce=${encodeURIComponent(nonce)}&secret=${encodeURIComponent(key)}`; - - const decryptResponse = await axios.get(fullUrl); - const decryptedData = decryptResponse.data; - - // Extract file URL from decrypted response - const fileMatch = decryptedData.match(/"file":"(.*?)"/); - if (fileMatch) { - m3u8Url = fileMatch[1]; - } else { - throw new Error('Could not extract video URL from decrypted response'); - } - } - - console.log(`Final M3U8 URL: ${m3u8Url}`); - - // Filter only megacdn links - if (!m3u8Url.includes('megacdn.co')) { - console.log('Skipping non-megacdn link'); - return null; - } - - // Parse master playlist to extract quality streams - const qualities = await this.parseM3U8Qualities(m3u8Url); - - return { - m3u8Url, - qualities, - headers: { - 'Referer': 'https://videostr.net/', - 'Origin': 'https://videostr.net/' - } - }; - - } catch (error) { - console.error('Videostr extraction error:', error.message); - return null; - } - } - - async parseM3U8Qualities(masterUrl) { - try { - const response = await axios.get(masterUrl, { - headers: { - 'Referer': 'https://videostr.net/', - 'Origin': 'https://videostr.net/' - } - }); - - const playlist = response.data; - const qualities = []; - - // Parse M3U8 master playlist - const lines = playlist.split('\n'); - for (let i = 0; i < lines.length; i++) { - const line = lines[i].trim(); - if (line.startsWith('#EXT-X-STREAM-INF:')) { - const nextLine = lines[i + 1]?.trim(); - if (nextLine && !nextLine.startsWith('#')) { - // Extract resolution and bandwidth - const resolutionMatch = line.match(/RESOLUTION=(\d+x\d+)/); - const bandwidthMatch = line.match(/BANDWIDTH=(\d+)/); - - const resolution = resolutionMatch ? resolutionMatch[1] : 'Unknown'; - const bandwidth = bandwidthMatch ? parseInt(bandwidthMatch[1]) : 0; - - // Determine quality label - let quality = 'Unknown'; - if (resolution.includes('1920x1080')) quality = '1080p'; - else if (resolution.includes('1280x720')) quality = '720p'; - else if (resolution.includes('640x360')) quality = '360p'; - else if (resolution.includes('854x480')) quality = '480p'; - - qualities.push({ - quality, - resolution, - bandwidth, - url: nextLine.startsWith('http') ? nextLine : new URL(nextLine, masterUrl).href - }); - } - } - } - - // Sort by bandwidth (highest first) - qualities.sort((a, b) => b.bandwidth - a.bandwidth); - - return qualities; - } catch (error) { - console.error('Error parsing M3U8 qualities:', error.message); - return []; - } - } - - async extractM3u8Links(query, episodeNumber = null, seasonNumber = null) { - try { - // Search for content - const searchResults = await this.search(query); - if (searchResults.length === 0) { - console.log('No search results found'); - return []; - } - - console.log(`Found ${searchResults.length} results`); - - // Try to find exact match first, then partial match - let selectedResult = searchResults.find(result => - result.title.toLowerCase() === query.toLowerCase() - ); - - if (!selectedResult) { - // Look for best partial match (contains all words from query) - const queryWords = query.toLowerCase().split(' '); - selectedResult = searchResults.find(result => { - const titleLower = result.title.toLowerCase(); - return queryWords.every(word => titleLower.includes(word)); - }); - } - - // Fallback to first result if no good match found - if (!selectedResult) { - selectedResult = searchResults[0]; - } - - console.log(`Selected: ${selectedResult.title}`); - - // Get content details - const contentDetails = await this.getContentDetails(selectedResult.url); - if (!contentDetails) { - console.log('Could not get content details'); - return []; - } - - let dataToProcess = []; - - if (contentDetails.type === 'movie') { - dataToProcess.push(contentDetails.data); - } else { - // For TV series, filter by episode/season if specified - let episodes = contentDetails.episodes; - - if (seasonNumber) { - episodes = episodes.filter(ep => ep.season === seasonNumber); - } - - if (episodeNumber) { - episodes = episodes.filter(ep => ep.episode === episodeNumber); - } - - if (episodes.length === 0) { - console.log('No matching episodes found'); - return []; - } - - // Use first matching episode or all if no specific episode requested - const targetEpisode = episodeNumber ? episodes[0] : episodes[0]; - console.log(`Selected episode: S${targetEpisode.season}E${targetEpisode.episode} - ${targetEpisode.name}`); - dataToProcess.push(targetEpisode.data); - } - - const allM3u8Links = []; - - // Process all data in parallel - const allPromises = []; - - for (const data of dataToProcess) { - // Get server links - const serverLinksPromise = this.getServerLinks(data).then(async (serverLinks) => { - console.log(`Found ${serverLinks.length} servers`); - - // Process all server links in parallel - const linkPromises = serverLinks.map(async (linkId) => { - try { - // Get source URL - const sourceUrl = await this.getSourceUrl(linkId); - if (!sourceUrl) return null; - - console.log(`Source URL: ${sourceUrl}`); - - // Check if it's a videostr URL - if (sourceUrl.includes('videostr.net')) { - const result = await this.extractVideostrM3u8(sourceUrl); - if (result) { - return { - source: 'videostr', - m3u8Url: result.m3u8Url, - qualities: result.qualities, - headers: result.headers - }; - } - } - return null; - } catch (error) { - console.error(`Error processing link ${linkId}:`, error.message); - return null; - } - }); - - return Promise.all(linkPromises); - }); - - allPromises.push(serverLinksPromise); - } - - // Wait for all promises to complete - const results = await Promise.all(allPromises); - - // Flatten and filter results - for (const serverResults of results) { - for (const result of serverResults) { - if (result) { - allM3u8Links.push(result); - } - } - } - - return allM3u8Links; - - } catch (error) { - console.error('Extraction error:', error.message); - return []; - } - } -} - -// CLI usage -if (require.main === module) { - const args = process.argv.slice(2); - - if (args.length === 0) { - console.log('Usage: node myflixer-extractor.js "" [episode] [season]'); - console.log('Examples:'); - console.log(' node myflixer-extractor.js "Avengers Endgame"'); - console.log(' node myflixer-extractor.js "Breaking Bad" 1 1 # Season 1, Episode 1'); - process.exit(1); - } - - const query = args[0]; - const episode = args[1] ? parseInt(args[1]) : null; - const season = args[2] ? parseInt(args[2]) : null; - - const extractor = new MyFlixerExtractor(); - - extractor.extractM3u8Links(query, episode, season) - .then(links => { - if (links.length === 0) { - console.log('No M3U8 links found'); - } else { - console.log('\n=== EXTRACTED M3U8 LINKS ==='); - links.forEach((link, index) => { - console.log(`\nLink ${index + 1}:`); - console.log(`Source: ${link.source}`); - console.log(`Master M3U8 URL: ${link.m3u8Url}`); - console.log(`Headers: ${JSON.stringify(link.headers, null, 2)}`); - - if (link.qualities && link.qualities.length > 0) { - console.log('Available Qualities:'); - link.qualities.forEach((quality, qIndex) => { - console.log(` ${qIndex + 1}. ${quality.quality} (${quality.resolution}) - ${Math.round(quality.bandwidth/1000)}kbps`); - console.log(` URL: ${quality.url}`); - }); - } - }); - } - }) - .catch(error => { - console.error('Error:', error.message); - process.exit(1); - }); -} - -module.exports = MyFlixerExtractor; \ No newline at end of file diff --git a/local-scrapers-repo/providers/netmirror.js b/local-scrapers-repo/providers/netmirror.js deleted file mode 100644 index 9c55f57..0000000 --- a/local-scrapers-repo/providers/netmirror.js +++ /dev/null @@ -1,740 +0,0 @@ -// NetMirror Scraper for Nuvio Local Scrapers -// React Native compatible version - No async/await for sandbox compatibility -// Fetches streaming links from net2025.cc for Netflix, Prime Video, and Disney+ content - -console.log('[NetMirror] Initializing NetMirror provider'); - -// Constants -const TMDB_API_KEY = "439c478a771f35c05022f9feabcca01c"; -const NETMIRROR_BASE = 'https://a.net2025.cc'; -const BASE_HEADERS = { - 'X-Requested-With': 'XMLHttpRequest', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', - 'Accept': 'application/json, text/plain, */*', - 'Accept-Language': 'en-US,en;q=0.5', - 'Connection': 'keep-alive' -}; - -// Global cookie storage -let globalCookie = ''; -let cookieTimestamp = 0; -const COOKIE_EXPIRY = 54000000; // 15 hours in milliseconds - -// Helper function to make HTTP requests -function makeRequest(url, options = {}) { - return fetch(url, { - ...options, - headers: { - ...BASE_HEADERS, - ...options.headers - }, - timeout: 10000 - }).then(function (response) { - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - return response; - }); -} - -// Get current Unix timestamp -function getUnixTime() { - return Math.floor(Date.now() / 1000); -} - -// Bypass authentication and get valid cookie -function bypass() { - // Check if we have a valid cached cookie - const now = Date.now(); - if (globalCookie && cookieTimestamp && (now - cookieTimestamp) < COOKIE_EXPIRY) { - console.log('[NetMirror] Using cached authentication cookie'); - return Promise.resolve(globalCookie); - } - - console.log('[NetMirror] Bypassing authentication...'); - - function attemptBypass(attempts) { - if (attempts >= 5) { - throw new Error('Max bypass attempts reached'); - } - - return makeRequest(`${NETMIRROR_BASE}/tv/p.php`, { - method: 'POST', - headers: BASE_HEADERS - }).then(function (response) { - // Extract cookie from response headers before reading text - const setCookieHeader = response.headers.get('set-cookie'); - let extractedCookie = null; - - if (setCookieHeader && (typeof setCookieHeader === 'string' || Array.isArray(setCookieHeader))) { - const cookieString = Array.isArray(setCookieHeader) ? setCookieHeader.join('; ') : setCookieHeader; - const cookieMatch = cookieString.match(/t_hash_t=([^;]+)/); - if (cookieMatch) { - extractedCookie = cookieMatch[1]; - } - } - - return response.text().then(function (responseText) { - // Check if response contains success indicator - if (!responseText.includes('"r":"n"')) { - console.log(`[NetMirror] Bypass attempt ${attempts + 1} failed, retrying...`); - return attemptBypass(attempts + 1); - } - - if (extractedCookie) { - globalCookie = extractedCookie; - cookieTimestamp = Date.now(); - console.log('[NetMirror] Authentication successful'); - return globalCookie; - } - - throw new Error('Failed to extract authentication cookie'); - }); - }); - } - - return attemptBypass(0); -} - -// Search for content on specific platform -function searchContent(query, platform) { - console.log(`[NetMirror] Searching for "${query}" on ${platform}...`); - - const ottMap = { - 'netflix': 'nf', - 'primevideo': 'pv', - 'disney': 'hs' - }; - - const ott = ottMap[platform.toLowerCase()] || 'nf'; - - return bypass().then(function (cookie) { - const cookies = { - 't_hash_t': cookie, - 'hd': 'on', - 'ott': ott - }; - - const cookieString = Object.entries(cookies) - .map(([key, value]) => `${key}=${value}`) - .join('; '); - - // Platform-specific search endpoints - const searchEndpoints = { - 'netflix': `${NETMIRROR_BASE}/search.php`, - 'primevideo': `${NETMIRROR_BASE}/pv/search.php`, - 'disney': `${NETMIRROR_BASE}/mobile/hs/search.php` - }; - - const searchUrl = searchEndpoints[platform.toLowerCase()] || searchEndpoints['netflix']; - - return makeRequest( - `${searchUrl}?s=${encodeURIComponent(query)}&t=${getUnixTime()}`, - { - headers: { - ...BASE_HEADERS, - 'Cookie': cookieString, - 'Referer': `${NETMIRROR_BASE}/tv/home` - } - } - ); - }).then(function (response) { - return response.json(); - }).then(function (searchData) { - if (searchData.searchResult && searchData.searchResult.length > 0) { - console.log(`[NetMirror] Found ${searchData.searchResult.length} results`); - return searchData.searchResult.map(item => ({ - id: item.id, - title: item.t, - posterUrl: `https://imgcdn.media/poster/v/${item.id}.jpg` - })); - } else { - console.log('[NetMirror] No results found'); - return []; - } - }); -} - -// Get episodes from specific season -function getEpisodesFromSeason(seriesId, seasonId, platform, page) { - const ottMap = { - 'netflix': 'nf', - 'primevideo': 'pv', - 'disney': 'hs' - }; - - const ott = ottMap[platform.toLowerCase()] || 'nf'; - - return bypass().then(function (cookie) { - const cookies = { - 't_hash_t': cookie, - 'ott': ott, - 'hd': 'on' - }; - - const cookieString = Object.entries(cookies) - .map(([key, value]) => `${key}=${value}`) - .join('; '); - - const episodes = []; - let currentPage = page || 1; - - // Platform-specific episodes endpoints - const episodesEndpoints = { - 'netflix': `${NETMIRROR_BASE}/episodes.php`, - 'primevideo': `${NETMIRROR_BASE}/pv/episodes.php`, - 'disney': `${NETMIRROR_BASE}/mobile/hs/episodes.php` - }; - - const episodesUrl = episodesEndpoints[platform.toLowerCase()] || episodesEndpoints['netflix']; - - function fetchPage(pageNum) { - return makeRequest( - `${episodesUrl}?s=${seasonId}&series=${seriesId}&t=${getUnixTime()}&page=${pageNum}`, - { - headers: { - ...BASE_HEADERS, - 'Cookie': cookieString, - 'Referer': `${NETMIRROR_BASE}/tv/home` - } - } - ).then(function (response) { - return response.json(); - }).then(function (episodeData) { - if (episodeData.episodes) { - episodes.push(...episodeData.episodes); - } - - if (episodeData.nextPageShow === 0) { - return episodes; - } else { - return fetchPage(pageNum + 1); - } - }).catch(function (error) { - console.log(`[NetMirror] Failed to load episodes from season ${seasonId}, page ${pageNum}`); - return episodes; - }); - } - - return fetchPage(currentPage); - }); -} - -// Load content details -function loadContent(contentId, platform) { - console.log(`[NetMirror] Loading content details for ID: ${contentId}`); - - const ottMap = { - 'netflix': 'nf', - 'primevideo': 'pv', - 'disney': 'hs' - }; - - const ott = ottMap[platform.toLowerCase()] || 'nf'; - - return bypass().then(function (cookie) { - const cookies = { - 't_hash_t': cookie, - 'ott': ott, - 'hd': 'on' - }; - - const cookieString = Object.entries(cookies) - .map(([key, value]) => `${key}=${value}`) - .join('; '); - - // Platform-specific post endpoints - const postEndpoints = { - 'netflix': `${NETMIRROR_BASE}/post.php`, - 'primevideo': `${NETMIRROR_BASE}/pv/post.php`, - 'disney': `${NETMIRROR_BASE}/mobile/hs/post.php` - }; - - const postUrl = postEndpoints[platform.toLowerCase()] || postEndpoints['netflix']; - - return makeRequest( - `${postUrl}?id=${contentId}&t=${getUnixTime()}`, - { - headers: { - ...BASE_HEADERS, - 'Cookie': cookieString, - 'Referer': `${NETMIRROR_BASE}/tv/home` - } - } - ); - }).then(function (response) { - return response.json(); - }).then(function (postData) { - console.log(`[NetMirror] Loaded: ${postData.title}`); - - let allEpisodes = postData.episodes || []; - - // If this is a TV series, fetch episodes from all seasons - if (postData.episodes && postData.episodes.length > 0 && postData.episodes[0] !== null) { - console.log('[NetMirror] Loading episodes from all seasons...'); - - // Create a promise chain to load all episodes - let episodePromise = Promise.resolve(); - - // Add episodes from current season if nextPageShow indicates more pages - if (postData.nextPageShow === 1 && postData.nextPageSeason) { - episodePromise = episodePromise.then(function () { - return getEpisodesFromSeason(contentId, postData.nextPageSeason, platform, 2); - }).then(function (additionalEpisodes) { - allEpisodes.push(...additionalEpisodes); - }); - } - - // Add episodes from other seasons (excluding the last one which is current) - if (postData.season && postData.season.length > 1) { - const otherSeasons = postData.season.slice(0, -1); // Remove last season - - otherSeasons.forEach(function (season) { - episodePromise = episodePromise.then(function () { - return getEpisodesFromSeason(contentId, season.id, platform, 1); - }).then(function (seasonEpisodes) { - allEpisodes.push(...seasonEpisodes); - }); - }); - } - - return episodePromise.then(function () { - console.log(`[NetMirror] Loaded ${allEpisodes.filter(ep => ep !== null).length} total episodes`); - - return { - id: contentId, - title: postData.title, - description: postData.desc, - year: postData.year, - episodes: allEpisodes, - seasons: postData.season || [], - isMovie: !postData.episodes || postData.episodes.length === 0 || postData.episodes[0] === null - }; - }); - } - - return { - id: contentId, - title: postData.title, - description: postData.desc, - year: postData.year, - episodes: allEpisodes, - seasons: postData.season || [], - isMovie: !postData.episodes || postData.episodes.length === 0 || postData.episodes[0] === null - }; - }); -} - -// Get streaming links -function getStreamingLinks(contentId, title, platform) { - console.log(`[NetMirror] Getting streaming links for: ${title}`); - - const ottMap = { - 'netflix': 'nf', - 'primevideo': 'pv', - 'disney': 'hs' - }; - - const ott = ottMap[platform.toLowerCase()] || 'nf'; - - return bypass().then(function (cookie) { - const cookies = { - 't_hash_t': cookie, - 'ott': ott, - 'hd': 'on' - }; - - const cookieString = Object.entries(cookies) - .map(([key, value]) => `${key}=${value}`) - .join('; '); - - // Platform-specific playlist endpoints - const playlistEndpoints = { - 'netflix': `${NETMIRROR_BASE}/tv/playlist.php`, - 'primevideo': `${NETMIRROR_BASE}/mobile/pv/playlist.php`, - 'disney': `${NETMIRROR_BASE}/mobile/hs/playlist.php` - }; - - const playlistUrl = playlistEndpoints[platform.toLowerCase()] || playlistEndpoints['netflix']; - - return makeRequest( - `${playlistUrl}?id=${contentId}&t=${encodeURIComponent(title)}&tm=${getUnixTime()}`, - { - headers: { - ...BASE_HEADERS, - 'Cookie': cookieString, - 'Referer': `${NETMIRROR_BASE}/tv/home` - } - } - ); - }).then(function (response) { - return response.json(); - }).then(function (playlist) { - if (!Array.isArray(playlist) || playlist.length === 0) { - console.log('[NetMirror] No streaming links found'); - return { sources: [], subtitles: [] }; - } - - const sources = []; - const subtitles = []; - - playlist.forEach(item => { - if (item.sources) { - item.sources.forEach(source => { - // Convert relative URLs to absolute URLs - let fullUrl = source.file; - if (source.file.startsWith('/') && !source.file.startsWith('//')) { - fullUrl = NETMIRROR_BASE + source.file; - } else if (source.file.startsWith('//')) { - fullUrl = 'https:' + source.file; - } - - sources.push({ - url: fullUrl, - quality: source.label, - type: source.type || 'application/x-mpegURL' - }); - }); - } - - if (item.tracks) { - item.tracks - .filter(track => track.kind === 'captions') - .forEach(track => { - // Convert relative URLs to absolute URLs for subtitles - let fullSubUrl = track.file; - if (track.file.startsWith('/') && !track.file.startsWith('//')) { - fullSubUrl = NETMIRROR_BASE + track.file; - } else if (track.file.startsWith('//')) { - fullSubUrl = 'https:' + track.file; - } - - subtitles.push({ - url: fullSubUrl, - language: track.label - }); - }); - } - }); - - console.log(`[NetMirror] Found ${sources.length} streaming sources and ${subtitles.length} subtitle tracks`); - return { sources, subtitles }; - }); -} - -// Find episode ID for TV shows -function findEpisodeId(episodes, season, episode) { - if (!episodes || episodes.length === 0) { - console.log('[NetMirror] No episodes found in content data'); - return null; - } - - const validEpisodes = episodes.filter(ep => ep !== null); - console.log(`[NetMirror] Found ${validEpisodes.length} valid episodes`); - - if (validEpisodes.length > 0) { - console.log(`[NetMirror] Sample episode structure:`, JSON.stringify(validEpisodes[0], null, 2)); - } - - const targetEpisode = validEpisodes.find(ep => { - // Handle different possible episode structure formats - let epSeason, epNumber; - - if (ep.s && ep.ep) { - epSeason = parseInt(ep.s.replace('S', '')); - epNumber = parseInt(ep.ep.replace('E', '')); - } else if (ep.season && ep.episode) { - epSeason = parseInt(ep.season); - epNumber = parseInt(ep.episode); - } else if (ep.season_number && ep.episode_number) { - epSeason = parseInt(ep.season_number); - epNumber = parseInt(ep.episode_number); - } else { - console.log(`[NetMirror] Unknown episode format:`, ep); - return false; - } - - console.log(`[NetMirror] Checking episode S${epSeason}E${epNumber} against target S${season}E${episode}`); - return epSeason === season && epNumber === episode; - }); - - if (targetEpisode) { - console.log(`[NetMirror] Found target episode:`, targetEpisode); - return targetEpisode.id; - } else { - console.log(`[NetMirror] Target episode S${season}E${episode} not found`); - return null; - } -} - -// Main function to get streams for TMDB content -function getStreams(tmdbId, mediaType = 'movie', seasonNum = null, episodeNum = null) { - console.log(`[NetMirror] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}${seasonNum ? `, S${seasonNum}E${episodeNum}` : ''}`); - - // Get TMDB info - const tmdbUrl = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY}`; - return makeRequest(tmdbUrl).then(function (tmdbResponse) { - return tmdbResponse.json(); - }).then(function (tmdbData) { - const title = mediaType === 'tv' ? tmdbData.name : tmdbData.title; - const year = mediaType === 'tv' ? tmdbData.first_air_date?.substring(0, 4) : tmdbData.release_date?.substring(0, 4); - - if (!title) { - throw new Error('Could not extract title from TMDB response'); - } - - console.log(`[NetMirror] TMDB Info: "${title}" (${year})`); - - // Try all platforms in sequence, but prioritize Prime Video for certain content - let platforms = ['netflix', 'primevideo', 'disney']; - - // Prioritize Prime Video for shows like "The Boys" - if (title.toLowerCase().includes('boys') || title.toLowerCase().includes('prime')) { - platforms = ['primevideo', 'netflix', 'disney']; - } - - console.log(`[NetMirror] Will try search queries: "${title}" and "${title} ${year}"`); - - function calculateSimilarity(str1, str2) { - const s1 = str1.toLowerCase().trim(); - const s2 = str2.toLowerCase().trim(); - - if (s1 === s2) return 1.0; - - const words1 = s1.split(/\s+/).filter(w => w.length > 0); - const words2 = s2.split(/\s+/).filter(w => w.length > 0); - - // If query is shorter, check if all query words are in title - if (words2.length <= words1.length) { - let exactMatches = 0; - for (const queryWord of words2) { - if (words1.includes(queryWord)) { - exactMatches++; - } - } - - // All query words must match for high similarity - if (exactMatches === words2.length) { - return 0.95 * (exactMatches / words1.length); - } - } - - // Check if title starts with query - if (s1.startsWith(s2)) { - return 0.9; - } - - return 0; - } - - function filterRelevantResults(searchResults, query) { - const filtered = searchResults.filter(result => { - const similarity = calculateSimilarity(result.title, query); - return similarity >= 0.7; - }); - - // Sort by similarity (highest first) - return filtered.sort((a, b) => { - const simA = calculateSimilarity(a.title, query); - const simB = calculateSimilarity(b.title, query); - return simB - simA; - }); - } - - function tryPlatform(platformIndex) { - if (platformIndex >= platforms.length) { - console.log('[NetMirror] No content found on any platform'); - return []; - } - - const platform = platforms[platformIndex]; - console.log(`[NetMirror] Trying platform: ${platform}`); - - // Try searching with just the title first - function trySearch(withYear) { - const searchQuery = withYear ? `${title} ${year}` : title; - console.log(`[NetMirror] Searching for: "${searchQuery}"`); - - return searchContent(searchQuery, platform).then(function (searchResults) { - if (searchResults.length === 0) { - if (!withYear && year) { - console.log(`[NetMirror] No results for "${title}", trying with year...`); - return trySearch(true); - } - return null; - } - - // Filter results for relevance - const relevantResults = filterRelevantResults(searchResults, title); - - if (relevantResults.length === 0) { - console.log(`[NetMirror] Found ${searchResults.length} results but none were relevant enough`); - if (!withYear && year) { - console.log(`[NetMirror] Trying with year...`); - return trySearch(true); - } - return null; - } - - // Use the most relevant search result - const selectedContent = relevantResults[0]; - console.log(`[NetMirror] Selected: ${selectedContent.title} (ID: ${selectedContent.id}) - filtered from ${searchResults.length} results`); - - return loadContent(selectedContent.id, platform).then(function (contentData) { - let targetContentId = selectedContent.id; - - // For TV shows, find the specific episode - let episodeData = null; - if (mediaType === 'tv' && !contentData.isMovie) { - const validEpisodes = contentData.episodes.filter(ep => ep !== null); - episodeData = validEpisodes.find(ep => { - let epSeason, epNumber; - - if (ep.s && ep.ep) { - epSeason = parseInt(ep.s.replace('S', '')); - epNumber = parseInt(ep.ep.replace('E', '')); - } else if (ep.season && ep.episode) { - epSeason = parseInt(ep.season); - epNumber = parseInt(ep.episode); - } else if (ep.season_number && ep.episode_number) { - epSeason = parseInt(ep.season_number); - epNumber = parseInt(ep.episode_number); - } - - return epSeason === (seasonNum || 1) && epNumber === (episodeNum || 1); - }); - - if (episodeData) { - targetContentId = episodeData.id; - console.log(`[NetMirror] Found episode ID: ${episodeData.id}`); - } else { - console.log(`[NetMirror] Episode S${seasonNum}E${episodeNum} not found`); - return null; - } - } - - return getStreamingLinks(targetContentId, title, platform).then(function (streamData) { - if (!streamData.sources || streamData.sources.length === 0) { - console.log(`[NetMirror] No streaming links found`); - return null; - } - - // Convert to Nuvio stream format - const streams = streamData.sources.map(source => { - // Extract quality from URL parameters or source label - let quality = 'HD'; - - // Try to extract quality from URL parameters - const urlQualityMatch = source.url.match(/[?&]q=(\d+p)/i); - if (urlQualityMatch) { - quality = urlQualityMatch[1]; - } else if (source.quality) { - // Try to extract from source label - const labelQualityMatch = source.quality.match(/(\d+p)/i); - if (labelQualityMatch) { - quality = labelQualityMatch[1]; - } else { - // Normalize quality labels - const normalizedQuality = source.quality.toLowerCase(); - if (normalizedQuality.includes('full hd') || normalizedQuality.includes('1080')) { - quality = '1080p'; - } else if (normalizedQuality.includes('hd') || normalizedQuality.includes('720')) { - quality = '720p'; - } else if (normalizedQuality.includes('480')) { - quality = '480p'; - } else { - quality = source.quality; - } - } - } else if (source.url.includes('720p')) { - quality = '720p'; - } else if (source.url.includes('480p')) { - quality = '480p'; - } else if (source.url.includes('1080p')) { - quality = '1080p'; - } - - // Build title with episode name if available - let streamTitle = `${title} ${year ? `(${year})` : ''} ${quality}`; - if (mediaType === 'tv') { - const episodeName = episodeData && episodeData.t ? episodeData.t : ''; - streamTitle += ` S${seasonNum}E${episodeNum}`; - if (episodeName) { - streamTitle += ` - ${episodeName}`; - } - } - - // Unified headers for all platforms (Netflix, Prime Video, and Disney) - const streamHeaders = { - "Accept": "application/vnd.apple.mpegurl, video/mp4, */*", - "Origin": "https://net2025.cc", - "Referer": "https://net2025.cc/tv/home", - "Cookie": "hd=on", - "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 26_0_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/138.0.7204.156 Mobile/15E148 Safari/604.1" - }; - - return { - name: `NetMirror (${platform.charAt(0).toUpperCase() + platform.slice(1)})`, - title: streamTitle, - url: source.url, - quality: quality, - type: source.type.includes('mpegURL') ? 'hls' : 'direct', - headers: streamHeaders - }; - }); - - // Sort streams: Auto quality first, then by quality (highest first) - streams.sort((a, b) => { - // Auto quality always comes first - if (a.quality.toLowerCase() === 'auto' && b.quality.toLowerCase() !== 'auto') { - return -1; - } - if (b.quality.toLowerCase() === 'auto' && a.quality.toLowerCase() !== 'auto') { - return 1; - } - - // If both are Auto or neither is Auto, sort by quality - const parseQuality = (quality) => { - const match = quality.match(/(\d{3,4})p/i); - return match ? parseInt(match[1], 10) : 0; - }; - - const qualityA = parseQuality(a.quality); - const qualityB = parseQuality(b.quality); - return qualityB - qualityA; // Highest quality first - }); - - console.log(`[NetMirror] Successfully processed ${streams.length} streams from ${platform}`); - return streams; - }); - }); - }); - } - - return trySearch(false).then(function (result) { - if (result) { - return result; - } else { - console.log(`[NetMirror] No content found on ${platform}, trying next platform`); - return tryPlatform(platformIndex + 1); - } - }).catch(function (error) { - console.log(`[NetMirror] Error on ${platform}: ${error.message}, trying next platform`); - return tryPlatform(platformIndex + 1); - }); - } - - return tryPlatform(0); - }).catch(function (error) { - console.error(`[NetMirror] Error in getStreams: ${error.message}`); - return []; - }); -} - -// Export the main function -if (typeof module !== 'undefined' && module.exports) { - module.exports = { getStreams }; -} else { - // For React Native environment - global.getStreams = getStreams; -} \ No newline at end of file diff --git a/local-scrapers-repo/providers/showbox.js b/local-scrapers-repo/providers/showbox.js deleted file mode 100644 index a8cf635..0000000 --- a/local-scrapers-repo/providers/showbox.js +++ /dev/null @@ -1,506 +0,0 @@ -// ShowBox (OG) Provider for Nuvio Local Scrapers -// React Native compatible – no Node core modules, no async/await - -const cheerio = require('cheerio-without-node-native'); - -// Constants -var TMDB_API_KEY = '439c478a771f35c05022f9feabcca01c'; -var SHOWBOX_BASE = 'https://www.showbox.media'; -var PROXY_PREFIX = 'https://timely-taiyaki-81a26d.netlify.app/?destination='; // Proxy all showbox.media -var FEBBOX_BASE = 'https://www.febbox.com'; -var FEBBOX_FILE_SHARE_LIST_URL = FEBBOX_BASE + '/file/file_share_list'; -var FEBBOX_COOKIE_VALUE = (typeof SCRAPER_SETTINGS !== 'undefined' && SCRAPER_SETTINGS && SCRAPER_SETTINGS.cookie) ? SCRAPER_SETTINGS.cookie : 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3NTA5MzA4NTYsIm5iZiI6MTc1MDkzMDg1NiwiZXhwIjoxNzgyMDM0ODc2LCJkYXRhIjp7InVpZCI6ODQ2NzQ4LCJ0b2tlbiI6ImIzNTllZDk1NjBkMDI5ZmQwY2IyNjdlYTZlMWIwMDlkIn19.WqD3ruYvVx8tyfFuRDMWDaTz1XdvLztW4h_rGt6xt8o'; -var DEFAULT_REGION = (typeof SCRAPER_SETTINGS !== 'undefined' && SCRAPER_SETTINGS && SCRAPER_SETTINGS.region) ? SCRAPER_SETTINGS.region : null; - -// Headers -var DEFAULT_HEADERS = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36', - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', - 'Accept-Language': 'en-US,en;q=0.5', - 'Connection': 'keep-alive' -}; - -function makeRequest(url, options) { - options = options || {}; - return fetch(url, { - method: options.method || 'GET', - body: options.body, - headers: Object.assign({}, DEFAULT_HEADERS, options.headers || {}) - }).then(function (response) { - if (!response.ok) { - throw new Error('HTTP ' + response.status + ': ' + response.statusText); - } - return response; - }); -} - -function proxify(url) { - return PROXY_PREFIX + encodeURIComponent(url); -} - -// TMDB minimal helper -function getTMDBDetails(tmdbId, mediaType) { - var u = 'https://api.themoviedb.org/3/' + (mediaType === 'tv' ? 'tv' : 'movie') + '/' + tmdbId + '?api_key=' + TMDB_API_KEY; - return makeRequest(u).then(function (r) { return r.json(); }).then(function (data) { - if (mediaType === 'movie') { - return { title: data.title, original_title: data.original_title, year: data.release_date ? data.release_date.split('-')[0] : null }; - } else { - return { title: data.name, original_title: data.original_name, year: data.first_air_date ? data.first_air_date.split('-')[0] : null }; - } - }).catch(function () { return null; }); -} - -function normalizeTitle(s) { - return (s || '').toLowerCase().replace(/[^a-z0-9\s]/g, ' ').replace(/\s+/g, ' ').trim(); -} - -function calculateSimilarity(a, b) { - var s1 = normalizeTitle(a), s2 = normalizeTitle(b); - if (s1 === s2) return 1.0; - var w1 = s1.split(' '), w2 = s2.split(' '); - var matches = 0; - for (var i = 0; i < w1.length; i++) { - var w = w1[i]; - if (w.length > 2 && w2.some(function (x) { return x.indexOf(w) !== -1 || w.indexOf(x) !== -1; })) matches++; - } - return matches / Math.max(w1.length, w2.length || 1); -} - -function findBestMatch(results, query, year) { - if (!results || results.length === 0) return null; - var scored = results.map(function (r) { - var score = 0; - score += calculateSimilarity(r.title, query) * 100; - if (year && r.year && String(r.year) === String(year)) score += 35; - if (normalizeTitle(r.title) === normalizeTitle(query)) score += 50; - return { item: r, score: score }; - }); - scored.sort(function (a, b) { return b.score - a.score; }); - return scored[0].item; -} - -// Search ShowBox via proxy -function searchShowbox(query, year, mediaType) { - var searchUrl = SHOWBOX_BASE + '/search?keyword=' + encodeURIComponent(query + (year ? (' ' + year) : '')); - return makeRequest(proxify(searchUrl)).then(function (r) { return r.text(); }).then(function (html) { - var $ = cheerio.load(html); - var results = []; - $('div.film-poster a.film-poster-ahref').each(function (i, el) { - var $el = $(el); - var title = ($el.attr('title') || '').trim(); - var href = $el.attr('href') || ''; - if (title && href) { - var full = SHOWBOX_BASE + (href.indexOf('/') === 0 ? '' : '/') + href; - var y = null; - var ym = title.match(/\((\d{4})\)$/); - if (ym && ym[1]) y = ym[1]; - // type check from URL - var isMovie = href.indexOf('/movie/') !== -1; - var isTv = href.indexOf('/tv/') !== -1; - if ((mediaType === 'movie' && isMovie) || (mediaType !== 'movie' && isTv)) { - results.push({ title: title, url: full, year: y }); - } else { - // still include but penalize later via similarity - results.push({ title: title, url: full, year: y }); - } - } - }); - return results; - }).catch(function () { return []; }); -} - -// Extract FebBox share link from ShowBox detail -function extractFebboxLink(detailUrl) { - return makeRequest(proxify(detailUrl)).then(function (r) { return r.text(); }).then(function (html) { - var $ = cheerio.load(html); - var link = null; - $('a[href*="febbox.com/share/"]').each(function (i, el) { - var h = $(el).attr('href'); - if (h && h.indexOf('febbox.com/share/') !== -1) { link = h; return false; } - }); - if (!link) { - var scripts = $('script').map(function (i, el) { return $(el).html() || ''; }).get().join('\n'); - var m = scripts.match(/['"](https?:\/\/www\.febbox\.com\/share\/[a-zA-Z0-9-]+)['"]/); - if (m && m[1]) link = m[1]; - } - if (link) return link; - // Fallback: try extracting numeric ID/type then call share_link API via proxy - var idAndType = extractShowboxIdAndType(html, detailUrl); - if (idAndType && idAndType.id && idAndType.type) { - return getShareLinkFromApi(idAndType.id, idAndType.type).then(function (apiLink) { return apiLink || null; }); - } - return null; - }).catch(function () { return null; }); -} - -// Extract numeric content id and type (1 movie, 2 tv) from detail page -function extractShowboxIdAndType(html, url) { - try { - var id = null; var type = null; - var um = (url || '').match(/\/(movie|tv)\/detail\/(\d+)/); - if (um) { - id = um[2]; - type = um[1] === 'movie' ? '1' : '2'; - } - if (!id) { - var $ = cheerio.load(html); - var link = $('h2.heading-name a[href*="/detail/"], h1.heading-name a[href*="/detail/"]').first(); - if (link && link.length) { - var href = link.attr('href') || ''; - var hm = href.match(/\/(movie|tv)\/detail\/(\d+)/); - if (hm) { id = hm[2]; type = hm[1] === 'movie' ? '1' : '2'; } - } - if (!id) { - var shareDiv = $('div.sharethis-inline-share-buttons'); - var href2 = null; - shareDiv.find('a[href*="/detail/"]').each(function (i, el) { if (!href2) href2 = $(el).attr('href'); }); - if (!href2) { - var durl = shareDiv.attr('data-url') || ''; - if (durl) href2 = durl; - } - if (href2) { - var hm2 = href2.match(/\/(movie|tv)\/detail\/(\d+)/); - if (hm2) { id = hm2[2]; type = hm2[1] === 'movie' ? '1' : '2'; } - } - } - } - if (id && type) return { id: id, type: type }; - return null; - } catch (e) { - return null; - } -} - -// Call ShowBox API to get FebBox link (proxied) -function getShareLinkFromApi(id, type) { - var apiUrl = SHOWBOX_BASE + '/index/share_link?id=' + encodeURIComponent(id) + '&type=' + encodeURIComponent(type); - return makeRequest(proxify(apiUrl), { headers: { 'Accept': 'application/json, text/javascript, */*; q=0.01', 'X-Requested-With': 'XMLHttpRequest' } }) - .then(function (r) { return r.text(); }) - .then(function (txt) { - try { - var json = JSON.parse(txt); - if (json && json.code === 1 && json.data && json.data.link) return json.data.link; - } catch (e) { /* ignore */ } - return null; - }).catch(function () { return null; }); -} - -// Parse direct jwplayer sources from FebBox share page (if present) -function parseDirectSourcesFromShare(html) { - try { - var m = html.match(/var\s+sources\s*=\s*(\[.*?\]);/s); - if (!m) return []; - var arr = JSON.parse(m[1]); - return (arr || []).filter(function (x) { return x && x.file; }).map(function (x) { - return { label: String(x.label || 'ORG'), url: String(x.file) }; - }); - } catch (e) { - return []; - } -} - -function getQualityFromLabel(label) { - var l = String(label || '').toLowerCase(); - if (l.indexOf('2160') !== -1 || l.indexOf('4k') !== -1 || l.indexOf('uhd') !== -1) return '2160p'; - if (l.indexOf('1080') !== -1) return '1080p'; - if (l.indexOf('720') !== -1) return '720p'; - if (l.indexOf('480') !== -1) return '480p'; - if (l.indexOf('360') !== -1) return '360p'; - return 'ORG'; -} - -// Size helpers via HEAD request -function formatSize(bytes) { - var b = parseInt(bytes, 10); - if (isNaN(b) || b <= 0) return null; - if (b < 1024) return b + ' B'; - if (b < 1024 * 1024) return (b / 1024).toFixed(2) + ' KB'; - if (b < 1024 * 1024 * 1024) return (b / (1024 * 1024)).toFixed(2) + ' MB'; - return (b / (1024 * 1024 * 1024)).toFixed(2) + ' GB'; -} - -function fetchSizeForUrl(url) { - try { - if ((url || '').toLowerCase().indexOf('.m3u8') !== -1) return Promise.resolve('Playlist (size N/A)'); - // Try HEAD without Range to get full content-length - return fetch(url, { method: 'HEAD', headers: { 'User-Agent': DEFAULT_HEADERS['User-Agent'] } }) - .then(function (res) { - if (!res) return null; - var len = res.headers && res.headers.get ? res.headers.get('content-length') : null; - if (len) return formatSize(len) || null; - var cr = res.headers && res.headers.get ? res.headers.get('content-range') : null; // bytes 0-0/12345 - if (cr) { - var m = cr.match(/\/(\d+)$/); - if (m && m[1]) return formatSize(m[1]) || null; - } - // Fallback: do a tiny ranged GET to read Content-Range total size - return fetch(url, { method: 'GET', headers: { 'Range': 'bytes=0-0', 'User-Agent': DEFAULT_HEADERS['User-Agent'] } }) - .then(function (r2) { - var cr2 = r2.headers && r2.headers.get ? r2.headers.get('content-range') : null; - if (cr2) { - var m2 = cr2.match(/\/(\d+)$/); - if (m2 && m2[1]) return formatSize(m2[1]) || null; - } - var len2 = r2.headers && r2.headers.get ? r2.headers.get('content-length') : null; - return len2 ? (formatSize(len2) || null) : null; - }).catch(function () { return null; }); - }).catch(function () { return null; }); - } catch (e) { - return Promise.resolve(null); - } -} - -function attachSizes(streams) { - var tasks = (streams || []).map(function (s) { - return fetchSizeForUrl(s.url).then(function (sz) { s.size = sz; return s; }).catch(function () { return s; }); - }); - return Promise.all(tasks).then(function (arr) { return arr; }); -} - -// Build display title per README: descriptive text, not the URL -function buildStreamTitle(label, fileName) { - if (fileName && fileName.trim()) return fileName; - return (label && String(label).trim()) ? String(label).trim() : 'ORG'; -} - -// Extract detailed filename (prefer KEY5 param if present) -function extractDetailedFilename(url) { - try { - var u = new URL(url); - var key5 = u.searchParams.get('KEY5'); - if (key5) { - try { return decodeURIComponent(key5); } catch (e) { return key5; } - } - var base = u.pathname.split('/').pop() || ''; - try { return decodeURIComponent(base); } catch (e2) { return base; } - } catch (e) { - return null; - } -} - -function sortPreferMkvOrg(streams) { - if (!streams || streams.length === 0) return streams; - function baseOrder(q) { - if (q === 'ORG') return 6; - if (q === '2160p') return 5; - if (q === '1080p') return 4; - if (q === '720p') return 3; - if (q === '480p') return 2; - if (q === '360p') return 1; - return 0; - } - function mkvBonus(s) { - var fname = (s.fileName || s.title || '').toLowerCase(); - var url = (s.url || '').toLowerCase(); - return (fname.indexOf('.mkv') !== -1 || /\.mkv(\?|$)/i.test(url)) ? 1 : 0; - } - function sizeBytes(sz) { - if (!sz) return 0; - var m = String(sz).match(/([\d.]+)\s*(gb|mb|kb|b)/i); - if (!m) return 0; - var v = parseFloat(m[1]); - var u = m[2].toLowerCase(); - var mult = (u==='gb')?1024*1024*1024:(u==='mb')?1024*1024:(u==='kb')?1024:1; - return Math.floor(v*mult); - } - return [].concat(streams).sort(function(a,b){ - var aBase = baseOrder(a.quality||''); - var bBase = baseOrder(b.quality||''); - if (aBase !== bBase) return bBase - aBase; - var aM = mkvBonus(a), bM = mkvBonus(b); - if (aM !== bM) return bM - aM; - var aS = sizeBytes(a.size), bS = sizeBytes(b.size); - if (aS !== bS) return bS - aS; - return 0; - }); -} - -// Extract (share_key, fids[]) from FebBox share page -function extractShareMeta(html, url) { - var $ = cheerio.load(html); - var shareKey = null; - var mk = (url || '').match(/\/share\/([a-zA-Z0-9-]+)/); - if (mk && mk[1]) shareKey = mk[1]; - if (!shareKey) { - var km = html.match(/(?:var\s+share_key\s*=|share_key:\s*|shareid=)"?([a-zA-Z0-9-]+)"?/); - if (km && km[1]) shareKey = km[1]; - } - var fids = []; - $('div.file').each(function (i, el) { - var $el = $(el); - var id = $el.attr('data-id'); - if (id && /^\d+$/.test(id) && !$el.hasClass('open_dir')) fids.push(id); - }); - fids = Array.from(new Set(fids)); - return { shareKey: shareKey, fids: fids }; -} - -// POST to FebBox player to resolve a fid -> sources -function resolveFid(fid, shareKey, region) { - var url = FEBBOX_BASE + '/file/player'; - var body = new URLSearchParams(); - body.append('fid', fid); - body.append('share_key', shareKey); - return makeRequest(url, { - method: 'POST', - body: body.toString(), - headers: { - 'Cookie': 'ui=' + FEBBOX_COOKIE_VALUE + (region ? ('; oss_group=' + region) : ''), - 'Content-Type': 'application/x-www-form-urlencoded' - } - }).then(function (r) { return r.text(); }).then(function (txt) { - // Either direct URL or JS with var sources = [...] - if (/^https?:\/\//i.test(txt) && (txt.indexOf('.mp4') !== -1 || txt.indexOf('.m3u8') !== -1)) { - return [{ label: 'DirectLink', url: txt.trim() }]; - } - var m = txt.match(/var\s+sources\s*=\s*(\[.*?\]);/s); - if (m && m[1]) { - try { return JSON.parse(m[1]); } catch (e) { return []; } - } - try { - var json = JSON.parse(txt); - if (json && json.msg) return []; - } catch (e) { /* ignore */ } - return []; - }).catch(function () { return []; }); -} - -// Fetch FebBox share page and return streams -function getStreamsFromFebboxShare(shareUrl, type, season, episode, region) { - return makeRequest(shareUrl, { headers: { 'Cookie': 'ui=' + FEBBOX_COOKIE_VALUE + (region ? ('; oss_group=' + region) : '') } }) - .then(function (r) { return r.text(); }) - .then(function (html) { - var direct = parseDirectSourcesFromShare(html); - if (direct.length > 0) { - return attachSizes(direct.map(function (s) { - return { - name: 'ShowBox', - title: buildStreamTitle(s.label, extractDetailedFilename(s.url)), - url: s.url, - quality: getQualityFromLabel(s.label), - size: null, - fileName: extractDetailedFilename(s.url), - type: 'direct' - }; - })).then(function (arr) { return sortPreferMkvOrg(arr); }); - } - var meta = extractShareMeta(html, shareUrl); - if (!meta.shareKey) return []; - - // If movie or there are direct file fids on root, resolve them directly - if (type === 'movie' || (meta.fids && meta.fids.length > 0)) { - var fidsDirect = meta.fids || []; - var tasksDirect = fidsDirect.map(function (fid) { return resolveFid(fid, meta.shareKey, region).then(function (arr) { return arr || []; }).catch(function () { return []; }); }); - return Promise.all(tasksDirect).then(function (arrs) { - var flatD = [].concat.apply([], arrs).filter(function (x) { return x && x.file; }); - return attachSizes(flatD.map(function (s) { return { name: 'ShowBox', title: buildStreamTitle(s.label, extractDetailedFilename(s.file)), url: s.file, quality: getQualityFromLabel(s.label), size: null, fileName: extractDetailedFilename(s.file), type: 'direct' }; })).then(function (arr) { return sortPreferMkvOrg(arr); }); - }); - } - - // TV folder traversal: find season folder and fetch file list via FebBox API - if (type !== 'movie' && season) { - var $root = cheerio.load(html); - var shareKey = meta.shareKey; - var seasonFolderId = null; - // locate season folders - $root('div.file.open_dir').each(function (i, el) { - var $el = $root(el); - var id = $el.attr('data-id'); - var fname = ($el.find('p.file_name').text() || $el.attr('data-path') || '').toLowerCase(); - var sNum = null; - var m1 = fname.match(/season\s+(\d+)/i); - var m2 = fname.match(/\bs(\d+)\b/i); - var m3 = fname.match(/season(\d+)/i); - if (m1 && m1[1]) sNum = parseInt(m1[1], 10); - else if (m2 && m2[1]) sNum = parseInt(m2[1], 10); - else if (m3 && m3[1]) sNum = parseInt(m3[1], 10); - if (!sNum) { - var onlyNum = fname.match(/\b(\d+)\b/); - if (onlyNum && onlyNum[1]) sNum = parseInt(onlyNum[1], 10); - } - if (id && sNum === parseInt(season, 10)) seasonFolderId = id; - }); - if (!seasonFolderId) return []; - - // fetch folder content via API - var listUrl = FEBBOX_FILE_SHARE_LIST_URL + '?share_key=' + encodeURIComponent(shareKey) + '&parent_id=' + encodeURIComponent(seasonFolderId) + '&is_html=1&pwd='; - return makeRequest(listUrl, { headers: { 'Cookie': 'ui=' + FEBBOX_COOKIE_VALUE + (region ? ('; oss_group=' + region) : ''), 'X-Requested-With': 'XMLHttpRequest' } }) - .then(function (res) { return res.text(); }) - .then(function (txt) { - var folderHtml = txt; - try { var j = JSON.parse(txt); if (j && j.html) folderHtml = j.html; } catch (e) { /* keep txt */ } - var $folder = cheerio.load(folderHtml || ''); - var targets = []; - $folder('div.file').each(function (i, el) { - var $el = $folder(el); - var id = $el.attr('data-id'); - if (!id || /open_dir/.test($el.attr('class') || '')) return; - var name = ($el.find('p.file_name').text() || '').toLowerCase(); - var ep = null; - var m = name.match(/(?:e|ep|episode)[\s._-]*0*(\d{1,3})/i); - if (m && m[1]) ep = parseInt(m[1], 10); - if (!ep) { - var only = name.match(/\b(\d{1,3})\b/); - if (only && only[1]) ep = parseInt(only[1], 10); - } - if (!episode || (ep && ep === parseInt(episode, 10))) targets.push(id); - }); - if (targets.length === 0) return []; - var tasks = targets.map(function (fid) { return resolveFid(fid, shareKey, region).then(function (arr) { return arr || []; }).catch(function () { return []; }); }); - return Promise.all(tasks).then(function (arrs) { - var flat = [].concat.apply([], arrs).filter(function (x) { return x && x.file; }); - return attachSizes(flat.map(function (s) { return { name: 'ShowBox', title: buildStreamTitle(s.label, extractDetailedFilename(s.file)), url: s.file, quality: getQualityFromLabel(s.label), size: null, fileName: extractDetailedFilename(s.file), type: 'direct' }; })).then(function (arr) { return sortPreferMkvOrg(arr); }); - }); - }).catch(function () { return []; }); - } - - return []; - }).catch(function () { return []; }); -} - -// Region-aware entry: force a specific oss_group region via cookie -function getStreamsByRegion(tmdbId, type, season, episode, region) { - type = type || 'movie'; - var tmdbType = (type === 'series' ? 'tv' : type); - return getTMDBDetails(tmdbId, tmdbType).then(function (tmdb) { - if (!tmdb || !tmdb.title) return []; - return searchShowbox(tmdb.title, tmdb.year, tmdbType === 'movie' ? 'movie' : 'tv').then(function (results) { - if (!results || results.length === 0) return []; - var best = findBestMatch(results, tmdb.title, tmdb.year) || results[0]; - return extractFebboxLink(best.url).then(function (shareUrl) { - if (!shareUrl) return []; - return getStreamsFromFebboxShare(shareUrl, tmdbType === 'movie' ? 'movie' : 'tv', season, episode, region) - .then(function (streams) { return sortPreferMkvOrg(streams || []); }) - .then(function (streams) { return streams.map(function (s) { s.name = 'ShowBox - ' + region; return s; }); }); - }); - }); - }).catch(function () { return []; }); -} - -// Main entry – Promise-based -function getStreams(tmdbId, type, season, episode) { - type = type || 'movie'; - var tmdbType = (type === 'series' ? 'tv' : type); - return getTMDBDetails(tmdbId, tmdbType).then(function (tmdb) { - if (!tmdb || !tmdb.title) return []; - return searchShowbox(tmdb.title, tmdb.year, tmdbType === 'movie' ? 'movie' : 'tv').then(function (results) { - if (!results || results.length === 0) return []; - var best = findBestMatch(results, tmdb.title, tmdb.year) || results[0]; - return extractFebboxLink(best.url).then(function (shareUrl) { - if (!shareUrl) return []; - return getStreamsFromFebboxShare(shareUrl, tmdbType === 'movie' ? 'movie' : 'tv', season, episode, DEFAULT_REGION) - .then(function (streams) { return sortPreferMkvOrg(streams || []); }); - }); - }); - }).catch(function () { return []; }); -} - -// Export -if (typeof module !== 'undefined' && module.exports) { - module.exports = { getStreams, getStreamsByRegion }; -} else { - // eslint-disable-next-line no-undef - global.getStreams = getStreams; -} - - diff --git a/local-scrapers-repo/providers/uhdmovies.js b/local-scrapers-repo/providers/uhdmovies.js deleted file mode 100644 index cdda5c4..0000000 --- a/local-scrapers-repo/providers/uhdmovies.js +++ /dev/null @@ -1,1081 +0,0 @@ -// UHD Movies Scraper for Nuvio Local Scrapers -// React Native compatible version with Cheerio support - -// Import cheerio-without-node-native for React Native -const cheerio = require('cheerio-without-node-native'); -console.log('[UHDMovies] Using cheerio-without-node-native for DOM parsing'); - -// Constants -const TMDB_API_KEY = "439c478a771f35c05022f9feabcca01c"; -const FALLBACK_DOMAIN = 'https://uhdmovies.email'; -const DOMAIN_CACHE_TTL = 4 * 60 * 60 * 1000; // 4 hours - -// Global variables for domain caching -let uhdMoviesDomain = FALLBACK_DOMAIN; -let domainCacheTimestamp = 0; - -// Fetch latest domain from GitHub -async function getUHDMoviesDomain() { - const now = Date.now(); - if (now - domainCacheTimestamp < DOMAIN_CACHE_TTL) { - return uhdMoviesDomain; - } - - try { - console.log('[UHDMovies] Fetching latest domain...'); - const response = await fetch('https://raw.githubusercontent.com/phisher98/TVVVV/refs/heads/main/domains.json', { - method: 'GET', - headers: { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' - } - }); - - if (response.ok) { - const data = await response.json(); - if (data && data.UHDMovies) { - uhdMoviesDomain = data.UHDMovies; - domainCacheTimestamp = now; - console.log(`[UHDMovies] Updated domain to: ${uhdMoviesDomain}`); - } - } - } catch (error) { - console.error(`[UHDMovies] Failed to fetch latest domain: ${error.message}`); - } - - return uhdMoviesDomain; -} - -// Helper function to make HTTP requests -async function makeRequest(url, options = {}) { - const defaultHeaders = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', - 'Accept-Language': 'en-US,en;q=0.5', - 'Accept-Encoding': 'gzip, deflate', - 'Connection': 'keep-alive', - 'Upgrade-Insecure-Requests': '1' - }; - - const response = await fetch(url, { - ...options, - headers: { - ...defaultHeaders, - ...options.headers - } - }); - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - - return response; -} - -// Search for movies on UHD Movies -async function searchMovies(query) { - try { - const domain = await getUHDMoviesDomain(); - const searchUrl = `${domain}/search/${encodeURIComponent(query)}`; - - console.log(`[UHDMovies] Searching: ${searchUrl}`); - - const response = await makeRequest(searchUrl); - const html = await response.text(); - - const results = []; - const $ = cheerio.load(html); - - // New logic for grid-based search results - $('article.gridlove-post').each((index, element) => { - const linkElement = $(element).find('a[href*="/download-"]'); - if (linkElement.length > 0) { - const link = linkElement.first().attr('href'); - // Prefer the 'title' attribute, fallback to h1 text - const title = linkElement.first().attr('title') || $(element).find('h1.sanket').text().trim(); - - if (link && title && !results.some(item => item.url === link)) { - // Extract year from title - const yearMatch = title.match(/\((\d{4})\)/); - const year = yearMatch ? parseInt(yearMatch[1]) : null; - - results.push({ - title: title.replace(/\(\d{4}\)/, '').trim(), - year, - url: link.startsWith('http') ? link : `${domain}${link}` - }); - } - } - }); - - // Fallback for original list-based search if new logic fails - if (results.length === 0) { - console.log('[UHDMovies] Grid search logic found no results, trying original list-based logic...'); - $('a[href*="/download-"]').each((index, element) => { - const link = $(element).attr('href'); - // Avoid duplicates by checking if link already exists in results - if (link && !results.some(item => item.url === link)) { - const title = $(element).text().trim(); - if (title) { - // Extract year from title - const yearMatch = title.match(/\((\d{4})\)/); - const year = yearMatch ? parseInt(yearMatch[1]) : null; - - results.push({ - title: title.replace(/\(\d{4}\)/, '').trim(), - year, - url: link.startsWith('http') ? link : `${domain}${link}` - }); - } - } - }); - } - - console.log(`[UHDMovies] Found ${results.length} search results`); - return results; - } catch (error) { - console.error(`[UHDMovies] Search failed: ${error.message}`); - return []; - } -} - -// Function to extract clean quality information from verbose text -function extractCleanQuality(fullQualityText) { - if (!fullQualityText || fullQualityText === 'Unknown Quality') { - return 'Unknown Quality'; - } - - const cleanedFullQualityText = fullQualityText.replace(/(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])/g, '').trim(); - const text = cleanedFullQualityText.toLowerCase(); - let quality = []; - - // Extract resolution - if (text.includes('2160p') || text.includes('4k')) { - quality.push('4K'); - } else if (text.includes('1080p')) { - quality.push('1080p'); - } else if (text.includes('720p')) { - quality.push('720p'); - } else if (text.includes('480p')) { - quality.push('480p'); - } - - // Extract special features - if (text.includes('hdr')) { - quality.push('HDR'); - } - if (text.includes('dolby vision') || text.includes('dovi') || /\bdv\b/.test(text)) { - quality.push('DV'); - } - if (text.includes('imax')) { - quality.push('IMAX'); - } - if (text.includes('bluray') || text.includes('blu-ray')) { - quality.push('BluRay'); - } - - // If we found any quality indicators, join them - if (quality.length > 0) { - return quality.join(' | '); - } - - // Fallback: try to extract a shorter version of the original text - const patterns = [ - /(\d{3,4}p.*?(?:x264|x265|hevc).*?)[\[\(]/i, - /(\d{3,4}p.*?)[\[\(]/i, - /((?:720p|1080p|2160p|4k).*?)$/i - ]; - - for (const pattern of patterns) { - const match = cleanedFullQualityText.match(pattern); - if (match && match[1].trim().length < 100) { - return match[1].trim().replace(/x265/ig, 'HEVC'); - } - } - - // Final fallback: truncate if too long - if (cleanedFullQualityText.length > 80) { - return cleanedFullQualityText.substring(0, 77).replace(/x265/ig, 'HEVC') + '...'; - } - - return cleanedFullQualityText.replace(/x265/ig, 'HEVC'); -} - -// Compare media info with search results -function compareMedia(mediaInfo, searchResult) { - const titleMatch = mediaInfo.title.toLowerCase().includes(searchResult.title.toLowerCase()) || - searchResult.title.toLowerCase().includes(mediaInfo.title.toLowerCase()); - - const yearMatch = !mediaInfo.year || !searchResult.year || - Math.abs(mediaInfo.year - searchResult.year) <= 1; - - return titleMatch && yearMatch; -} - -// Extract quality information from page title -function extractQualityFromTitle(pageTitle) { - if (!pageTitle) return 'Unknown Quality'; - - const qualities = []; - const title = pageTitle.toLowerCase(); - - // Extract resolution - if (title.includes('2160p') || title.includes('4k')) { - qualities.push('4K'); - } else if (title.includes('1080p')) { - qualities.push('1080p'); - } else if (title.includes('720p')) { - qualities.push('720p'); - } else if (title.includes('480p')) { - qualities.push('480p'); - } - - // Extract special features - if (title.includes('hdr')) qualities.push('HDR'); - if (title.includes('dolby vision') || title.includes('dv')) qualities.push('DV'); - if (title.includes('imax')) qualities.push('IMAX'); - if (title.includes('bluray') || title.includes('blu-ray')) qualities.push('BluRay'); - if (title.includes('hevc') || title.includes('x265')) qualities.push('HEVC'); - if (title.includes('10bit')) qualities.push('10bit'); - - return qualities.length > 0 ? qualities.join(' | ') : 'Unknown Quality'; -} - -// Extract download links from movie page -async function extractDownloadLinks(movieUrl, targetYear = null) { - try { - console.log(`[UHDMovies] Extracting links from: ${movieUrl}`); - - const response = await makeRequest(movieUrl); - const html = await response.text(); - - const links = []; - const $ = cheerio.load(html); - const movieTitle = $('h1').first().text().trim(); - - // Find all download links (the new SID links) and their associated quality information - $('a[href*="tech.unblockedgames.world"], a[href*="tech.examzculture.in"], a[href*="tech.examdegree.site"]').each((index, element) => { - const link = $(element).attr('href'); - - if (link && !links.some(item => item.url === link)) { - let quality = 'Unknown Quality'; - let size = 'Unknown'; - - // Method 1: Look for quality in the closest preceding paragraph or heading - const prevElement = $(element).closest('p').prev(); - if (prevElement.length > 0) { - const prevText = prevElement.text().trim(); - if (prevText && prevText.length > 20 && !prevText.includes('Download')) { - quality = prevText; - } - } - - // Method 2: Look for quality in parent's siblings - if (quality === 'Unknown Quality') { - const parentSiblings = $(element).parent().prevAll().first().text().trim(); - if (parentSiblings && parentSiblings.length > 20) { - quality = parentSiblings; - } - } - - // Method 3: Look for bold/strong text above the link - if (quality === 'Unknown Quality') { - const strongText = $(element).closest('p').prevAll().find('strong, b').last().text().trim(); - if (strongText && strongText.length > 20) { - quality = strongText; - } - } - - // Method 4: Look for the entire paragraph containing quality info - if (quality === 'Unknown Quality') { - let currentElement = $(element).parent(); - for (let i = 0; i < 5; i++) { - currentElement = currentElement.prev(); - if (currentElement.length === 0) break; - - const text = currentElement.text().trim(); - if (text && text.length > 30 && - (text.includes('1080p') || text.includes('720p') || text.includes('2160p') || - text.includes('4K') || text.includes('HEVC') || text.includes('x264') || text.includes('x265'))) { - quality = text; - break; - } - } - } - - // Year-based filtering for collections - if (targetYear && quality !== 'Unknown Quality') { - // Check for years in quality text - const yearMatches = quality.match(/\((\d{4})\)/g); - let hasMatchingYear = false; - - if (yearMatches && yearMatches.length > 0) { - for (const yearMatch of yearMatches) { - const year = parseInt(yearMatch.replace(/[()]/g, '')); - if (year === targetYear) { - hasMatchingYear = true; - break; - } - } - if (!hasMatchingYear) { - console.log(`[UHDMovies] Skipping link due to year mismatch. Target: ${targetYear}, Found: ${yearMatches.join(', ')} in "${quality}"`); - return; // Skip this link - } - } else { - // If no year in quality text, check filename and other indicators - const linkText = $(element).text().trim(); - const parentText = $(element).parent().text().trim(); - const combinedText = `${quality} ${linkText} ${parentText}`; - - // Look for years in combined text - const allYearMatches = combinedText.match(/\((\d{4})\)/g) || combinedText.match(/(\d{4})/g); - if (allYearMatches) { - let foundTargetYear = false; - for (const yearMatch of allYearMatches) { - const year = parseInt(yearMatch.replace(/[()]/g, '')); - if (year >= 1900 && year <= 2030) { // Valid movie year range - if (year === targetYear) { - foundTargetYear = true; - break; - } - } - } - if (!foundTargetYear && allYearMatches.length > 0) { - console.log(`[UHDMovies] Skipping link due to no matching year found. Target: ${targetYear}, Found years: ${allYearMatches.join(', ')} in combined text`); - return; // Skip this link - } - } - - // Additional check: if quality contains movie names that don't match target year - const lowerQuality = quality.toLowerCase(); - if (targetYear === 2015) { - if (lowerQuality.includes('wasp') || lowerQuality.includes('quantumania')) { - console.log(`[UHDMovies] Skipping link for 2015 target as it contains 'wasp' or 'quantumania': "${quality}"`); - return; // Skip this link - } - } - } - } - - // Extract size from quality text if present - const sizeMatch = quality.match(/\[([0-9.,]+\s*[KMGT]B[^\]]*)\]/); - if (sizeMatch) { - size = sizeMatch[1]; - } - - // Clean up the quality information - const cleanQuality = extractCleanQuality(quality); - - links.push({ - url: link, - quality: cleanQuality, - size: size, - rawQuality: quality.replace(/(\r\n|\n|\r)/gm, " ").replace(/\s+/g, ' ').trim() - }); - } - }); - - console.log(`[UHDMovies] Extracted ${links.length} download links`); - return links; - } catch (error) { - console.error(`[UHDMovies] Failed to extract links: ${error.message}`); - return []; - } -} - -// Parse size string to bytes for sorting -function parseSize(sizeString) { - if (!sizeString || typeof sizeString !== 'string') return 0; - - const match = sizeString.match(/(\d+(?:\.\d+)?)\s*(GB|MB|TB)/i); - if (!match) return 0; - - const value = parseFloat(match[1]); - const unit = match[2].toUpperCase(); - - switch (unit) { - case 'TB': return value * 1024 * 1024 * 1024 * 1024; - case 'GB': return value * 1024 * 1024 * 1024; - case 'MB': return value * 1024 * 1024; - default: return value; - } -} - -// Function to resolve SID links to driveleech URLs (React Native compatible) -async function resolveSidToDriveleech(sidUrl) { - console.log(`[UHDMovies] Resolving SID link: ${sidUrl}`); - const origin = new URL(sidUrl).origin; - - try { - // Step 0: Get the _wp_http value - console.log(" [SID] Step 0: Fetching initial page..."); - const responseStep0 = await makeRequest(sidUrl); - const html0 = await responseStep0.text(); - - const wpHttpRegex = /]*name="_wp_http"[^>]*value="([^"]*)"[^>]*>/i; - const actionRegex = /]*id="landing"[^>]*action="([^"]*)"[^>]*>/i; - - const wpHttpMatch = wpHttpRegex.exec(html0); - const actionMatch = actionRegex.exec(html0); - - if (!wpHttpMatch || !actionMatch) { - console.error(" [SID] Error: Could not find _wp_http in initial form."); - return null; - } - - const wpHttp = wpHttpMatch[1]; - const actionUrl = actionMatch[1]; - - // Step 1: POST to the first form's action URL - console.log(" [SID] Step 1: Submitting initial form..."); - const step1Data = new URLSearchParams({ '_wp_http': wpHttp }); - const responseStep1 = await fetch(actionUrl, { - method: 'POST', - body: step1Data, - headers: { - 'Referer': sidUrl, - 'Content-Type': 'application/x-www-form-urlencoded', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36' - } - }); - - const html1 = await responseStep1.text(); - - // Step 2: Parse verification page for second form - console.log(" [SID] Step 2: Parsing verification page..."); - const action2Regex = /]*id="landing"[^>]*action="([^"]*)"[^>]*>/i; - const wpHttp2Regex = /]*name="_wp_http2"[^>]*value="([^"]*)"[^>]*>/i; - const tokenRegex = /]*name="token"[^>]*value="([^"]*)"[^>]*>/i; - - const action2Match = action2Regex.exec(html1); - const wpHttp2Match = wpHttp2Regex.exec(html1); - const tokenMatch = tokenRegex.exec(html1); - - if (!action2Match) { - console.error(" [SID] Error: Could not find verification form action."); - return null; - } - - const action2Url = action2Match[1]; - const wpHttp2 = wpHttp2Match ? wpHttp2Match[1] : ''; - const token = tokenMatch ? tokenMatch[1] : ''; - - // Step 3: POST to the verification URL - console.log(" [SID] Step 3: Submitting verification..."); - const step2Data = new URLSearchParams({ '_wp_http2': wpHttp2, 'token': token }); - const responseStep2 = await fetch(action2Url, { - method: 'POST', - body: step2Data, - headers: { - 'Referer': responseStep1.url, - 'Content-Type': 'application/x-www-form-urlencoded', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36' - } - }); - - const html2 = await responseStep2.text(); - - // Step 4: Find dynamic cookie and link from JavaScript - console.log(" [SID] Step 4: Parsing final page for JS data..."); - let finalLinkPath = null; - let cookieName = null; - let cookieValue = null; - - // Look for the JavaScript patterns from the original - const cookieMatch = html2.match(/s_343\('([^']+)',\s*'([^']+)'/); - const linkMatch = html2.match(/c\.setAttribute\("href",\s*"([^"]+)"\)/); - - if (cookieMatch) { - cookieName = cookieMatch[1].trim(); - cookieValue = cookieMatch[2].trim(); - } - if (linkMatch) { - finalLinkPath = linkMatch[1].trim(); - } - - if (!finalLinkPath || !cookieName || !cookieValue) { - console.error(" [SID] Error: Could not extract dynamic cookie/link from JS."); - return null; - } - - const finalUrl = new URL(finalLinkPath, origin).href; - console.log(` [SID] Dynamic link found: ${finalUrl}`); - console.log(` [SID] Dynamic cookie found: ${cookieName}=${cookieValue}`); - - // Step 5: Set cookie and make final request - console.log(" [SID] Step 5: Setting cookie and making final request..."); - const cookieHeader = `${cookieName}=${cookieValue}`; - - const finalResponse = await fetch(finalUrl, { - headers: { - 'Referer': responseStep2.url, - 'Cookie': cookieHeader, - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36' - } - }); - - const finalHtml = await finalResponse.text(); - - // Step 6: Extract driveleech URL from meta refresh tag - const metaRefreshRegex = /]*http-equiv="refresh"[^>]*content="[^"]*url=([^"]*)"[^>]*>/i; - const metaMatch = metaRefreshRegex.exec(finalHtml); - - if (metaMatch && metaMatch[1]) { - const driveleechUrl = metaMatch[1].replace(/['"]/g, ''); - console.log(` [SID] SUCCESS! Resolved Driveleech URL: ${driveleechUrl}`); - return driveleechUrl; - } - - console.error(" [SID] Error: Could not find meta refresh tag with Driveleech URL."); - return null; - - } catch (error) { - console.error(` [SID] Error during SID resolution: ${error.message}`); - return null; - } -} - -// Function to try Instant Download method -async function tryInstantDownload(html) { - // Look for video-seed.pro or video-leech.pro links (the actual instant download pattern) - const videoSeedRegex = /href="([^"]*(?:video-seed\.pro|video-leech\.pro)[^"]*)"/i; - const match = videoSeedRegex.exec(html); - - if (!match || !match[1]) { - return null; - } - - const instantDownloadLink = match[1]; - console.log('[UHDMovies] Found "Instant Download" link, attempting to extract final URL...'); - - try { - const url = new URL(instantDownloadLink); - const keys = url.searchParams.get('url'); - - if (keys) { - const apiUrl = `${url.origin}/api`; - const formData = new URLSearchParams(); - formData.append('keys', keys); - - const apiResponse = await fetch(apiUrl, { - method: 'POST', - body: formData, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'x-token': url.hostname, - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' - } - }); - - if (apiResponse.ok) { - const responseData = await apiResponse.json(); - if (responseData && responseData.url) { - let finalUrl = responseData.url; - // Fix spaces in workers.dev URLs by encoding them properly - if (finalUrl.includes('workers.dev')) { - const urlParts = finalUrl.split('/'); - const filename = urlParts[urlParts.length - 1]; - const encodedFilename = filename.replace(/ /g, '%20'); - urlParts[urlParts.length - 1] = encodedFilename; - finalUrl = urlParts.join('/'); - } - console.log('[UHDMovies] Extracted final link from API:', finalUrl); - return finalUrl; - } - } - } - - console.log('[UHDMovies] Could not find a valid final download link from Instant Download.'); - return null; - } catch (error) { - console.log(`[UHDMovies] Error processing "Instant Download": ${error.message}`); - return null; - } -} - -// Function to try Resume Cloud method -async function tryResumeCloud(html) { - // Try multiple patterns to match the resume cloud button - const patterns = [ - /]*href="([^"]*)"[^>]*class="[^"]*btn-warning[^"]*"[^>]*>.*?Resume Cloud.*?<\/a>/i, - /href="([^"]*zfile[^"]*)"/i, - /]*href="([^"]*)"[^>]*>.*?Resume Cloud.*?<\/a>/i - ]; - - let resumeLink = null; - for (const pattern of patterns) { - const match = pattern.exec(html); - if (match && match[1]) { - resumeLink = match[1]; - console.log(`[UHDMovies] Found "Resume Cloud" link using pattern: ${resumeLink}`); - break; - } - } - - if (!resumeLink) { - console.log('[UHDMovies] No Resume Cloud link found'); - return null; - } - - // Check if it's already a direct download link (workers.dev) - if (resumeLink.includes('workers.dev')) { - let directLink = resumeLink; - // Fix spaces in workers.dev URLs by encoding them properly - const urlParts = directLink.split('/'); - const filename = urlParts[urlParts.length - 1]; - const encodedFilename = filename.replace(/ /g, '%20'); - urlParts[urlParts.length - 1] = encodedFilename; - directLink = urlParts.join('/'); - console.log(`[UHDMovies] Found direct "Cloud Resume Download" link: ${directLink}`); - return directLink; - } - - // Otherwise, follow the link to get the final download - try { - const resumeUrl = resumeLink.startsWith('http') ? resumeLink : new URL(resumeLink, 'https://driveleech.net').href; - console.log(`[UHDMovies] Found 'Resume Cloud' page link. Following to: ${resumeUrl}`); - - const finalPageResponse = await makeRequest(resumeUrl); - const finalPageHtml = await finalPageResponse.text(); - - // Look for direct download links with multiple patterns - const downloadLinkPatterns = [ - /]*class="[^"]*btn-success[^"]*"[^>]*href="([^"]*workers\.dev[^"]*)"[^>]*>/i, - /]*href="([^"]*workers\.dev[^"]*)"[^>]*class="[^"]*btn-success[^"]*"[^>]*>/i, - /]*href="([^"]*driveleech\.net\/d\/[^"]*)"[^>]*>/i, - /]*href="([^"]*)"[^>]*>.*?Download.*?<\/a>/i - ]; - - let finalDownloadLink = null; - for (const pattern of downloadLinkPatterns) { - const linkMatch = pattern.exec(finalPageHtml); - if (linkMatch && linkMatch[1]) { - finalDownloadLink = linkMatch[1]; - break; - } - } - - if (finalDownloadLink) { - // Fix spaces in workers.dev URLs by encoding them properly - if (finalDownloadLink.includes('workers.dev')) { - const urlParts = finalDownloadLink.split('/'); - const filename = urlParts[urlParts.length - 1]; - const encodedFilename = filename.replace(/ /g, '%20'); - urlParts[urlParts.length - 1] = encodedFilename; - finalDownloadLink = urlParts.join('/'); - } - console.log(`[UHDMovies] Extracted final Resume Cloud link: ${finalDownloadLink}`); - return finalDownloadLink; - } else { - console.log('[UHDMovies] Could not find the final download link on the "Resume Cloud" page.'); - return null; - } - } catch (error) { - console.log(`[UHDMovies] Error processing "Resume Cloud": ${error.message}`); - return null; - } -} - -// Validate if a video URL is working (not 404 or broken) -async function validateVideoUrl(url, timeout = 10000) { - try { - console.log(`[UHDMovies] Validating URL: ${url.substring(0, 100)}...`); - const response = await fetch(url, { - method: 'HEAD', - headers: { - 'Range': 'bytes=0-1', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' - } - }); - - if (response.ok || response.status === 206) { - console.log(`[UHDMovies] ✓ URL validation successful (${response.status})`); - return true; - } else { - console.log(`[UHDMovies] ✗ URL validation failed with status: ${response.status}`); - return false; - } - } catch (error) { - console.log(`[UHDMovies] ✗ URL validation failed: ${error.message}`); - return false; - } -} - -// Function to get final download URL from driveleech page -async function getFinalLink(driveleechUrl) { - try { - console.log(`[UHDMovies] Processing driveleech page: ${driveleechUrl}`); - - const response = await makeRequest(driveleechUrl); - const html = await response.text(); - - // Check for JavaScript redirect - const jsRedirectRegex = /window\.location\.replace\("([^"]+)"\)/; - const jsMatch = jsRedirectRegex.exec(html); - - let finalHtml = html; - if (jsMatch) { - const newUrl = new URL(jsMatch[1], 'https://driveleech.net/').href; - console.log(`[UHDMovies] Found JavaScript redirect to: ${newUrl}`); - const newResponse = await makeRequest(newUrl); - finalHtml = await newResponse.text(); - } - - // Extract size and filename information - let sizeInfo = 'Unknown'; - let fileName = null; - - const sizeRegex = /Size\s*:\s*([0-9.,]+\s*[KMGT]B)/i; - const sizeMatch = sizeRegex.exec(finalHtml); - if (sizeMatch) { - sizeInfo = sizeMatch[1]; - } - - const nameRegex = /Name\s*:\s*([^<\n]+)/i; - const nameMatch = nameRegex.exec(finalHtml); - if (nameMatch) { - fileName = nameMatch[1].trim(); - } - - // Try download methods - const downloadMethods = [ - { name: 'Resume Cloud', func: tryResumeCloud }, - { name: 'Instant Download', func: tryInstantDownload } - ]; - - for (const method of downloadMethods) { - try { - console.log(`[UHDMovies] Trying ${method.name}...`); - const finalUrl = await method.func(finalHtml); - - if (finalUrl) { - // Check if URL validation is enabled - if (typeof URL_VALIDATION_ENABLED !== 'undefined' && !URL_VALIDATION_ENABLED) { - console.log(`[UHDMovies] ✓ URL validation disabled, accepting ${method.name} result`); - return { url: finalUrl, size: sizeInfo, fileName: fileName }; - } - - const isValid = await validateVideoUrl(finalUrl); - if (isValid) { - console.log(`[UHDMovies] ✓ Successfully resolved using ${method.name}`); - return { url: finalUrl, size: sizeInfo, fileName: fileName }; - } else { - console.log(`[UHDMovies] ✗ ${method.name} returned invalid URL, trying next method...`); - } - } - } catch (error) { - console.log(`[UHDMovies] ✗ ${method.name} failed: ${error.message}`); - } - } - - console.log('[UHDMovies] ✗ All download methods failed'); - return null; - - } catch (error) { - console.error(`[UHDMovies] Error in getFinalLink: ${error.message}`); - return null; - } -} - -// Resolve download links with full processing chain -async function resolveDownloadLink(linkInfo) { - try { - console.log(`[UHDMovies] Resolving link: ${linkInfo.quality}`); - - // Step 1: Resolve SID link to driveleech URL - let driveleechUrl = null; - - if (linkInfo.url.includes('tech.unblockedgames.world') || - linkInfo.url.includes('tech.examzculture.in') || - linkInfo.url.includes('tech.examdegree.site') || - linkInfo.url.includes('tech.creativeexpressionsblog.com')) { - driveleechUrl = await resolveSidToDriveleech(linkInfo.url); - } else if (linkInfo.url.includes('driveleech.net') || linkInfo.url.includes('driveseed.org')) { - driveleechUrl = linkInfo.url; - } - - if (!driveleechUrl) { - console.log(`[UHDMovies] Could not resolve SID link for ${linkInfo.quality}`); - return null; - } - - // Filter out non-driveleech/driveseed URLs - if (!driveleechUrl.includes('driveleech.net') && !driveleechUrl.includes('driveseed.org')) { - console.log(`[UHDMovies] Skipping non-driveleech URL: ${driveleechUrl}`); - return null; - } - - // Step 2: Get final streaming URL from driveleech page - const finalLinkInfo = await getFinalLink(driveleechUrl); - - if (!finalLinkInfo) { - console.log(`[UHDMovies] Could not get final link for ${linkInfo.quality}`); - return null; - } - - // Step 3: Return formatted stream info - const fileName = finalLinkInfo.fileName || linkInfo.quality; - const cleanFileName = fileName.replace(/\.[^/.]+$/, "").replace(/[._]/g, ' '); - - return { - name: `UHD Movies`, - title: `${cleanFileName}\n${finalLinkInfo.size}`, - url: finalLinkInfo.url, - quality: linkInfo.quality, - size: finalLinkInfo.size, - fileName: finalLinkInfo.fileName, - type: 'direct' - }; - - } catch (error) { - console.error(`[UHDMovies] Failed to resolve link: ${error.message}`); - return null; - } -} - -// Extract TV show download links from show page using Cheerio (same approach as Node.js version) -async function extractTvShowDownloadLinks(showPageUrl, targetSeason, targetEpisode) { - try { - console.log(`[UHDMovies] Extracting TV show links from: ${showPageUrl} for S${targetSeason}E${targetEpisode}`); - - const response = await makeRequest(showPageUrl); - const html = await response.text(); - - const links = []; - const $ = cheerio.load(html); - const showTitle = $('h1').first().text().trim(); - - // --- NEW LOGIC TO SCOPE SEARCH TO THE CORRECT SEASON --- - let inTargetSeason = false; - let qualityText = ''; - - $('.entry-content').find('*').each((index, element) => { - const $el = $(element); - const text = $el.text().trim(); - const seasonMatch = text.match(/^SEASON\s+(\d+)/i); - - // Check if we are entering a new season block - if (seasonMatch) { - const currentSeasonNum = parseInt(seasonMatch[1], 10); - if (currentSeasonNum == targetSeason) { - inTargetSeason = true; - console.log(`[UHDMovies] Entering Season ${targetSeason} block.`); - } else if (inTargetSeason) { - // We've hit the next season, so we stop. - console.log(`[UHDMovies] Exiting Season ${targetSeason} block, now in Season ${currentSeasonNum}.`); - inTargetSeason = false; - return false; // Exit .each() loop - } - } - - if (inTargetSeason) { - // This element is within the correct season's block. - - // Is this a quality header? (e.g., a
 or a 

with ) - // It often contains resolution, release group, etc. - const isQualityHeader = $el.is('pre, p:has(strong), p:has(b), h3, h4'); - if (isQualityHeader) { - const headerText = $el.text().trim(); - // Filter out irrelevant headers. We can be more aggressive here. - if (headerText.length > 5 && !/plot|download|screenshot|trailer|join|powered by|season/i.test(headerText) && !($el.find('a').length > 0)) { - qualityText = headerText; // Store the most recent quality header - } - } - - // Is this a paragraph with episode links? - if ($el.is('p') && $el.find('a[href*="tech.unblockedgames.world"], a[href*="tech.examzculture.in"], a[href*="tech.examdegree.site"]').length > 0) { - const linksParagraph = $el; - const episodeRegex = new RegExp(`^Episode\\s+0*${targetEpisode}(?!\\d)`, 'i'); - const targetEpisodeLink = linksParagraph.find('a').filter((i, el) => { - return episodeRegex.test($(el).text().trim()); - }).first(); - - if (targetEpisodeLink.length > 0) { - const link = targetEpisodeLink.attr('href'); - if (link && !links.some(item => item.url === link)) { - const sizeMatch = qualityText.match(/\[\s*([0-9.,]+\s*[KMGT]B)/i); - const size = sizeMatch ? sizeMatch[1] : 'Unknown'; - - const cleanQuality = extractCleanQuality(qualityText); - const rawQuality = qualityText.replace(/(\r\n|\n|\r)/gm, " ").replace(/\s+/g, ' ').trim(); - - console.log(`[UHDMovies] Found match: Quality='${qualityText}', Link='${link}'`); - links.push({ - url: link, - quality: cleanQuality, - size: size, - rawQuality: rawQuality - }); - } - } - } - } - }); - - if (links.length === 0) { - console.log('[UHDMovies] Main extraction logic failed. Trying fallback method with season filtering.'); - $('.entry-content').find('a[href*="tech.unblockedgames.world"], a[href*="tech.examzculture.in"], a[href*="tech.examdegree.site"]').each((i, el) => { - const linkElement = $(el); - const episodeRegex = new RegExp(`^Episode\\s+0*${targetEpisode}(?!\\d)`, 'i'); - - if (episodeRegex.test(linkElement.text().trim())) { - const link = linkElement.attr('href'); - if (link && !links.some(item => item.url === link)) { - let qualityText = 'Unknown Quality'; - const parentP = linkElement.closest('p, div'); - - // Look for season information in the quality text and surrounding context - let foundSeasonMatch = false; - - // Check previous elements for quality and season info - let currentElement = parentP; - for (let j = 0; j < 10; j++) { - currentElement = currentElement.prev(); - if (currentElement.length === 0) break; - - const prevText = currentElement.text().trim(); - if (prevText && prevText.length > 5) { - // Check if this text contains season information - const seasonRegex = new RegExp(`S0?${targetSeason}(?![0-9])`, 'i'); - const seasonWordRegex = new RegExp(`Season\\s+0*${targetSeason}(?![0-9])`, 'i'); - - if (seasonRegex.test(prevText) || seasonWordRegex.test(prevText)) { - qualityText = prevText; - foundSeasonMatch = true; - break; - } - - // If we find a different season, skip this link - const otherSeasonRegex = /S0?(\d+)(?![0-9])|Season\s+(\d+)(?![0-9])/i; - const otherSeasonMatch = otherSeasonRegex.exec(prevText); - if (otherSeasonMatch) { - const foundSeason = parseInt(otherSeasonMatch[1] || otherSeasonMatch[2]); - if (foundSeason !== targetSeason) { - console.log(`[UHDMovies] Skipping link - found Season ${foundSeason}, looking for Season ${targetSeason}`); - return; // Skip this link - } - } - } - } - - // Only add the link if we found a season match or no season info at all - if (foundSeasonMatch || qualityText === 'Unknown Quality') { - if (qualityText === 'Unknown Quality') { - // Last resort: check immediate previous element - const prevElement = parentP.prev(); - if (prevElement.length > 0) { - const prevText = prevElement.text().trim(); - if (prevText && prevText.length > 5 && !prevText.toLowerCase().includes('download')) { - qualityText = prevText; - } - } - } - - const sizeMatch = qualityText.match(/\[([0-9.,]+[KMGT]B[^\]]*)\]/i); - const size = sizeMatch ? sizeMatch[1] : 'Unknown'; - const cleanQuality = extractCleanQuality(qualityText); - const rawQuality = qualityText.replace(/(\r\n|\n|\r)/gm, " ").replace(/\s+/g, ' ').trim(); - - console.log(`[UHDMovies] Found match via fallback: Quality='${qualityText}', Link='${link}'`); - links.push({ - url: link, - quality: cleanQuality, - size: size, - rawQuality: rawQuality - }); - } - } - } - }); - } - - console.log(`[UHDMovies] Found ${links.length} episode links for S${targetSeason}E${targetEpisode}`); - return links; - } catch (error) { - console.error(`[UHDMovies] Failed to extract TV show links: ${error.message}`); - return []; - } -} - -// Main function - this is the interface our local scraper service expects -async function getStreams(tmdbId, mediaType = 'movie', season = null, episode = null) { - console.log(`[UHDMovies] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}${mediaType === 'tv' ? `, S:${season}E:${episode}` : ''}`); - - try { - // Get TMDB info - const tmdbUrl = `https://api.themoviedb.org/3/${mediaType === 'tv' ? 'tv' : 'movie'}/${tmdbId}?api_key=${TMDB_API_KEY}`; - const tmdbResponse = await makeRequest(tmdbUrl); - const tmdbData = await tmdbResponse.json(); - - const mediaInfo = { - title: mediaType === 'tv' ? tmdbData.name : tmdbData.title, - year: parseInt(((mediaType === 'tv' ? tmdbData.first_air_date : tmdbData.release_date) || '').split('-')[0], 10) - }; - - if (!mediaInfo.title) { - throw new Error('Could not extract title from TMDB response'); - } - - console.log(`[UHDMovies] TMDB Info: "${mediaInfo.title}" (${mediaInfo.year || 'N/A'})`); - - // Search for the media - let searchTitle = mediaInfo.title.replace(/:/g, '').replace(/\s*&\s*/g, ' and '); - let searchResults = await searchMovies(searchTitle); - - // Try fallback search if no results - if (searchResults.length === 0 || !searchResults.some(result => compareMedia(mediaInfo, result))) { - console.log(`[UHDMovies] Primary search failed, trying fallback...`); - const fallbackTitle = mediaInfo.title.split(':')[0].trim(); - if (fallbackTitle !== searchTitle) { - searchResults = await searchMovies(fallbackTitle); - } - } - - if (searchResults.length === 0) { - console.log(`[UHDMovies] No search results found`); - return []; - } - - // Find best match - const bestMatch = searchResults.find(result => compareMedia(mediaInfo, result)) || searchResults[0]; - console.log(`[UHDMovies] Using result: "${bestMatch.title}" (${bestMatch.year})`); - - // Extract download links based on media type - let downloadLinks = []; - if (mediaType === 'tv' && season && episode) { - downloadLinks = await extractTvShowDownloadLinks(bestMatch.url, season, episode); - } else { - downloadLinks = await extractDownloadLinks(bestMatch.url); - } - - if (downloadLinks.length === 0) { - console.log(`[UHDMovies] No download links found`); - return []; - } - - // Resolve links to streams - const streamPromises = downloadLinks.map(link => resolveDownloadLink(link)); - const streams = (await Promise.all(streamPromises)).filter(Boolean); - - // Sort by size (largest first) - streams.sort((a, b) => { - const sizeA = parseSize(a.size); - const sizeB = parseSize(b.size); - return sizeB - sizeA; - }); - - console.log(`[UHDMovies] Successfully processed ${streams.length} streams`); - return streams; - - } catch (error) { - console.error(`[UHDMovies] Error in getStreams: ${error.message}`); - return []; - } -} - -// Export the main function -if (typeof module !== 'undefined' && module.exports) { - module.exports = { getStreams }; -} else { - // For React Native environment - global.getStreams = getStreams; -} \ No newline at end of file diff --git a/local-scrapers-repo/providers/watch32.js b/local-scrapers-repo/providers/watch32.js deleted file mode 100644 index 299ed97..0000000 --- a/local-scrapers-repo/providers/watch32.js +++ /dev/null @@ -1,547 +0,0 @@ -// Watch32 Scraper for Nuvio Local Scrapers -// React Native compatible version - Standalone (no external dependencies) - -// Import cheerio-without-node-native for React Native -const cheerio = require('cheerio-without-node-native'); -console.log('[Watch32] Using cheerio-without-node-native for DOM parsing'); - -// Constants -const TMDB_API_KEY = "439c478a771f35c05022f9feabcca01c"; -const MAIN_URL = 'https://watch32.sx'; -const VIDEOSTR_URL = 'https://videostr.net'; - -// Helper function to make HTTP requests -function makeRequest(url, options = {}) { - const defaultHeaders = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', - 'Accept-Language': 'en-US,en;q=0.5', - 'Connection': 'keep-alive' - }; - - return fetch(url, { - method: options.method || 'GET', - headers: { ...defaultHeaders, ...options.headers }, - ...options - }) - .then(response => { - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - return response; - }) - .catch(error => { - console.error(`[Watch32] Request failed for ${url}: ${error.message}`); - throw error; - }); -} - -// Search for content -function searchContent(query) { - const searchUrl = `${MAIN_URL}/search/${query.replace(/\s+/g, '-')}`; - console.log(`[Watch32] Searching: ${searchUrl}`); - - return makeRequest(searchUrl) - .then(response => response.text()) - .then(html => { - const $ = cheerio.load(html); - const results = []; - - $('.flw-item').each((i, element) => { - const title = $(element).find('h2.film-name > a').attr('title'); - const link = $(element).find('h2.film-name > a').attr('href'); - const poster = $(element).find('img.film-poster-img').attr('data-src'); - - if (title && link) { - results.push({ - title, - url: link.startsWith('http') ? link : `${MAIN_URL}${link}`, - poster - }); - } - }); - - console.log(`[Watch32] Found ${results.length} search results`); - return results; - }) - .catch(error => { - console.error(`[Watch32] Search error: ${error.message}`); - return []; - }); -} - -// Get content details (movie or TV series) -function getContentDetails(url) { - console.log(`[Watch32] Getting content details: ${url}`); - - return makeRequest(url) - .then(response => response.text()) - .then(html => { - const $ = cheerio.load(html); - const contentId = $('.detail_page-watch').attr('data-id'); - const name = $('.detail_page-infor h2.heading-name > a').text(); - const isMovie = url.includes('movie'); - - if (isMovie) { - return { - type: 'movie', - name, - data: `list/${contentId}` - }; - } else { - // Get TV series episodes - return makeRequest(`${MAIN_URL}/ajax/season/list/${contentId}`) - .then(response => response.text()) - .then(seasonsHtml => { - const $seasons = cheerio.load(seasonsHtml); - const episodes = []; - const seasonPromises = []; - - $seasons('a.ss-item').each((i, season) => { - const seasonId = $(season).attr('data-id'); - const seasonNum = $(season).text().replace('Season ', ''); - - const episodePromise = makeRequest(`${MAIN_URL}/ajax/season/episodes/${seasonId}`) - .then(response => response.text()) - .then(episodesHtml => { - const $episodes = cheerio.load(episodesHtml); - - $episodes('a.eps-item').each((i, episode) => { - const epId = $(episode).attr('data-id'); - const title = $(episode).attr('title'); - const match = title.match(/Eps (\d+): (.+)/); - - if (match) { - episodes.push({ - id: epId, - episode: parseInt(match[1]), - name: match[2], - season: parseInt(seasonNum.replace('Series', '').trim()), - data: `servers/${epId}` - }); - } - }); - }); - - seasonPromises.push(episodePromise); - }); - - return Promise.all(seasonPromises) - .then(() => ({ - type: 'series', - name, - episodes - })); - }); - } - }) - .catch(error => { - console.error(`[Watch32] Content details error: ${error.message}`); - return null; - }); -} - -// Get server links for content -function getServerLinks(data) { - console.log(`[Watch32] Getting server links: ${data}`); - - return makeRequest(`${MAIN_URL}/ajax/episode/${data}`) - .then(response => response.text()) - .then(html => { - const $ = cheerio.load(html); - const servers = []; - - $('a.link-item').each((i, element) => { - const linkId = $(element).attr('data-linkid') || $(element).attr('data-id'); - if (linkId) { - servers.push(linkId); - } - }); - - return servers; - }) - .catch(error => { - console.error(`[Watch32] Server links error: ${error.message}`); - return []; - }); -} - -// Get source URL from link ID -function getSourceUrl(linkId) { - console.log(`[Watch32] Getting source URL for linkId: ${linkId}`); - - return makeRequest(`${MAIN_URL}/ajax/episode/sources/${linkId}`) - .then(response => response.json()) - .then(data => data.link) - .catch(error => { - console.error(`[Watch32] Source URL error: ${error.message}`); - return null; - }); -} - -// Extract M3U8 from Videostr -function extractVideostrM3u8(url) { - console.log(`[Watch32] Extracting from Videostr: ${url}`); - - const headers = { - 'Accept': '*/*', - 'X-Requested-With': 'XMLHttpRequest', - 'Referer': VIDEOSTR_URL, - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; Win64; x64) AppleWebKit/537.36' - }; - - // Extract ID from URL - const id = url.split('/').pop().split('?')[0]; - - // Get nonce from embed page - return makeRequest(url, { headers }) - .then(response => response.text()) - .then(embedHtml => { - // Try to find 48-character nonce - let nonce = embedHtml.match(/\b[a-zA-Z0-9]{48}\b/); - if (nonce) { - nonce = nonce[0]; - } else { - // Try to find three 16-character segments - const matches = embedHtml.match(/\b([a-zA-Z0-9]{16})\b.*?\b([a-zA-Z0-9]{16})\b.*?\b([a-zA-Z0-9]{16})\b/); - if (matches) { - nonce = matches[1] + matches[2] + matches[3]; - } - } - - if (!nonce) { - throw new Error('Could not extract nonce'); - } - - console.log(`[Watch32] Extracted nonce: ${nonce}`); - - // Get sources from API - const apiUrl = `${VIDEOSTR_URL}/embed-1/v3/e-1/getSources?id=${id}&_k=${nonce}`; - console.log(`[Watch32] API URL: ${apiUrl}`); - - return makeRequest(apiUrl, { headers }) - .then(response => response.json()) - .then(sourcesData => { - console.log('[Watch32] Sources data:', JSON.stringify(sourcesData, null, 2)); - - if (!sourcesData.sources || sourcesData.sources.length === 0) { - throw new Error('No sources found in response'); - } - - // Get the first source file (matching Kotlin logic) - const encoded = sourcesData.sources[0].file; - console.log('[Watch32] Encoded source:', encoded); - - // Check if sources is already an M3U8 URL - if (encoded.includes('.m3u8')) { - console.log('[Watch32] Source is already M3U8 URL'); - return encoded; - } - - console.log('[Watch32] Sources are encrypted, attempting to decrypt...'); - - // Get decryption key - use 'mega' key like Kotlin version - return makeRequest('https://raw.githubusercontent.com/yogesh-hacker/MegacloudKeys/refs/heads/main/keys.json') - .then(response => response.json()) - .then(keyData => { - console.log('[Watch32] Key data:', JSON.stringify(keyData, null, 2)); - - const key = keyData.mega; // Use 'mega' key like Kotlin - - if (!key) { - throw new Error('Could not get decryption key (mega)'); - } - - console.log('[Watch32] Using mega key for decryption'); - - // Decrypt using Google Apps Script - exact same logic as Kotlin - const decodeUrl = 'https://script.google.com/macros/s/AKfycbxHbYHbrGMXYD2-bC-C43D3njIbU-wGiYQuJL61H4vyy6YVXkybMNNEPJNPPuZrD1gRVA/exec'; - const fullUrl = `${decodeUrl}?encrypted_data=${encodeURIComponent(encoded)}&nonce=${encodeURIComponent(nonce)}&secret=${encodeURIComponent(key)}`; - - console.log('[Watch32] Decryption URL:', fullUrl); - - return makeRequest(fullUrl) - .then(response => response.text()) - .then(decryptedData => { - console.log('[Watch32] Decrypted response:', decryptedData); - - // Extract file URL from decrypted response - exact same regex as Kotlin - const fileMatch = decryptedData.match(/"file":"(.*?)"/); - if (fileMatch) { - const m3u8Url = fileMatch[1]; - console.log('[Watch32] Extracted M3U8 URL:', m3u8Url); - return m3u8Url; - } else { - throw new Error('Video URL not found in decrypted response'); - } - }); - }); - }) - .then(finalM3u8Url => { - console.log(`[Watch32] Final M3U8 URL: ${finalM3u8Url}`); - - // Accept both megacdn and other reliable CDN links - if (!finalM3u8Url.includes('megacdn.co') && !finalM3u8Url.includes('akmzed.cloud') && !finalM3u8Url.includes('sunnybreeze')) { - console.log('[Watch32] Skipping unreliable CDN link'); - return null; - } - - // Parse master playlist to extract quality streams - return parseM3U8Qualities(finalM3u8Url) - .then(qualities => ({ - m3u8Url: finalM3u8Url, - qualities, - headers: { - 'Referer': 'https://videostr.net/', - 'Origin': 'https://videostr.net/', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' - } - })); - }); - }) - .catch(error => { - console.error(`[Watch32] Videostr extraction error: ${error.message}`); - return null; - }); -} - -// Parse M3U8 master playlist to extract qualities -function parseM3U8Qualities(masterUrl) { - return makeRequest(masterUrl, { - headers: { - 'Referer': 'https://videostr.net/', - 'Origin': 'https://videostr.net/' - } - }) - .then(response => response.text()) - .then(playlist => { - const qualities = []; - - // Parse M3U8 master playlist - const lines = playlist.split('\n'); - for (let i = 0; i < lines.length; i++) { - const line = lines[i].trim(); - if (line.startsWith('#EXT-X-STREAM-INF:')) { - const nextLine = lines[i + 1]?.trim(); - if (nextLine && !nextLine.startsWith('#')) { - // Extract resolution and bandwidth - const resolutionMatch = line.match(/RESOLUTION=(\d+x\d+)/); - const bandwidthMatch = line.match(/BANDWIDTH=(\d+)/); - - const resolution = resolutionMatch ? resolutionMatch[1] : 'Unknown'; - const bandwidth = bandwidthMatch ? parseInt(bandwidthMatch[1]) : 0; - - // Determine quality label - let quality = 'Unknown'; - if (resolution.includes('1920x1080')) quality = '1080p'; - else if (resolution.includes('1280x720')) quality = '720p'; - else if (resolution.includes('640x360')) quality = '360p'; - else if (resolution.includes('854x480')) quality = '480p'; - - qualities.push({ - quality, - resolution, - bandwidth, - url: nextLine.startsWith('http') ? nextLine : new URL(nextLine, masterUrl).href - }); - } - } - } - - // Sort by bandwidth (highest first) - qualities.sort((a, b) => b.bandwidth - a.bandwidth); - - return qualities; - }) - .catch(error => { - console.error(`[Watch32] Error parsing M3U8 qualities: ${error.message}`); - return []; - }); -} - -// Main scraping function -function getStreams(tmdbId, mediaType, season, episode) { - console.log(`[Watch32] Searching for: ${tmdbId} (${mediaType})`); - - // First, get movie/TV show details from TMDB - const tmdbUrl = `https://api.themoviedb.org/3/${mediaType}/${tmdbId}?api_key=${TMDB_API_KEY}`; - - return makeRequest(tmdbUrl) - .then(response => response.json()) - .then(tmdbData => { - const title = mediaType === 'tv' ? tmdbData.name : tmdbData.title; - const year = mediaType === 'tv' ? tmdbData.first_air_date?.substring(0, 4) : tmdbData.release_date?.substring(0, 4); - - if (!title) { - throw new Error('Could not extract title from TMDB response'); - } - - console.log(`[Watch32] TMDB Info: "${title}" (${year || 'N/A'})`); - - // Build search query - use title instead of TMDB ID - const query = year ? `${title} ${year}` : title; - - return searchContent(query).then(searchResults => ({ searchResults, query, tmdbData })); - }) - .then(({ searchResults, query, tmdbData }) => { - if (searchResults.length === 0) { - console.log('[Watch32] No search results found'); - return []; - } - - console.log(`[Watch32] Found ${searchResults.length} results`); - - // Try to find exact match first, then partial match - let selectedResult = searchResults.find(result => - result.title.toLowerCase() === query.toLowerCase() - ); - - if (!selectedResult) { - // Look for best partial match (contains all words from query) - const queryWords = query.toLowerCase().split(' '); - selectedResult = searchResults.find(result => { - const titleLower = result.title.toLowerCase(); - return queryWords.every(word => titleLower.includes(word)); - }); - } - - // Fallback to first result if no good match found - if (!selectedResult) { - selectedResult = searchResults[0]; - } - - console.log(`[Watch32] Selected: ${selectedResult.title}`); - - // Get content details - return getContentDetails(selectedResult.url).then(contentDetails => ({ contentDetails, tmdbData })); - }) - .then(({ contentDetails, tmdbData }) => { - if (!contentDetails) { - console.log('[Watch32] Could not get content details'); - return []; - } - - let itemsToProcess = []; - - if (contentDetails.type === 'movie') { - itemsToProcess.push({ data: contentDetails.data, episodeMeta: null }); - } else { - // For TV series, filter by episode/season if specified - let episodes = contentDetails.episodes; - - if (season) { - episodes = episodes.filter(ep => ep.season === season); - } - - if (episode) { - episodes = episodes.filter(ep => ep.episode === episode); - } - - if (episodes.length === 0) { - console.log('[Watch32] No matching episodes found'); - return []; - } - - // Process all matching episodes in parallel - episodes.forEach(ep => { - console.log(`[Watch32] Queue episode: S${ep.season}E${ep.episode} - ${ep.name}`); - itemsToProcess.push({ data: ep.data, episodeMeta: ep }); - }); - } - - // Process all data - const allPromises = itemsToProcess.map(item => { - return getServerLinks(item.data) - .then(serverLinks => { - console.log(`[Watch32] Found ${serverLinks.length} servers`); - - // Process all server links - const linkPromises = serverLinks.map(linkId => { - return getSourceUrl(linkId) - .then(sourceUrl => { - if (!sourceUrl) return null; - - console.log(`[Watch32] Source URL: ${sourceUrl}`); - - // Check if it's a videostr URL - if (sourceUrl.includes('videostr.net')) { - return extractVideostrM3u8(sourceUrl); - } - return null; - }) - .catch(error => { - console.error(`[Watch32] Error processing link ${linkId}: ${error.message}`); - return null; - }); - }); - - return Promise.all(linkPromises); - }) - .then(results => ({ results, episodeMeta: item.episodeMeta })); - }); - - return Promise.all(allPromises).then(resultsWithMeta => ({ resultsWithMeta, tmdbData, contentDetails })); - }) - .then(({ resultsWithMeta, tmdbData, contentDetails }) => { - // Flatten and filter results - const allM3u8Links = []; - for (const item of resultsWithMeta) { - const serverResults = item.results; - for (const result of serverResults) { - if (result) { - allM3u8Links.push({ link: result, episodeMeta: item.episodeMeta }); - } - } - } - - // Build title with year and episode info - const title = mediaType === 'tv' ? tmdbData.name : tmdbData.title; - const year = mediaType === 'tv' ? tmdbData.first_air_date?.substring(0, 4) : tmdbData.release_date?.substring(0, 4); - - // Convert to Nuvio format - const formattedLinks = []; - - allM3u8Links.forEach(item => { - const link = item.link; - const episodeMeta = item.episodeMeta; - let perItemTitle = `${title} (${year || 'N/A'})`; - if (mediaType === 'tv' && episodeMeta) { - perItemTitle += ` - S${episodeMeta.season}E${episodeMeta.episode}`; - } - if (link.qualities && link.qualities.length > 0) { - link.qualities.forEach(quality => { - formattedLinks.push({ - name: `Watch32 - ${quality.quality}`, - title: perItemTitle, - url: quality.url, - quality: quality.quality, - headers: link.headers || {}, - subtitles: [] - }); - }); - } else { - // Skip unknown quality links - console.log('[Watch32] Skipping unknown quality link'); - } - }); - - console.log(`[Watch32] Total found: ${formattedLinks.length} streams`); - return formattedLinks; - }) - .catch(error => { - console.error(`[Watch32] Scraping error: ${error.message}`); - return []; - }) - .catch(error => { - console.error(`[Watch32] TMDB API error: ${error.message}`); - return []; - }); -} - -// Export the main function -if (typeof module !== 'undefined' && module.exports) { - module.exports = { getStreams }; -} else { - // For React Native environment - global.Watch32ScraperModule = { getStreams }; -} \ No newline at end of file diff --git a/local-scrapers-repo/providers/xprime.js b/local-scrapers-repo/providers/xprime.js deleted file mode 100644 index 635207d..0000000 --- a/local-scrapers-repo/providers/xprime.js +++ /dev/null @@ -1,800 +0,0 @@ -// Xprime Scraper for Nuvio Local Scrapers -// React Native compatible version - Standalone (no external dependencies) - -// TMDB API Configuration -const TMDB_API_KEY = '439c478a771f35c05022f9feabcca01c'; -const TMDB_BASE_URL = 'https://api.themoviedb.org/3'; - -// Working headers for Cloudflare Workers URLs -const WORKING_HEADERS = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - 'Accept': 'video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5', - 'Accept-Language': 'en-US,en;q=0.9', - 'Accept-Encoding': 'identity', - 'Origin': 'https://xprime.tv', - 'Referer': 'https://xprime.tv/', - 'Sec-Fetch-Dest': 'video', - 'Sec-Fetch-Mode': 'no-cors', - 'Sec-Fetch-Site': 'cross-site', - 'DNT': '1' -}; - -// M3U8 Resolver Functions (inlined to remove external dependency) - -// Parse M3U8 content and extract quality streams -function parseM3U8(content, baseUrl) { - const lines = content.split('\n').map(line => line.trim()).filter(line => line); - const streams = []; - - let currentStream = null; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - - if (line.startsWith('#EXT-X-STREAM-INF:')) { - // Parse stream info - currentStream = { - bandwidth: null, - resolution: null, - codecs: null, - url: null - }; - - // Extract bandwidth - const bandwidthMatch = line.match(/BANDWIDTH=(\d+)/); - if (bandwidthMatch) { - currentStream.bandwidth = parseInt(bandwidthMatch[1]); - } - - // Extract resolution - const resolutionMatch = line.match(/RESOLUTION=(\d+x\d+)/); - if (resolutionMatch) { - currentStream.resolution = resolutionMatch[1]; - } - - // Extract codecs - const codecsMatch = line.match(/CODECS="([^"]+)"/); - if (codecsMatch) { - currentStream.codecs = codecsMatch[1]; - } - - } else if (currentStream && !line.startsWith('#')) { - // This is the URL for the current stream - currentStream.url = resolveUrl(line, baseUrl); - streams.push(currentStream); - currentStream = null; - } - } - - return streams; -} - -// Resolve relative URLs against base URL -function resolveUrl(url, baseUrl) { - if (url.startsWith('http')) { - return url; - } - - try { - return new URL(url, baseUrl).toString(); - } catch (error) { - console.log(`⚠️ Could not resolve URL: ${url} against ${baseUrl}`); - return url; - } -} - -// Determine quality from resolution or bandwidth -function getQualityFromStream(stream) { - if (stream.resolution) { - const [width, height] = stream.resolution.split('x').map(Number); - - if (height >= 2160) return '4K'; - if (height >= 1440) return '1440p'; - if (height >= 1080) return '1080p'; - if (height >= 720) return '720p'; - if (height >= 480) return '480p'; - if (height >= 360) return '360p'; - return '240p'; - } - - if (stream.bandwidth) { - const mbps = stream.bandwidth / 1000000; - - if (mbps >= 15) return '4K'; - if (mbps >= 8) return '1440p'; - if (mbps >= 5) return '1080p'; - if (mbps >= 3) return '720p'; - if (mbps >= 1.5) return '480p'; - if (mbps >= 0.8) return '360p'; - return '240p'; - } - - return 'Unknown'; -} - -// Fetch and resolve M3U8 playlist -function resolveM3U8(url, sourceName = 'Unknown') { - console.log(`🔍 Resolving M3U8 playlist for ${sourceName}...`); - console.log(`📡 URL: ${url.substring(0, 80)}...`); - - return fetch(url, { - method: 'GET', - headers: WORKING_HEADERS, - timeout: 15000 - }).then(function(response) { - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - - return response.text().then(function(content) { - console.log(`✅ Fetched M3U8 content (${content.length} bytes)`); - - // Check if it's a master playlist (contains #EXT-X-STREAM-INF) - if (content.includes('#EXT-X-STREAM-INF:')) { - console.log(`📋 Master playlist detected - parsing quality streams...`); - - const streams = parseM3U8(content, url); - console.log(`🎬 Found ${streams.length} quality streams`); - - const resolvedStreams = []; - - for (const stream of streams) { - const quality = getQualityFromStream(stream); - - // Extract clean server name from sourceName - const cleanServerName = sourceName.replace(/^XPRIME\s+/i, '').replace(/\s+-\s+.*$/, ''); - const formattedName = `XPRIME ${cleanServerName.charAt(0).toUpperCase() + cleanServerName.slice(1)} - ${quality}`; - - resolvedStreams.push({ - source: sourceName, - name: formattedName, - url: stream.url, - quality: quality, - resolution: stream.resolution, - bandwidth: stream.bandwidth, - codecs: stream.codecs, - type: 'M3U8', - headers: WORKING_HEADERS, - referer: 'https://xprime.tv' - }); - - console.log(` 📊 ${quality} (${stream.resolution || 'Unknown resolution'}) - ${Math.round((stream.bandwidth || 0) / 1000000 * 10) / 10} Mbps`); - } - - // Sort by quality (highest first) - resolvedStreams.sort((a, b) => { - const qualityOrder = { '4K': 4, '1440p': 3, '1080p': 2, '720p': 1, '480p': 0, '360p': -1, '240p': -2, 'Unknown': -3 }; - return (qualityOrder[b.quality] || -3) - (qualityOrder[a.quality] || -3); - }); - - return { - success: true, - type: 'master', - streams: resolvedStreams, - originalUrl: url - }; - - } else if (content.includes('#EXTINF:')) { - console.log(`📺 Media playlist detected - single quality stream`); - - // Extract clean server name from sourceName - const cleanServerName = sourceName.replace(/^XPRIME\s+/i, '').replace(/\s+-\s+.*$/, ''); - const formattedName = `XPRIME ${cleanServerName.charAt(0).toUpperCase() + cleanServerName.slice(1)} - Unknown`; - - return { - success: true, - type: 'media', - streams: [{ - source: sourceName, - name: formattedName, - url: url, - quality: 'Unknown', - type: 'M3U8', - headers: WORKING_HEADERS, - referer: 'https://xprime.tv' - }], - originalUrl: url - }; - - } else { - throw new Error('Invalid M3U8 content - no playlist markers found'); - } - }); - }).catch(function(error) { - console.log(`❌ Failed to resolve M3U8: ${error.message}`); - - return { - success: false, - error: error.message, - streams: [], - originalUrl: url - }; - }); -} - -// Resolve multiple M3U8 URLs -function resolveMultipleM3U8(links) { - console.log(`🚀 Resolving ${links.length} M3U8 playlists in parallel...`); - - const resolvePromises = links.map(function(link) { - return resolveM3U8(link.url, link.name).then(function(result) { - return { - originalLink: link, - resolution: result - }; - }); - }); - - return Promise.allSettled(resolvePromises).then(function(results) { - const allResolvedStreams = []; - const failedResolutions = []; - - for (const result of results) { - if (result.status === 'fulfilled') { - const { originalLink, resolution } = result.value; - - if (resolution.success) { - allResolvedStreams.push(...resolution.streams); - } else { - failedResolutions.push({ - link: originalLink, - error: resolution.error - }); - } - } else { - failedResolutions.push({ - link: 'Unknown', - error: result.reason.message - }); - } - } - - console.log(`\n📊 Resolution Summary:`); - console.log(`✅ Successfully resolved: ${allResolvedStreams.length} streams`); - console.log(`❌ Failed resolutions: ${failedResolutions.length}`); - - if (failedResolutions.length > 0) { - console.log(`\n❌ Failed resolutions:`); - failedResolutions.forEach((failure, index) => { - console.log(` ${index + 1}. ${failure.link.name || 'Unknown'}: ${failure.error}`); - }); - } - - return { - success: allResolvedStreams.length > 0, - streams: allResolvedStreams, - failed: failedResolutions, - summary: { - total: links.length, - resolved: allResolvedStreams.length, - failed: failedResolutions.length - } - }; - }); -} - -// Constants -const FALLBACK_DOMAIN = 'https://xprime.tv'; -const DOMAIN_CACHE_TTL = 4 * 60 * 60 * 1000; // 4 hours - -// Global variables for domain caching -let xprimeDomain = FALLBACK_DOMAIN; -let domainCacheTimestamp = 0; - -// Utility Functions -function getQualityFromName(qualityStr) { - if (!qualityStr) return 'Unknown'; - - const quality = qualityStr.toLowerCase(); - const qualityMap = { - '2160p': '4K', '4k': '4K', - '1440p': '1440p', '2k': '1440p', - '1080p': '1080p', 'fhd': '1080p', 'full hd': '1080p', - '720p': '720p', 'hd': '720p', - '480p': '480p', 'sd': '480p', - '360p': '360p', - '240p': '240p' - }; - - for (const [key, value] of Object.entries(qualityMap)) { - if (quality.includes(key)) return value; - } - - // Try to extract number from string and format consistently - const match = qualityStr.match(/(\d{3,4})[pP]?/); - if (match) { - const resolution = parseInt(match[1]); - if (resolution >= 2160) return '4K'; - if (resolution >= 1440) return '1440p'; - if (resolution >= 1080) return '1080p'; - if (resolution >= 720) return '720p'; - if (resolution >= 480) return '480p'; - if (resolution >= 360) return '360p'; - return '240p'; - } - - return 'Unknown'; -} - -// Fetch latest domain from GitHub -function getXprimeDomain() { - const now = Date.now(); - if (now - domainCacheTimestamp < DOMAIN_CACHE_TTL) { - return Promise.resolve(xprimeDomain); - } - - console.log('[Xprime] Fetching latest domain...'); - return fetch('https://raw.githubusercontent.com/phisher98/TVVVV/refs/heads/main/domains.json', { - method: 'GET', - headers: { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' - } - }).then(function(response) { - if (response.ok) { - return response.json().then(function(data) { - if (data && data.xprime) { - xprimeDomain = data.xprime; - domainCacheTimestamp = now; - console.log(`[Xprime] Updated domain to: ${xprimeDomain}`); - } - return xprimeDomain; - }); - } - return xprimeDomain; - }).catch(function(error) { - console.error(`[Xprime] Failed to fetch latest domain: ${error.message}`); - return xprimeDomain; - }); -} - -// Helper function to make HTTP requests -function makeRequest(url, options = {}) { - const defaultHeaders = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - 'Accept': 'application/json, text/plain, */*', - 'Accept-Language': 'en-US,en;q=0.9', - 'Accept-Encoding': 'gzip, deflate, br' - }; - - return fetch(url, { - method: options.method || 'GET', - headers: { ...defaultHeaders, ...options.headers }, - ...options - }).then(function(response) { - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - return response; - }).catch(function(error) { - console.error(`[Xprime] Request failed for ${url}: ${error.message}`); - throw error; - }); -} - -// Hardcoded Server List -function getXprimeServers(api) { - console.log('[Xprime] Using hardcoded servers...'); - const hardcodedServers = [ - { name: 'primebox', status: 'ok' }, - { name: 'rage', status: 'ok' }, - // Temporarily disabled Phoenix server - // { name: 'phoenix', status: 'ok' }, - // Temporarily disabled Fox server - // { name: 'fox', status: 'ok' } - ]; - console.log(`[Xprime] Using ${hardcodedServers.length} hardcoded servers: ${hardcodedServers.map(s => s.name).join(', ')}`); - return Promise.resolve(hardcodedServers); -} - -// Build Query Parameters -function buildQueryParams(serverName, title, year, id, season, episode) { - const params = new URLSearchParams(); - params.append('name', title || ''); - - if (serverName === 'primebox') { - if (year) params.append('fallback_year', year.toString()); - if (season && episode) { - params.append('season', season.toString()); - params.append('episode', episode.toString()); - } - } else { - if (year) params.append('year', year.toString()); - if (id) { - params.append('id', id); - params.append('imdb', id); - } - if (season && episode) { - params.append('season', season.toString()); - params.append('episode', episode.toString()); - } - } - - return params.toString(); -} - -// Process PrimeBox Response -function processPrimeBoxResponse(data, serverLabel, serverName) { - const links = []; - const subtitles = []; - - try { - if (data.streams) { - // Process quality streams - fix: use available_qualities instead of qualities - if (data.available_qualities && Array.isArray(data.available_qualities)) { - data.available_qualities.forEach(quality => { - const url = data.streams[quality]; - if (url) { - const normalizedQuality = getQualityFromName(quality); - links.push({ - source: serverLabel, - name: `XPRIME ${serverName.charAt(0).toUpperCase() + serverName.slice(1)} - ${normalizedQuality}`, - url: url.trim(), // Remove any whitespace - quality: normalizedQuality, - type: 'VIDEO', - headers: WORKING_HEADERS, - referer: 'https://xprime.tv' - }); - } - }); - } - } - - // Process subtitles - if (data.has_subtitles && data.subtitles && Array.isArray(data.subtitles)) { - data.subtitles.forEach(sub => { - if (sub.file) { - subtitles.push({ - language: sub.label || 'Unknown', - url: sub.file.trim() // Remove any whitespace - }); - } - }); - } - } catch (error) { - console.error(`[Xprime] Error parsing PrimeBox response: ${error.message}`); - } - - return { links, subtitles }; -} - -// Process Other Server Response -function processOtherServerResponse(data, serverLabel, serverName) { - const links = []; - - try { - // Special handling for Rage server response - if (serverName === 'rage' && data && data.success && Array.isArray(data.qualities)) { - data.qualities.forEach(function(q) { - if (q && q.url) { - const normalizedQuality = getQualityFromName(q.quality); - // Normalize size to a human-readable string - let sizeStr = 'Unknown'; - if (typeof q.size === 'number' && isFinite(q.size)) { - const gb = q.size / (1024 * 1024 * 1024); - const mb = q.size / (1024 * 1024); - sizeStr = gb >= 1 ? `${gb.toFixed(2)} GB` : `${mb.toFixed(0)} MB`; - } else if (typeof q.size === 'string' && q.size.trim()) { - sizeStr = q.size.trim(); - } - links.push({ - source: serverLabel, - name: `XPRIME ${serverName.charAt(0).toUpperCase() + serverName.slice(1)} - ${normalizedQuality}`, - url: q.url, - quality: normalizedQuality, - size: sizeStr, - type: 'VIDEO', - headers: WORKING_HEADERS, - referer: 'https://xprime.tv' - }); - } - }); - } else if (data.url) { - // Try to extract quality from the URL or response data - let quality = 'Unknown'; - - // Check if there's quality information in the response - if (data.quality) { - quality = getQualityFromName(data.quality); - } else { - // Try to extract quality from URL patterns - const urlQualityMatch = data.url.match(/(\d{3,4})p/i); - if (urlQualityMatch) { - quality = getQualityFromName(urlQualityMatch[1] + 'p'); - } - } - - links.push({ - source: serverLabel, - name: `XPRIME ${serverName.charAt(0).toUpperCase() + serverName.slice(1)} - ${quality}`, - url: data.url, - quality: quality, - type: 'M3U8', - headers: WORKING_HEADERS, - referer: 'https://xprime.tv' - }); - } - } catch (error) { - console.error(`[Xprime] Error parsing server response: ${error.message}`); - } - - return { links, subtitles: [] }; -} - -// Group streams by quality for better organization -function groupStreamsByQuality(streams, subtitles, mediaInfo = {}) { - // Create media title with details - let mediaTitle = ''; - if (mediaInfo.title) { - if (mediaInfo.mediaType === 'tv' && mediaInfo.season && mediaInfo.episode) { - mediaTitle = `${mediaInfo.title} S${String(mediaInfo.season).padStart(2, '0')}E${String(mediaInfo.episode).padStart(2, '0')}`; - } else if (mediaInfo.year) { - mediaTitle = `${mediaInfo.title} (${mediaInfo.year})`; - } else { - mediaTitle = mediaInfo.title; - } - } - - // Group streams by quality - const qualityGroups = {}; - - streams.forEach(stream => { - const quality = stream.quality || 'Unknown'; - if (!qualityGroups[quality]) { - qualityGroups[quality] = []; - } - - qualityGroups[quality].push({ - name: stream.name, - title: mediaTitle || '', - url: stream.url, - quality: quality, - size: stream.size || 'Unknown', - headers: stream.headers || WORKING_HEADERS, - subtitles: subtitles - }); - }); - - // Define quality order (highest to lowest) - const qualityOrder = ['4K', '1440p', '1080p', '720p', '480p', '360p', '240p', 'Unknown']; - - // Sort and flatten the grouped streams - const sortedStreams = []; - qualityOrder.forEach(quality => { - if (qualityGroups[quality]) { - // Sort streams within the same quality by server name - qualityGroups[quality].sort((a, b) => a.name.localeCompare(b.name)); - sortedStreams.push(...qualityGroups[quality]); - } - }); - - // Add any qualities not in the predefined order - Object.keys(qualityGroups).forEach(quality => { - if (!qualityOrder.includes(quality)) { - qualityGroups[quality].sort((a, b) => a.name.localeCompare(b.name)); - sortedStreams.push(...qualityGroups[quality]); - } - }); - - return sortedStreams; -} - -// Get movie/TV show details from TMDB -function getTMDBDetails(tmdbId, mediaType) { - const endpoint = mediaType === 'tv' ? 'tv' : 'movie'; - const url = `${TMDB_BASE_URL}/${endpoint}/${tmdbId}?api_key=${TMDB_API_KEY}&append_to_response=external_ids`; - - return makeRequest(url) - .then(response => { - if (!response.ok) { - throw new Error(`TMDB API error: ${response.status}`); - } - return response.json(); - }) - .then(data => { - const title = mediaType === 'tv' ? data.name : data.title; - const releaseDate = mediaType === 'tv' ? data.first_air_date : data.release_date; - const year = releaseDate ? parseInt(releaseDate.split('-')[0]) : null; - - return { - title: title, - year: year, - imdbId: data.external_ids?.imdb_id || null - }; - }); -} - -// Main scraping function - Updated to match Nuvio interface -function getStreams(tmdbId, mediaType = 'movie', season = null, episode = null) { - console.log(`[Xprime] Fetching streams for TMDB ID: ${tmdbId}, Type: ${mediaType}${mediaType === 'tv' ? `, S:${season}E:${episode}` : ''}`); - - // First, get movie/TV show details from TMDB - return getTMDBDetails(tmdbId, mediaType) - .then(mediaInfo => { - if (!mediaInfo.title) { - throw new Error('Could not extract title from TMDB response'); - } - - console.log(`[Xprime] TMDB Info: "${mediaInfo.title}" (${mediaInfo.year || 'N/A'})`); - console.log(`[Xprime] Searching for: ${mediaInfo.title} (${mediaInfo.year})`); - - const { title, year, imdbId } = mediaInfo; - const type = mediaType; // Keep the original mediaType - - return getXprimeDomain().then(function(api) { - return getXprimeServers(api).then(function(servers) { - if (servers.length === 0) { - console.log('[Xprime] No active servers found'); - return []; - } - - console.log(`[Xprime] Processing ${servers.length} servers in parallel`); - - const allLinks = []; - const allSubtitles = []; - - // Process servers in parallel for better performance - const serverPromises = servers.map(function(server) { - console.log(`[Xprime] Processing server: ${server.name}`); - - // Rage server requires a different endpoint (backend.xprime.tv) and TMDB id param - let serverUrl; - if (server.name === 'rage') { - if (type === 'tv' && season && episode) { - serverUrl = `https://backend.xprime.tv/rage?id=${encodeURIComponent(tmdbId)}&season=${encodeURIComponent(season)}&episode=${encodeURIComponent(episode)}`; - } else { - serverUrl = `https://backend.xprime.tv/rage?id=${encodeURIComponent(tmdbId)}`; - } - } else { - const queryParams = buildQueryParams(server.name, title, year, imdbId, season, episode); - serverUrl = `${api}/${server.name}?${queryParams}`; - } - - console.log(`[Xprime] Request URL: ${serverUrl}`); - - return makeRequest(serverUrl, { - headers: { - 'Origin': server.name === 'rage' ? 'https://xprime.tv' : api, - 'Referer': server.name === 'rage' ? 'https://xprime.tv/' : api - } - }).then(function(response) { - return response.json().then(function(data) { - const serverLabel = `Xprime ${server.name.charAt(0).toUpperCase() + server.name.slice(1)}`; - let result; - - if (server.name === 'primebox') { - result = processPrimeBoxResponse(data, serverLabel, server.name); - } else { - result = processOtherServerResponse(data, serverLabel, server.name); - } - - console.log(`[Xprime] Server ${server.name}: Found ${result.links.length} links, ${result.subtitles.length} subtitles`); - return result; - }); - }).catch(function(error) { - console.error(`[Xprime] Error on server ${server.name}: ${error.message}`); - return { links: [], subtitles: [] }; - }); - }); - - // Wait for all server requests to complete - return Promise.allSettled(serverPromises).then(function(results) { - // Process results - for (const result of results) { - if (result.status === 'fulfilled') { - const { links, subtitles } = result.value; - allLinks.push(...links); - allSubtitles.push(...subtitles); - } - } - - console.log(`[Xprime] Total found: ${allLinks.length} links, ${allSubtitles.length} subtitles`); - - // Separate M3U8 links from direct video links - const m3u8Links = allLinks.filter(link => link.type === 'M3U8'); - const directLinks = allLinks.filter(link => link.type !== 'M3U8'); - - let resolvedStreams = []; - - // Resolve M3U8 playlists to extract individual quality streams - if (m3u8Links.length > 0) { - console.log(`[Xprime] Resolving ${m3u8Links.length} M3U8 playlists...`); - - return resolveMultipleM3U8(m3u8Links).then(function(resolutionResult) { - if (resolutionResult.success && resolutionResult.streams.length > 0) { - console.log(`[Xprime] Successfully resolved ${resolutionResult.streams.length} quality streams`); - resolvedStreams = resolutionResult.streams; - } else { - console.log(`[Xprime] M3U8 resolution failed, using master playlist URLs`); - resolvedStreams = m3u8Links; - } - - // Combine resolved streams with direct links - const finalLinks = [...directLinks, ...resolvedStreams]; - - console.log(`[Xprime] Final result: ${finalLinks.length} total streams (${resolvedStreams.length} from M3U8, ${directLinks.length} direct)`); - - // Group streams by quality and format for Nuvio - const mediaInfoForGrouping = { - title: title, - year: year, - mediaType: mediaType, - season: season, - episode: episode - }; - const formattedLinks = groupStreamsByQuality(finalLinks, allSubtitles, mediaInfoForGrouping); - - // Add provider identifier for header detection - formattedLinks.forEach(link => { - link.provider = 'xprime'; - }); - - return formattedLinks; - }).catch(function(error) { - console.error(`[Xprime] M3U8 resolution error: ${error.message}`); - resolvedStreams = m3u8Links; - - // Combine resolved streams with direct links - const finalLinks = [...directLinks, ...resolvedStreams]; - - console.log(`[Xprime] Final result: ${finalLinks.length} total streams (${resolvedStreams.length} from M3U8, ${directLinks.length} direct)`); - - // Group streams by quality and format for Nuvio - const mediaInfoForGrouping = { - title: title, - year: year, - mediaType: mediaType, - season: season, - episode: episode - }; - const formattedLinks = groupStreamsByQuality(finalLinks, allSubtitles, mediaInfoForGrouping); - - // Add provider identifier for header detection - formattedLinks.forEach(link => { - link.provider = 'xprime'; - }); - - return formattedLinks; - }); - } else { - // No M3U8 links, just return direct links - const finalLinks = [...directLinks, ...resolvedStreams]; - - console.log(`[Xprime] Final result: ${finalLinks.length} total streams (${resolvedStreams.length} from M3U8, ${directLinks.length} direct)`); - - // Group streams by quality and format for Nuvio - const mediaInfoForGrouping = { - title: title, - year: year, - mediaType: mediaType, - season: season, - episode: episode - }; - const formattedLinks = groupStreamsByQuality(finalLinks, allSubtitles, mediaInfoForGrouping); - - // Add provider identifier for header detection - formattedLinks.forEach(link => { - link.provider = 'xprime'; - }); - - return formattedLinks; - } - }); - }); - }).catch(function(error) { - console.error(`[Xprime] Scraping error: ${error.message}`); - return []; - }); - }) - .catch(function(error) { - console.error(`[Xprime] TMDB or scraping error: ${error.message}`); - return []; - }); -} - -// Export the main function -if (typeof module !== 'undefined' && module.exports) { - module.exports = { getStreams }; -} else { - // For React Native environment - global.XprimeScraperModule = { getStreams }; -} \ No newline at end of file