From 1266e482da86cb66ed50da79e59605c6cbf57efc Mon Sep 17 00:00:00 2001 From: tapframe Date: Wed, 27 Aug 2025 21:50:17 +0530 Subject: [PATCH] Fix submodule issue and add AltStore source configuration --- 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 ++++++++++++ 14 files changed, 8502 insertions(+), 1 deletion(-) delete mode 160000 local-scrapers-repo create mode 100644 local-scrapers-repo/LICENSE create mode 100644 local-scrapers-repo/README.md create mode 100644 local-scrapers-repo/manifest.json create mode 100644 local-scrapers-repo/providers/4khdhub.js create mode 100644 local-scrapers-repo/providers/dahmermovies.js create mode 100644 local-scrapers-repo/providers/hdrezka.js create mode 100644 local-scrapers-repo/providers/moviesmod.js create mode 100644 local-scrapers-repo/providers/myflixer-extractor.js create mode 100644 local-scrapers-repo/providers/netmirror.js create mode 100644 local-scrapers-repo/providers/showbox.js create mode 100644 local-scrapers-repo/providers/uhdmovies.js create mode 100644 local-scrapers-repo/providers/watch32.js create mode 100644 local-scrapers-repo/providers/xprime.js diff --git a/local-scrapers-repo b/local-scrapers-repo deleted file mode 160000 index e48fd1a2..00000000 --- a/local-scrapers-repo +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e48fd1a255b90cac10cd60fdddab8410783434a1 diff --git a/local-scrapers-repo/LICENSE b/local-scrapers-repo/LICENSE new file mode 100644 index 00000000..f288702d --- /dev/null +++ b/local-scrapers-repo/LICENSE @@ -0,0 +1,674 @@ + 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 new file mode 100644 index 00000000..ff3d2428 --- /dev/null +++ b/local-scrapers-repo/README.md @@ -0,0 +1,1136 @@ +# 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 new file mode 100644 index 00000000..8e0c50bf --- /dev/null +++ b/local-scrapers-repo/manifest.json @@ -0,0 +1,150 @@ +{ + "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 new file mode 100644 index 00000000..cbdb4bf4 --- /dev/null +++ b/local-scrapers-repo/providers/4khdhub.js @@ -0,0 +1,774 @@ +// 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 new file mode 100644 index 00000000..bef7581a --- /dev/null +++ b/local-scrapers-repo/providers/dahmermovies.js @@ -0,0 +1,329 @@ +// 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 new file mode 100644 index 00000000..8d9ea294 --- /dev/null +++ b/local-scrapers-repo/providers/hdrezka.js @@ -0,0 +1,437 @@ +// 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 new file mode 100644 index 00000000..973558a6 --- /dev/null +++ b/local-scrapers-repo/providers/moviesmod.js @@ -0,0 +1,857 @@ +// 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 new file mode 100644 index 00000000..5494a98d --- /dev/null +++ b/local-scrapers-repo/providers/myflixer-extractor.js @@ -0,0 +1,471 @@ +#!/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 new file mode 100644 index 00000000..9c55f570 --- /dev/null +++ b/local-scrapers-repo/providers/netmirror.js @@ -0,0 +1,740 @@ +// 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 new file mode 100644 index 00000000..a8cf6350 --- /dev/null +++ b/local-scrapers-repo/providers/showbox.js @@ -0,0 +1,506 @@ +// 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 new file mode 100644 index 00000000..cdda5c4b --- /dev/null +++ b/local-scrapers-repo/providers/uhdmovies.js @@ -0,0 +1,1081 @@ +// 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 new file mode 100644 index 00000000..299ed97a --- /dev/null +++ b/local-scrapers-repo/providers/watch32.js @@ -0,0 +1,547 @@ +// 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 new file mode 100644 index 00000000..635207d8 --- /dev/null +++ b/local-scrapers-repo/providers/xprime.js @@ -0,0 +1,800 @@ +// 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