diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..c766950 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "deps/libmpv"] + path = deps/libmpv + url = https://github.com/Zaarrg/libmpv + branch = master diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..7ccc54d --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,60 @@ +cmake_minimum_required(VERSION 3.16) + +project(stremio VERSION "5.0.7") + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") + +# Locate MPV +if(CMAKE_SIZEOF_VOID_P EQUAL 8) + # 64-bit architecture + set(MPV_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/deps/libmpv/x86_64/include") + set(MPV_LIBRARY "${CMAKE_CURRENT_SOURCE_DIR}/deps/libmpv/x86_64/mpv.lib") + set(MPV_DLL "${CMAKE_CURRENT_SOURCE_DIR}/deps/libmpv/x86_64/libmpv-2.dll") +else() + # 32-bit architecture + set(MPV_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/deps/libmpv/i686/include") + set(MPV_LIBRARY "${CMAKE_CURRENT_SOURCE_DIR}/deps/libmpv/i686/mpv.lib") + set(MPV_DLL "${CMAKE_CURRENT_SOURCE_DIR}/deps/libmpv/i686/libmpv-2.dll") +endif() + + +include_directories(${MPV_INCLUDE_DIR}) + +find_package(OpenSSL REQUIRED) +find_package(CURL REQUIRED) +find_package(nlohmann_json CONFIG REQUIRED) +find_package(unofficial-webview2 CONFIG REQUIRED) + +set(SOURCES + src/main.cpp + stremio.rc + src/resource.h +) + +add_executable(${PROJECT_NAME} WIN32 ${SOURCES}) + + +target_link_libraries(${PROJECT_NAME} PRIVATE + user32.lib + gdi32.lib + ole32.lib + oleaut32.lib + shell32.lib + advapi32.lib + nlohmann_json::nlohmann_json + unofficial::webview2::webview2 + OpenSSL::SSL + OpenSSL::Crypto + CURL::libcurl + ${MPV_LIBRARY} +) + +target_compile_definitions(${PROJECT_NAME} PRIVATE $<$:DEBUG_BUILD>) + +# Copy MPV DLL +add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${MPV_DLL}" + $ +) \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..840e2a4 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,595 @@ +GNU General Public License +========================== + +_Version 3, 29 June 2007_ +_Copyright © 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 +<>. \ No newline at end of file diff --git a/README.md b/README.md index 88b0fda..f26021b 100644 --- a/README.md +++ b/README.md @@ -1 +1,182 @@ -"# New Branch" +

+ Stremio Web Desktop Logo +

+
+

🌌 Stremio Desktop
Community

+
+ +

Stremio Desktop app with the latest Stremio web UI v5, built with Qt6

+

+ + C++ + + WebView2 + + MPV + + Win32 + + Scoop + + Chocolatey + + Streaming + + Torrents +

+ +## 🌟 **Features** +- 🚀 **Latest Technology**: Built with WebView2 to provide the newest features and best performance +- 🌐 **Latest Web Ui**: Always up-to-date with Stremio Web v5 +- 🎞️ **Native Playback**: Integrated Player for native 4K playback, hardware decoding, and fastest video performance +- 🔍 **Video Upscaling**: Upscaling support for anything mpv supports +- 🎥 **Full MPV Support**: Full MPV support use any hwdec, gpu-api or gpu-context like d3d, opengl and vulkan or `target-colorspace-hint=yes` for DV content +- 🌈 **HDR Support**: Full HDR Support thanks to fully supported mpv and any other mpv feature +- 🔊 **Dolby Atmos Support**: Support for all mpv advanced audio features. +- 🖼️ **Picture in Picture**: Picture in Picture Mode Support +- 🌑 **Dark Mode**: Windows Dark mode support +- 🖼️ **Thumbnail Preview**: ThumbFast support to allow for preview thumbnails when seeking +- 📁 **Local File Playback**: Play any local file that MPV and ffmpeg supports, just use open with 'stremio' or drag and drop +- 🌀 **Torrent Support**: Play any local .torrent file or any magnet: link +- 📺 **Chromecast Support**: Cast Videos to your Chromecast +- ➕ **Easy Addon Install**: Just use the install Button like stremio v4, no need to copy paste urls +- 💼 **Portable Version**: Fully portable version including WebView2. +- ⚙️ **App Settings**: Customize App behavior like CloseOnExit, PauseOnMinimize or PauseOnLostFocus and more via stremio-settings.ini. + + + +

+ Stremio Web Desktop Screenshot +

+ +## 🔧 Installation + +1. 🪟 **Windows x64 and x86** + 1. 📥 **Installer** + - **Install using the** `Installer`. Download `stremio-5.0.0-x64.exe` or `stremio-5.0.0-x86.exe` and run it. + 2. 💼 **Portable Version** + - **Install using the** `Archive`. Download `stremio-5.0.0-x64.7z` or `stremio-5.0.0-x86.7z` extract it and run `stremio.exe` + 3. 🥄 **Scoop.sh** + - Coming Soon! + 4. 🍫 **Chocolatey** + - Coming Soon! + +> **⏳ Note:** If you have stremio-desktop v4.x.x installed make sure to uninstall it first. Otherwise there might be issues. + +2. 🚀 **Linux, macOS** +- Coming soon! + +> **⏳ Note:** Linux and Mac release will take considerable time as they need their own build with os specific technology's + +## 🔍 **Mpv Upscalers** + +- 🎥 **[Anime4k](https://github.com/bloc97/Anime4K)** + - ✅ Included by default. + - 🔢 Use `CTRL+1` - `CTRL+6` to enable shaders. + - ❌ Use `CTRL+0` to disable. + +- 🎨 **[AnimeJaNai](https://github.com/the-database/mpv-upscale-2x_animejanai)** + - ❌ Not included by default. + - 📥 Download from the **Stremio-Desktop-v5** [release tab](https://github.com/Zaarrg/stremio-desktop-v5) the `stremio-animejanai-3.x.x.7z` for Stremio and drop the content of the 7z into `%localAppData%\Programs\LNV\Stremio-5\` and `replace all` + - 🛠️ **Changes made:** + - Removed `mpvnet.exe` as Stremio is used as the player. + - Adjusted `mpv.conf` to work with Stremio. + - Adjusted `input.conf` to work with Stremio. + - ⌨️ **Possible Keybindings** + - 🎬 `CTRL+J` Show Upscaler Status + - 🛠️ `CTRL+E` Open AnimeJaNai ConfEditor + - ❌ `CTRL+0` Disable Upscaling + - 🔢 `SHIFT+1` - `SHIFT+3` Select Quality, Balanced or Performance Profiles + - ⚙️ `CTRL+1` - `CTRL+9` Switch between Custom Profiles + - 🔗 For more, check [AnimeJaNai](https://github.com/the-database/mpv-upscale-2x_animejanai) + +> **⏳ Note:** When using AnimeJaNai on first playback Stremio will be unresponsive and a console will open to build the model via e.g. TensorRT. You will need to wait until the console closes for playback to start. This happens only once per model. + + +- 🚀 **Nvidia RTX and Intel VSR Scaling** + - ✅ Supported by using `mpv.conf`. + - ✍️ Modify in `%localAppData%\Programs\LNV\Stremio-5\` the ``portable_config/mpv.conf`` and add the line ``vf=d3d11vpp=scale=2:scaling-mode=nvidia`` more details [here](https://www.reddit.com/r/nvidia/comments/1foyl4n/mpv_player_v0390_adds_rtx_video_super_resolution/) + +## 🔍 **Mpv Addons** + +- 🎥 **[ThumbFast](https://github.com/po5/thumbfast)** + - 🔧 Go in the `Stremio-Dekstop-v5` Repo to ``utils/mpv/thumbfast`` or ``direct-link`` and download ``thumbfast.7z``. Drag and Drop the archive contents into ``%localAppData%\Programs\LNV\Stremio-5\`` + - 📁 Works best with local files as there is no **network bottleneck**. U can `Drag and Drop` any local file into **Stremio** or right click ``Open With > Stremio`` + + +## ✨ **Stremio App** + +- 📁 **Local Files** + - Play any **local file** or **torrent** by `drag and dropping` or ``Open With > Stremio`` that mpv and ffmpeg support + - Play any **magnet** by `opening it via the browser` in Stremio or `copy pasting` it into the **Search Bar** + +- 🧩 **Browser Extensions** + - Add any Browser Extension to Stremio by dropping the ``unpacked`` Extension into ``portable_config/extensions`` + - On Start Extensions from ``portable_config/extensions`` are loaded. + - 👉 **To install extension:** + 1. Get the ``unpacked`` Extensions from``%localAppData%\Microsoft\Edge SxS\User Data\Default\Extensions``. + 2. Here look for the `mainfest.json` for example ublock `{string-id}/1.62.0_0/manifest.json` as all the content beside is the extension + 3. Now we can copy the contents of ``{string-id}/1.62.0_0`` to ``%localAppData%\Programs\LNV\Stremio-5\portable_config\extensions\ublock`` + 4. Important is that the ``mainfest.json`` is located directly in ``portable_config\extensions\ublock`` + 5. Done. Restart app and addon will be loaded. If loading fails check ``portable_config\errors-{date}.txt`` + +- ⚙️ **App Settings** + - All App Settings can be adjusted with ``portable_config\stremio-settings.ini`` + - Some options can be set by `right-clicking` on the **tray icon** as well. + - ⌨️ **Possible Settings** + - ❌ ``CloseOnExit`` Close app on exit instead of minimized to tray + - 🌓 ``UseDarkTheme`` Toggle dark theme + - 📏 ``ThumbFastHeight`` Enable thumbfast and set the thumbfast image height. This adjust the offset of the top left corner of the thumb. Meaning `100` will move the top left corner 100px up. `0` disables thumbfast + - 😴 ``PauseOnMinimize`` Pause playback on window minimize + - 👀 ``PauseOnLostFocus`` Pause playback on window loses focus + - 🔍 ``AllowZoom`` Allow zoom via `pinch action` or ``CTRL+Scroll`` + +## 🎛️ **Mpv Configuration** + +Enhance your Stremio experience by customizing the MPV player settings. Below are the key configuration files and guidelines to help you get started: + +- 📁 **`mpv.conf` Location** + - The ``mpv.conf`` file can be found in the following location: + - **Installation Path:** ``%localAppData%\Programs\LNV\Stremio-5\portable_config\mpv.conf`` + - **Shaders Folder:** Located within the installation directory ``..\Stremio-5\portable_config\shaders``. + +> **⏳ Note:** Any other configuration files can be just dropped into ``%localAppData%\Programs\LNV\Stremio-5\portable_config`` as this is the mpv ``config-dir`` like ``input.conf``. ``scripts`` or ``scripts-conf`` + +- **🎹 Usage example in `input.conf` using [Anime4k](https://github.com/bloc97/Anime4K):** + ```shell + # Optimized shaders for higher-end GPU + CTRL+1 no-osd change-list glsl-shaders set "~~/shaders/Anime4K_Clamp_Highlights.glsl;~~/shaders/Anime4K_Restore_CNN_VL.glsl;~~/shaders/Anime4K_Upscale_CNN_x2_VL.glsl;~~/shaders/Anime4K_AutoDownscalePre_x2.glsl;~~/shaders/Anime4K_AutoDownscalePre_x4.glsl;~~/shaders/Anime4K_Upscale_CNN_x2_M.glsl"; show-text "Anime4K: Mode A (HQ)" + CTRL+2 no-osd change-list glsl-shaders set "~~/shaders/Anime4K_Clamp_Highlights.glsl;~~/shaders/Anime4K_Restore_CNN_Soft_VL.glsl;~~/shaders/Anime4K_Upscale_CNN_x2_VL.glsl;~~/shaders/Anime4K_AutoDownscalePre_x2.glsl;~~/shaders/Anime4K_AutoDownscalePre_x4.glsl;~~/shaders/Anime4K_Upscale_CNN_x2_M.glsl"; show-text "Anime4K: Mode B (HQ)" + CTRL+3 no-osd change-list glsl-shaders set "~~/shaders/Anime4K_Clamp_Highlights.glsl;~~/shaders/Anime4K_Upscale_Denoise_CNN_x2_VL.glsl;~~/shaders/Anime4K_AutoDownscalePre_x2.glsl;~~/shaders/Anime4K_AutoDownscalePre_x4.glsl;~~/shaders/Anime4K_Upscale_CNN_x2_M.glsl"; show-text "Anime4K: Mode C (HQ)" + CTRL+4 no-osd change-list glsl-shaders set "~~/shaders/Anime4K_Clamp_Highlights.glsl;~~/shaders/Anime4K_Restore_CNN_VL.glsl;~~/shaders/Anime4K_Upscale_CNN_x2_VL.glsl;~~/shaders/Anime4K_Restore_CNN_M.glsl;~~/shaders/Anime4K_AutoDownscalePre_x2.glsl;~~/shaders/Anime4K_AutoDownscalePre_x4.glsl;~~/shaders/Anime4K_Upscale_CNN_x2_M.glsl"; show-text "Anime4K: Mode A+A (HQ)" + CTRL+5 no-osd change-list glsl-shaders set "~~/shaders/Anime4K_Clamp_Highlights.glsl;~~/shaders/Anime4K_Restore_CNN_Soft_VL.glsl;~~/shaders/Anime4K_Upscale_CNN_x2_VL.glsl;~~/shaders/Anime4K_AutoDownscalePre_x2.glsl;~~/shaders/Anime4K_AutoDownscalePre_x4.glsl;~~/shaders/Anime4K_Restore_CNN_Soft_M.glsl;~~/shaders/Anime4K_Upscale_CNN_x2_M.glsl"; show-text "Anime4K: Mode B+B (HQ)" + CTRL+6 no-osd change-list glsl-shaders set "~~/shaders/Anime4K_Clamp_Highlights.glsl;~~/shaders/Anime4K_Upscale_Denoise_CNN_x2_VL.glsl;~~/shaders/Anime4K_AutoDownscalePre_x2.glsl;~~/shaders/Anime4K_AutoDownscalePre_x4.glsl;~~/shaders/Anime4K_Restore_CNN_M.glsl;~~/shaders/Anime4K_Upscale_CNN_x2_M.glsl"; show-text "Anime4K: Mode C+A (HQ)" + + CTRL+0 no-osd change-list glsl-shaders clr ""; show-text "GLSL shaders cleared" + ``` +> **⏳ Note:** Some keys might not work as key presses are converted from js event.codes to literal values for mpv + +## ⚙️ **Start Arguments** +Use these extra arguments when launching the application: + +| Argument | Example | Description | +|-----------------------------|-------------------------------------------------------|-----------------------------------------------------------------------------------------------------------| +| --webui-url= | --webui-url=https://web.stremio.com/ | Loads web ui from `https://web.stremio.com/` | +| --streaming-server-disabled | | Disable auto start of `streamio-server`, Default behaviour in prod +| --autoupdater-force-full | | Forces Autoupdate to always do a `full-update` rather than `partial` | +| --autoupdater-endpoint= | --autoupdater-endpoint==https://verison.mydomain.com/ | Overrides default checking endpoint for the autoupdater | + +> **⏳ Note:** By default will use as ``webui-url`` the [stremio-web-shell](https://github.com/Zaarrg/stremio-web-shell-fixes) web-ui hosted [here](https://zaarrg.github.io/stremio-web-shell-fixes/#/) which includes fixes to run smoothly with qt6 + +## 📚 **Guide / Docs** +If you want to build this app yourself, check the “[docs](https://github.com/Zaarrg/stremio-desktop-v5/tree/master/docs)” folder in this repository for setup instructions and additional information. + +## ⚠️ **Disclaimer** +This project is not affiliated with **Stremio** in any way. + +## 🤝 **Support Development** +If you enjoy this project and want to support further development, consider [buying me a coffee](https://ko-fi.com/zaarrg). Your support means a lot! ☕ + +

+ ⭐ Made with ❤️ by Zaarrg +

\ No newline at end of file diff --git a/build/build_anime4k.js b/build/build_anime4k.js new file mode 100644 index 0000000..e4fcceb --- /dev/null +++ b/build/build_anime4k.js @@ -0,0 +1,229 @@ +#!/usr/bin/env node + +/** + * build_anime4k.js + * + * This script performs the following: (Needed for deploy_windows to include Anime4k in installer) + * 1. Determines the latest Anime4K version from bloc97/Anime4K releases. + * 2. Downloads the corresponding GLSL_Windows_High-end.zip from Tama47/Anime4K. + * 3. Auto-detects 7z.exe on the system. + * 4. Saves the downloaded zip as anime4k-High-end.zip in utils/mpv. + * 5. Extracts the zip into the anime4k folder. + * 6. Cleans up temporary files. + * + * Usage: + * node build_anime4k.js + */ + +const fs = require('fs'); +const path = require('path'); +const https = require('https'); +const { execSync } = require('child_process'); +const os = require('os'); + +// Configuration +const BLOC97_API_URL = 'https://api.github.com/repos/bloc97/Anime4K/releases/latest'; +const TEMP_DIR = path.join(os.tmpdir(), 'anime4k_build_temp'); +const OUTPUT_DIR = path.resolve(__dirname, '..', 'utils', 'mpv', 'anime4k'); +const OUTPUT_ZIP_NAME = 'anime4k-High-end.zip'; +const EXTRACTION_DIR = path.join(OUTPUT_DIR, 'portable_config'); + +// Common 7z.exe installation paths on Windows +const COMMON_7Z_PATHS = [ + path.join(process.env.PROGRAMFILES || 'C:\\Program Files', '7-Zip', '7z.exe'), + path.join(process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)', '7-Zip', '7z.exe'), + path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'), '7-Zip', '7z.exe') +]; + +// Maximum number of redirects to follow +const MAX_REDIRECTS = 5; + +// Helper Functions + +function httpsGet(url, headers = {}, redirectCount = 0) { + return new Promise((resolve, reject) => { + if (redirectCount > MAX_REDIRECTS) return reject(new Error('Too many redirects')); + + const options = { + headers: { + 'User-Agent': 'Node.js Script', + ...headers + } + }; + https.get(url, options, (res) => { + if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + return resolve(httpsGet(res.headers.location, headers, redirectCount + 1)); + } + if (res.statusCode !== 200) { + res.resume(); + return reject(new Error(`Request Failed. Status Code: ${res.statusCode}`)); + } + let data = ''; + res.setEncoding('utf8'); + res.on('data', chunk => data += chunk); + res.on('end', () => resolve(data)); + }).on('error', e => reject(e)); + }); +} + +function downloadFile(url, dest, headers = {}, redirectCount = 0) { + return new Promise((resolve, reject) => { + if (redirectCount > MAX_REDIRECTS) return reject(new Error('Too many redirects')); + + const options = { + headers: { + 'User-Agent': 'Node.js Script', + ...headers + } + }; + + https.get(url, options, (res) => { + if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + console.log(`Redirecting to ${res.headers.location}`); + return resolve(downloadFile(res.headers.location, dest, headers, redirectCount + 1)); + } + if (res.statusCode !== 200) { + res.resume(); + return reject(new Error(`Failed to get '${url}' (${res.statusCode})`)); + } + + const totalSize = parseInt(res.headers['content-length'], 10); + let downloadedSize = 0; + const file = fs.createWriteStream(dest); + res.pipe(file); + + res.on('data', chunk => { + downloadedSize += chunk.length; + if (totalSize) { + const percent = ((downloadedSize / totalSize) * 100).toFixed(2); + process.stdout.write(`Downloading... ${percent}%\r`); + } else { + process.stdout.write(`Downloading... ${downloadedSize} bytes\r`); + } + }); + + file.on('finish', () => { + file.close(() => { + process.stdout.write('\n'); + resolve(); + }); + }); + + file.on('error', err => { + fs.unlink(dest, () => reject(err)); + }); + }).on('error', err => reject(err)); + }); +} + +function execCommand(command, cwd = process.cwd()) { + try { + execSync(command, { stdio: 'inherit', cwd }); + } catch (error) { + throw new Error(`Command failed: ${command}\n${error.message}`); + } +} + +function commandExists(command) { + try { + execSync(`where ${command}`, { stdio: 'ignore' }); + return true; + } catch { + return false; + } +} + +function find7zExecutable() { + if (commandExists('7z')) { + console.log('Found 7z.exe in PATH.'); + return '7z'; + } + for (const potentialPath of COMMON_7Z_PATHS) { + if (fs.existsSync(potentialPath)) { + console.log(`Found 7z.exe at: ${potentialPath}`); + return `"${potentialPath}"`; + } + } + throw new Error('7z.exe not found. Please install 7-Zip.'); +} + +// Main Build Function +(async function buildAnime4K() { + try { + console.log('=== Build Anime4K Script Started ==='); + + const sevenZipPath = find7zExecutable(); + + if (fs.existsSync(TEMP_DIR)) fs.rmSync(TEMP_DIR, { recursive: true, force: true }); + fs.mkdirSync(TEMP_DIR, { recursive: true }); + console.log(`Created temporary directory at ${TEMP_DIR}`); + + console.log('Fetching latest Anime4K version information...'); + const releaseData = await httpsGet(BLOC97_API_URL); + const releaseJson = JSON.parse(releaseData); + const version = releaseJson.tag_name; + console.log(`Latest version: ${version}`); + + const downloadUrl = `https://github.com/Tama47/Anime4K/releases/download/${version}/GLSL_Windows_High-end.zip`; + const downloadedFilePath = path.join(TEMP_DIR, 'GLSL_Windows_High-end.zip'); + + console.log(`Downloading GLSL_Windows_High-end.zip for version ${version}...`); + await downloadFile(downloadUrl, downloadedFilePath); + console.log(`Downloaded to ${downloadedFilePath}`); + + // Ensure output directory exists + fs.mkdirSync(OUTPUT_DIR, { recursive: true }); + + const outputZipPath = path.join(OUTPUT_DIR, OUTPUT_ZIP_NAME); + fs.copyFileSync(downloadedFilePath, outputZipPath); + console.log(`Saved zip as ${outputZipPath}`); + + // Extract the zip to the anime4k folder + fs.mkdirSync(EXTRACTION_DIR, { recursive: true }); + console.log(`Extracting ${outputZipPath} to ${EXTRACTION_DIR}...`); + execCommand(`${sevenZipPath} x "${outputZipPath}" -o"${EXTRACTION_DIR}" -y`); + console.log('Extraction complete.'); + + // Path to the mpv.conf file inside the extraction directory + const mpvConfPath = path.join(EXTRACTION_DIR, 'mpv.conf'); + + // Check if mpv.conf exists before attempting to modify it + if (fs.existsSync(mpvConfPath)) { + console.log(`Modifying ${mpvConfPath} to comment out glsl-shaders lines...`); + // Read the existing content of mpv.conf + let confData = fs.readFileSync(mpvConfPath, 'utf8'); + // Split the file into lines + const lines = confData.split(/\r?\n/); + // Map over lines to comment out ones that start with 'glsl-shaders=' + const modifiedLines = lines.map(line => { + // Trim whitespace at beginning of line for accurate check + const trimmedLine = line.trim(); + if (trimmedLine.startsWith('glsl-shaders=')) { + // If not already commented out, add a comment marker + if (!trimmedLine.startsWith('#')) { + return `# ${line}`; + } + } + return line; + }); + // Join the modified lines back together + confData = modifiedLines.join(os.EOL); + // Write the changes back to mpv.conf + fs.writeFileSync(mpvConfPath, confData, 'utf8'); + console.log('Modification complete.'); + } else { + console.log(`Warning: ${mpvConfPath} not found. Skipping modification.`); + } + + // Cleanup + console.log(`Cleaning up temporary files at ${TEMP_DIR}...`); + fs.rmSync(TEMP_DIR, { recursive: true, force: true }); + console.log('Cleanup complete.'); + + console.log('=== Build Anime4K Script Completed Successfully ==='); + process.exit(1); + } catch (error) { + console.error('Error during build:', error.message); + process.exit(1); + } +})(); diff --git a/build/build_animejanai.js b/build/build_animejanai.js new file mode 100644 index 0000000..d6e6743 --- /dev/null +++ b/build/build_animejanai.js @@ -0,0 +1,354 @@ +#!/usr/bin/env node + +/** + * build_animejanai.js + * + * This script performs the following: + * 1. Downloads the latest 'full-package' .7z release from Animejanai GitHub. + * 2. Extracts the archive. + * 3. Deletes specified files and folders. + * 4. Modifies configuration files as per stremio requirements. + * 5. Repackages the modified files into a new .7z archive with maximum compression. + * 6. Places the final archive in utils/mpv. + * 7. Cleans up temporary files. + * + * Usage: + * node build_animejanai.js + */ + +const fs = require('fs'); +const path = require('path'); +const https = require('https'); +const { execSync } = require('child_process'); +const os = require('os'); + +// Configuration +const GITHUB_API_URL = 'https://api.github.com/repos/the-database/mpv-upscale-2x_animejanai/releases/latest'; +const TEMP_DIR = path.join(os.tmpdir(), 'animejanai_build_temp'); +const OUTPUT_DIR = path.resolve(__dirname, '..', 'utils', 'mpv', 'stremio-animejanai'); +const OUTPUT_FILENAME_TEMPLATE = 'stremio-animejanai-{version}.7z'; + +// Files and directories to delete +const FILES_TO_DELETE = [ + 'libmpv-2.dll', + 'libmpvnet.pdb', + 'MediaInfo.dll', + 'mpvnet.com', + 'mpvnet.dll.config', + 'mpvnet.exe', + 'mpvnet.pdb', + 'NGettext.Wpf.pdb' +]; + +const FOLDERS_TO_DELETE = [ + 'Locale' +]; + +// Configuration for input.conf replacement +const NEW_INPUT_CONF_CONTENT = ` +Ctrl+E show-text "Launching AnimeJaNaiConfEditor..."; run "~~\\..\\animejanai\\AnimeJaNaiConfEditor.exe" #menu: AnimeJaNai > Launch AnimeJaNaiConfEditor +Ctrl+J script-binding "show_animejanai_stats" #menu: AnimeJaNai > Toggle AnimeJaNai Stats + +) show-text "2x_AnimeJaNai_V3 Off"; apply-profile upscale-off; +Ctrl+0 show-text "2x_AnimeJaNai_V3 Off"; apply-profile upscale-off; +SHIFT+1 show-text "2x_AnimeJaNai_V3 Quality"; apply-profile upscale-on-quality; +SHIFT+2 show-text "2x_AnimeJaNai_V3 Balanced"; apply-profile upscale-on-balanced; +SHIFT+3 show-text "2x_AnimeJaNai_V3 Performance"; apply-profile upscale-on-performance; +Ctrl+1 show-text "2x_AnimeJaNai_V3 Custom Profile 1"; apply-profile upscale-on-1; +Ctrl+2 show-text "2x_AnimeJaNai_V3 Custom Profile 2"; apply-profile upscale-on-2; +Ctrl+3 show-text "2x_AnimeJaNai_V3 Custom Profile 3"; apply-profile upscale-on-3; +Ctrl+4 show-text "2x_AnimeJaNai_V3 Custom Profile 4"; apply-profile upscale-on-4; +Ctrl+5 show-text "2x_AnimeJaNai_V3 Custom Profile 5"; apply-profile upscale-on-5; +Ctrl+6 show-text "2x_AnimeJaNai_V3 Custom Profile 6"; apply-profile upscale-on-6; +Ctrl+7 show-text "2x_AnimeJaNai_V3 Custom Profile 7"; apply-profile upscale-on-7; +Ctrl+8 show-text "2x_AnimeJaNai_V3 Custom Profile 8"; apply-profile upscale-on-8; +Ctrl+9 show-text "2x_AnimeJaNai_V3 Custom Profile 9"; apply-profile upscale-on-9; +`; + +// Lines to delete from mpv.conf +const LINES_TO_DELETE_IN_MPV_CONF = [ + 'save-position-on-quit=yes', + 'watch-later-options=start', + 'reset-on-next-file=pause' +]; + +// Common 7z.exe installation paths on Windows +const COMMON_7Z_PATHS = [ + path.join(process.env.PROGRAMFILES || 'C:\\Program Files', '7-Zip', '7z.exe'), + path.join(process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)', '7-Zip', '7z.exe'), + path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'), '7-Zip', '7z.exe') +]; + +// Maximum number of redirects to follow +const MAX_REDIRECTS = 5; + +// Function to make HTTPS GET requests with GitHub API headers +function httpsGet(url, headers = {}, redirectCount = 0) { + return new Promise((resolve, reject) => { + if (redirectCount > MAX_REDIRECTS) { + return reject(new Error('Too many redirects')); + } + + const options = { + headers: { + 'User-Agent': 'Node.js Script', + ...headers + } + }; + https.get(url, options, (res) => { + if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + // Handle redirects + resolve(httpsGet(res.headers.location, headers, redirectCount + 1)); + return; + } + if (res.statusCode !== 200) { + reject(new Error(`Request Failed. Status Code: ${res.statusCode}`)); + res.resume(); // Consume response data to free up memory + return; + } + let data = ''; + res.setEncoding('utf8'); + res.on('data', (chunk) => data += chunk); + res.on('end', () => resolve(data)); + }).on('error', (e) => reject(e)); + }); +} + +// Function to download a file from a URL, handling redirects and showing progress +function downloadFile(url, dest, headers = {}, redirectCount = 0) { + return new Promise((resolve, reject) => { + if (redirectCount > MAX_REDIRECTS) { + return reject(new Error('Too many redirects')); + } + + const options = { + headers: { + 'User-Agent': 'Node.js Script', + ...headers + } + }; + + https.get(url, options, (res) => { + if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + // Handle redirects + console.log(`Redirecting to ${res.headers.location}`); + downloadFile(res.headers.location, dest, headers, redirectCount + 1).then(resolve).catch(reject); + return; + } + + if (res.statusCode !== 200) { + reject(new Error(`Failed to get '${url}' (${res.statusCode})`)); + res.resume(); + return; + } + + const totalSize = parseInt(res.headers['content-length'], 10); + let downloadedSize = 0; + + const file = fs.createWriteStream(dest); + res.pipe(file); + + res.on('data', (chunk) => { + downloadedSize += chunk.length; + if (totalSize) { + const percent = ((downloadedSize / totalSize) * 100).toFixed(2); + process.stdout.write(`Downloading... ${percent}%\r`); + } else { + process.stdout.write(`Downloading... ${downloadedSize} bytes\r`); + } + }); + + file.on('finish', () => { + file.close(() => { + process.stdout.write('\n'); + resolve(); + }); + }); + + file.on('error', (err) => { + fs.unlink(dest, () => reject(err)); + }); + }).on('error', (err) => { + reject(err); + }); + }); +} + +// Function to execute a shell command synchronously +function execCommand(command, cwd = process.cwd()) { + try { + execSync(command, { stdio: 'inherit', cwd }); + } catch (error) { + throw new Error(`Command failed: ${command}\n${error.message}`); + } +} + +// Function to check if a command exists +function commandExists(command) { + try { + execSync(`where ${command}`, { stdio: 'ignore' }); + return true; + } catch { + return false; + } +} + +// Function to find 7z.exe in common installation paths +function find7zExecutable() { + // First, check if 7z is in PATH + if (commandExists('7z')) { + console.log('Found 7z.exe in PATH.'); + return '7z'; + } + + // Search in common installation directories + for (const potentialPath of COMMON_7Z_PATHS) { + if (fs.existsSync(potentialPath)) { + console.log(`Found 7z.exe at: ${potentialPath}`); + return `"${potentialPath}"`; // Quote the path in case it contains spaces + } + } + + // If not found, throw an error + throw new Error('7z.exe not found. Please install 7-Zip and ensure 7z.exe is in your PATH or installed in a common directory.'); +} + +// Main Build Function +(async function buildAnimeJanai() { + try { + console.log('=== Build AnimeJaNai Script Started ==='); + + // Locate 7z.exe + const sevenZipPath = find7zExecutable(); + + // Create temporary directory + if (fs.existsSync(TEMP_DIR)) { + fs.rmSync(TEMP_DIR, { recursive: true, force: true }); + } + fs.mkdirSync(TEMP_DIR, { recursive: true }); + console.log(`Created temporary directory at ${TEMP_DIR}`); + + // Step 1: Fetch latest release info from GitHub + console.log('Fetching latest release information from GitHub...'); + const releaseData = await httpsGet(GITHUB_API_URL); + const releaseJson = JSON.parse(releaseData); + const version = releaseJson.tag_name || 'latest'; + console.log(`Latest version: ${version}`); + + // Step 2: Find the 'full-package' .7z asset + const assets = releaseJson.assets; + const fullPackageAsset = assets.find(asset => asset.name.includes('full-package') && asset.name.endsWith('.7z')); + + if (!fullPackageAsset) { + throw new Error("No 'full-package' .7z asset found in the latest release."); + } + + const downloadUrl = fullPackageAsset.browser_download_url; + const assetName = fullPackageAsset.name; + const downloadedFilePath = path.join(TEMP_DIR, assetName); + + console.log(`Downloading asset: ${assetName}`); + await downloadFile(downloadUrl, downloadedFilePath); + console.log(`Downloaded to ${downloadedFilePath}`); + + // Step 3: Extract the .7z archive + const extractDir = path.join(TEMP_DIR, 'extracted'); + fs.mkdirSync(extractDir, { recursive: true }); + console.log(`Extracting ${downloadedFilePath} to ${extractDir}...`); + execCommand(`${sevenZipPath} x "${downloadedFilePath}" -o"${extractDir}" -y`, TEMP_DIR); + console.log('Extraction complete.'); + + // Step 4: Identify the root directory inside the extracted folder + const extractedItems = fs.readdirSync(extractDir); + let rootDir = extractDir; + + if (extractedItems.length === 1 && fs.lstatSync(path.join(extractDir, extractedItems[0])).isDirectory()) { + rootDir = path.join(extractDir, extractedItems[0]); + console.log(`Detected root directory: ${rootDir}`); + } else { + console.log('No single root directory detected. Proceeding with extracted contents.'); + } + + // Step 5: Delete specified files + console.log('Deleting specified files...'); + FILES_TO_DELETE.forEach(file => { + const filePath = path.join(rootDir, file); + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + console.log(`Deleted file: ${filePath}`); + } else { + console.log(`File not found (skipped): ${filePath}`); + } + }); + + // Step 6: Delete specified folders + console.log('Deleting specified folders...'); + FOLDERS_TO_DELETE.forEach(folder => { + const folderPath = path.join(rootDir, folder); + if (fs.existsSync(folderPath)) { + fs.rmSync(folderPath, { recursive: true, force: true }); + console.log(`Deleted folder: ${folderPath}`); + } else { + console.log(`Folder not found (skipped): ${folderPath}`); + } + }); + + // Step 7: Modify portable_config/input.conf + const portableConfigDir = path.join(rootDir, 'portable_config'); + const inputConfPath = path.join(portableConfigDir, 'input.conf'); + + if (fs.existsSync(inputConfPath)) { + fs.writeFileSync(inputConfPath, NEW_INPUT_CONF_CONTENT, 'utf8'); + console.log(`Modified input.conf at ${inputConfPath}`); + } else { + console.warn(`input.conf not found at ${inputConfPath}. Skipping modification.`); + } + + // Step 8: Modify mpv.conf + const mpvConfPath = path.join(portableConfigDir, 'mpv.conf'); // Corrected path + + if (fs.existsSync(mpvConfPath)) { + let mpvConfContent = fs.readFileSync(mpvConfPath, 'utf8'); + + // Replace vo={something} with vo=libmpv + mpvConfContent = mpvConfContent.replace(/^vo=.*/m, 'vo=libmpv'); + + // Remove specified lines + LINES_TO_DELETE_IN_MPV_CONF.forEach(line => { + const regex = new RegExp(`^${line}$`, 'm'); + mpvConfContent = mpvConfContent.replace(regex, ''); + }); + + // Write the modified content back + fs.writeFileSync(mpvConfPath, mpvConfContent, 'utf8'); + console.log(`Modified mpv.conf at ${mpvConfPath}`); + } else { + console.warn(`mpv.conf not found at ${mpvConfPath}. Skipping modification.`); + } + + // Step 9: Repack the modified files into a new .7z archive + const outputVersion = version.startsWith('v') ? version.slice(1) : version; + const outputFilename = OUTPUT_FILENAME_TEMPLATE.replace('{version}', outputVersion); + const outputFilePath = path.join(OUTPUT_DIR, outputFilename); + + // Ensure output directory exists + fs.mkdirSync(OUTPUT_DIR, { recursive: true }); + + console.log(`Packing modified files into ${outputFilePath} with maximum compression...`); + + // Change working directory to rootDir to ensure files are added directly + execCommand(`${sevenZipPath} a -t7z "${outputFilePath}" * -mx=9`, rootDir); + console.log(`Packaged archive created at ${outputFilePath}`); + + // Step 10: Cleanup temporary directory + console.log(`Cleaning up temporary files at ${TEMP_DIR}...`); + fs.rmSync(TEMP_DIR, { recursive: true, force: true }); + console.log('Cleanup complete.'); + + console.log('=== Build AnimeJaNai Script Completed Successfully ==='); + process.exit(1); + } catch (error) { + console.error('Error during build:', error.message); + process.exit(1); + } +})(); diff --git a/build/build_checksums.js b/build/build_checksums.js new file mode 100644 index 0000000..820988a --- /dev/null +++ b/build/build_checksums.js @@ -0,0 +1,381 @@ +/**************************************************** + * build_checksums.js + * + * Usage: + * node build_checksums.js "" "" "" "" + * + * Example: + * node build_checksums.js "C:\\Program Files\\OpenSSL-Win64\\bin" "5.0.0-beta.7" "5.0.7" "4.20.11" + * + * This script: + * 1) Validates CLI args: OPENSSL_BIN, GIT_TAG, SHELL_VERSION, SERVER_VERSION + * 2) Locates and verifies openssl.exe + * 3) Computes sha256 checksums for Stremio.-x64.exe, -x86.exe, and server.js + * 4) Updates version-details.json and version.json for the built-in auto-updater. + * 5) Signs version-details.json, base64-encodes the signature, injects signature into version.json. + * 6) Cleans up ephemeral signature files. + * 7) Also updates: + * - utils/chocolatey/stremio.nuspec (the tag) + * - utils/chocolatey/tools/chocolateyinstall.ps1 (the download URL(s)) + * - utils/scoop/stremio-desktop-v5.json (the "version", "url", and "hash" fields for x86/x64) + * 8) Generates .sha256 files for the x86/x64 executables (so Scoop autoupdate can consume them). + * + ****************************************************/ + +const fs = require("fs"); +const path = require("path"); +const { execFileSync } = require("child_process"); + +// Parse CLI arguments +const [,, OPENSSL_BIN, GIT_TAG, SHELL_VERSION, SERVER_VERSION] = process.argv; + +(async function main() { + // 1) Validate args + if (!OPENSSL_BIN || !GIT_TAG || !SHELL_VERSION || !SERVER_VERSION) { + console.error("Usage: node build_checksums.js "); + console.error('Example: node build_checksums.js "C:\\Program Files\\OpenSSL-Win64\\bin" "5.0.0-beta.7" "5.0.7" 4.20.11'); + process.exit(1); + } + + // 2) Verify openssl.exe + const opensslExe = path.join(OPENSSL_BIN, "openssl.exe"); + if (!fs.existsSync(opensslExe)) { + console.error("ERROR: Cannot find openssl.exe in:", opensslExe); + process.exit(1); + } + + console.log("Using OpenSSL at:", OPENSSL_BIN); + console.log("Git Tag:", GIT_TAG); + console.log("Shell Version:", SHELL_VERSION); + console.log("server.js Version:", SERVER_VERSION); + console.log(); + + // 3) Build paths + const scriptDir = path.dirname(__filename); + const projectRoot = path.resolve(scriptDir, ".."); + + // The local EXE file names; adapt if your naming convention differs + const exeNameX64 = `Stremio ${SHELL_VERSION}-x64.exe`; + const exeNameX86 = `Stremio ${SHELL_VERSION}-x86.exe`; + + const EXE_PATH_x64 = path.join(projectRoot, "utils", exeNameX64); + const EXE_PATH_x86 = path.join(projectRoot, "utils", exeNameX86); + + // Where is server.js? Adjust if needed + const SERVERJS_PATH = path.join(projectRoot, "utils", "windows", "server.js"); + + // version details + const VERSION_DETAILS_PATH = path.join(projectRoot, "version", "version-details.json"); + const VERSION_JSON_PATH = path.join(projectRoot, "version", "version.json"); + const PRIVATE_KEY = path.join(projectRoot, "private_key.pem"); + + // Paths to your choco and scoop files: + const CHOCO_NUSPEC_PATH = path.join(projectRoot, "utils", "chocolatey", "stremio.nuspec"); + const CHOCO_INSTALL_PS1_PATH = path.join(projectRoot, "utils", "chocolatey", "tools", "chocolateyinstall.ps1"); + const SCOOP_MANIFEST_PATH = path.join(projectRoot, "utils", "scoop", "stremio-desktop-v5.json"); + + // 4) Generate SHA-256 for the .exe and server.js + checkFileExists(EXE_PATH_x64, "Stremio x64 .exe"); + checkFileExists(EXE_PATH_x86, "Stremio x86 .exe"); + const exeHash_x64 = computeSha256(opensslExe, EXE_PATH_x64); + const exeHash_x86 = computeSha256(opensslExe, EXE_PATH_x86); + + checkFileExists(SERVERJS_PATH, "server.js"); + const serverHash = computeSha256(opensslExe, SERVERJS_PATH); + + console.log("EXE sha256 x64 =", exeHash_x64); + console.log("EXE sha256 x86 =", exeHash_x86); + console.log("server.js sha256 =", serverHash); + console.log(); + + // 5) Update version-details.json + checkFileExists(VERSION_DETAILS_PATH, "version-details.json"); + let versionDetails; + try { + versionDetails = JSON.parse(fs.readFileSync(VERSION_DETAILS_PATH, "utf8")); + } catch (err) { + console.error("ERROR: Unable to parse version-details.json:", err.message); + process.exit(1); + } + + // Ensure structure + if (!versionDetails.files) { + console.error("ERROR: version-details.json missing property 'files'"); + process.exit(1); + } + + // Update version-details.json + versionDetails.shellVersion = SHELL_VERSION; + + // windows-x64 + if (!versionDetails.files["windows-x64"]) versionDetails.files["windows-x64"] = {}; + versionDetails.files["windows-x64"].url = `https://github.com/Zaarrg/stremio-desktop-v5/releases/download/${GIT_TAG}/Stremio.${SHELL_VERSION}-x64.exe`; + versionDetails.files["windows-x64"].checksum = exeHash_x64; + + // windows-x86 + if (!versionDetails.files["windows-x86"]) versionDetails.files["windows-x86"] = {}; + versionDetails.files["windows-x86"].url = `https://github.com/Zaarrg/stremio-desktop-v5/releases/download/${GIT_TAG}/Stremio.${SHELL_VERSION}-x86.exe`; + versionDetails.files["windows-x86"].checksum = exeHash_x86; + + // server.js + if (!versionDetails.files["server.js"]) versionDetails.files["server.js"] = {}; + versionDetails.files["server.js"].url = `https://dl.strem.io/server/${SERVER_VERSION}/desktop/server.js`; + versionDetails.files["server.js"].checksum = serverHash; + + // Save updated version-details.json + try { + fs.writeFileSync(VERSION_DETAILS_PATH, JSON.stringify(versionDetails, null, 2), "utf8"); + } catch (e) { + console.error("ERROR: Failed writing version-details.json:", e.message); + process.exit(1); + } + + // 6) Sign version-details.json & base64-encode + checkFileExists(PRIVATE_KEY, "private_key.pem"); + process.chdir(path.join(projectRoot, "version")); + + const sigFile = path.join(process.cwd(), "version-details.json.sig"); + const sigB64 = path.join(process.cwd(), "version-details.json.sig.b64"); + if (fs.existsSync(sigFile)) fs.unlinkSync(sigFile); + if (fs.existsSync(sigB64)) fs.unlinkSync(sigB64); + + console.log(`Signing version-details.json with ${PRIVATE_KEY}...`); + try { + execFileSync(opensslExe, [ + "dgst", + "-sha256", + "-sign", + PRIVATE_KEY, + "-out", + "version-details.json.sig", + "version-details.json" + ], { stdio: "inherit" }); + } catch (err) { + console.error("ERROR: Signing failed:", err.message); + process.exit(1); + } + + try { + execFileSync(opensslExe, [ + "base64", + "-in", + "version-details.json.sig", + "-out", + "version-details.json.sig.b64" + ], { stdio: "inherit" }); + } catch (err) { + console.error("ERROR: Base64 encoding failed:", err.message); + process.exit(1); + } + + if (!fs.existsSync(sigB64)) { + console.error("ERROR: Could not create signature file:", sigB64); + process.exit(1); + } + process.chdir(projectRoot); + + // 7) Insert signature into version.json + checkFileExists(VERSION_JSON_PATH, "version.json"); + console.log(`Updating signature in "${VERSION_JSON_PATH}"...`); + let signatureB64; + try { + signatureB64 = fs.readFileSync(sigB64, "utf8").replace(/\r?\n/g, ""); + } catch (err) { + console.error("ERROR: Unable to read version-details.json.sig.b64:", err.message); + process.exit(1); + } + + let versionJson; + try { + versionJson = JSON.parse(fs.readFileSync(VERSION_JSON_PATH, "utf8")); + } catch (err) { + console.error("ERROR: Unable to parse version.json:", err.message); + process.exit(1); + } + + versionJson.signature = signatureB64; + try { + fs.writeFileSync(VERSION_JSON_PATH, JSON.stringify(versionJson, null, 2), "utf8"); + } catch (err) { + console.error("ERROR: Unable to write version.json:", err.message); + process.exit(1); + } + + // Cleanup ephemeral signature files + try { + if (fs.existsSync(sigFile)) fs.unlinkSync(sigFile); + if (fs.existsSync(sigB64)) fs.unlinkSync(sigB64); + } catch (cleanupErr) { + console.error("WARNING: Could not remove signature files:", cleanupErr.message); + } + + console.log("\nSuccess! Checksums and signature updated. Now updating Scoop & Chocolatey files...\n"); + + // 8) Update stremio.nuspec + updateStremioNuspec(CHOCO_NUSPEC_PATH, SHELL_VERSION); + + // 9) Update chocolateyinstall.ps1 URLs + updateChocolateyInstall(CHOCO_INSTALL_PS1_PATH, GIT_TAG, SHELL_VERSION); + + // 10) Update the Scoop manifest (stremio-desktop-v5.json) + updateScoopManifest(SCOOP_MANIFEST_PATH, GIT_TAG, SHELL_VERSION, exeHash_x64, exeHash_x86); + + // 11) Generate .sha256 files for each EXE in /utils. + // This is required if you keep "hash.url" in your Scoop "autoupdate" section. + generateSha256FilesForScoop( + projectRoot, + GIT_TAG, + SHELL_VERSION, + { x64: exeHash_x64, x86: exeHash_x86 } + ); + + console.log("\nAll updates complete. You may now commit/push these changes and attach the .exe and .sha256 files to your release.\n"); + process.exit(0); + +})().catch(err => { + console.error("Unexpected error:", err); + process.exit(1); +}); + + +/************************************************************ + * Helper Functions + ************************************************************/ + +function checkFileExists(filePath, label) { + if (!fs.existsSync(filePath)) { + console.error(`ERROR: ${label} file not found at: ${filePath}`); + process.exit(1); + } +} + +// runs "openssl dgst -sha256 " and returns the hex string +function computeSha256(opensslExe, filePath) { + try { + const output = execFileSync(opensslExe, ["dgst", "-sha256", filePath], { encoding: "utf8" }); + // Typically "SHA256(file)= " + const match = output.match(/=.\s*([0-9a-fA-F]+)/); + if (!match) { + console.error("ERROR: Unexpected openssl dgst output for", filePath, "-", output); + process.exit(1); + } + return match[1].toLowerCase(); + } catch (err) { + console.error(`ERROR: openssl dgst failed for ${filePath}:`, err.message); + process.exit(1); + } +} + +// 8) Update the stremio.nuspec with the new +function updateStremioNuspec(nuspecPath, newVersion) { + checkFileExists(nuspecPath, "stremio.nuspec"); + let content = fs.readFileSync(nuspecPath, "utf8"); + + // Replace the ... with newVersion + content = content.replace( + /[^<]+<\/version>/, + `${newVersion}` + ); + + fs.writeFileSync(nuspecPath, content, "utf8"); + console.log(`Updated stremio.nuspec to ${newVersion}`); +} + +// 9) Update chocolateyinstall.ps1 with new GIT_TAG + SHELL_VERSION in the URLs +function updateChocolateyInstall(ps1Path, gitTag, newVersion) { + checkFileExists(ps1Path, "chocolateyinstall.ps1"); + let content = fs.readFileSync(ps1Path, "utf8"); + + // For 64-bit: + // if ([Environment]::Is64BitOperatingSystem) { + // $packageArgs['url'] = 'https://github.com/Zaarrg/stremio-desktop-v5/releases/download//Stremio.-x64.exe' + // } + const new64Url = `if ([Environment]::Is64BitOperatingSystem) {\n $packageArgs['url'] = 'https://github.com/Zaarrg/stremio-desktop-v5/releases/download/${gitTag}/Stremio.${newVersion}-x64.exe'`; + content = content.replace( + /if\s*\(\[Environment\]::Is64BitOperatingSystem\)\s*\{\s*\$packageArgs\['url'\]\s*=\s*'[^']+/m, + new64Url + ); + + // For 32-bit: + // } else { + // $packageArgs['url'] = 'https://github.com/Zaarrg/stremio-desktop-v5/releases/download//Stremio.-x86.exe' + // } + const new86Url = `} else {\n $packageArgs['url'] = 'https://github.com/Zaarrg/stremio-desktop-v5/releases/download/${gitTag}/Stremio.${newVersion}-x86.exe'`; + content = content.replace( + /}\s*else\s*\{\s*\$packageArgs\['url'\]\s*=\s*'[^']+/m, + new86Url + ); + + fs.writeFileSync(ps1Path, content, "utf8"); + console.log(`Updated chocolateyinstall.ps1 URLs to version ${newVersion}`); +} + +// 10) Update the Scoop manifest stremio-desktop-v5.json +function updateScoopManifest(scoopPath, gitTag, newVersion, hash64, hash86) { + checkFileExists(scoopPath, "stremio-desktop-v5.json"); + let scoopJson; + + try { + const raw = fs.readFileSync(scoopPath, "utf8"); + scoopJson = JSON.parse(raw); + } catch (err) { + console.error("ERROR: Unable to parse scoop manifest JSON:", err.message); + process.exit(1); + } + + // "version": "5.0.7" + scoopJson.version = newVersion; + + if (!scoopJson.architecture || !scoopJson.architecture["64bit"] || !scoopJson.architecture["32bit"]) { + console.error("ERROR: scoop manifest missing architecture stanzas"); + process.exit(1); + } + + // Update 64bit url + hash + scoopJson.architecture["64bit"].url = `https://github.com/Zaarrg/stremio-desktop-v5/releases/download/${gitTag}/Stremio.${newVersion}-x64.exe`; + scoopJson.architecture["64bit"].hash = hash64; + + // Update 32bit url + hash + scoopJson.architecture["32bit"].url = `https://github.com/Zaarrg/stremio-desktop-v5/releases/download/${gitTag}/Stremio.${newVersion}-x86.exe`; + scoopJson.architecture["32bit"].hash = hash86; + + // If you want to rely on .sha256 files for autoupdate, keep the `hash.url` lines in "autoupdate". + // If you prefer not to upload .sha256 files, remove or modify that. + // Just note that removing them will break the default Scoop auto-updater checks. + + // Save updates + try { + fs.writeFileSync(scoopPath, JSON.stringify(scoopJson, null, 2), "utf8"); + } catch (err) { + console.error("ERROR: Failed writing scoop manifest:", err.message); + process.exit(1); + } + + console.log(`Updated Scoop manifest with version=${newVersion}, x64Hash=${hash64}, x86Hash=${hash86}`); +} + +// 11) Create .sha256 files for each EXE in /utils so Scoop "autoupdate" can fetch them +function generateSha256FilesForScoop(projectRoot, gitTag, shellVersion, hashes) { + // We'll create: Stremio.-x64.exe.sha256 and Stremio.-x86.exe.sha256 + // in /utils, each containing the hex digest plus a newline. + + const outDir = path.join(projectRoot, "utils"); + if (!fs.existsSync(outDir)) { + fs.mkdirSync(outDir, { recursive: true }); + } + + // x64 + const x64filename = `Stremio.${shellVersion}-x64.exe.sha256`; + const x64path = path.join(outDir, x64filename); + fs.writeFileSync(x64path, hashes.x64 + "\n", "utf8"); + + // x86 + const x86filename = `Stremio.${shellVersion}-x86.exe.sha256`; + const x86path = path.join(outDir, x86filename); + fs.writeFileSync(x86path, hashes.x86 + "\n", "utf8"); + + console.log( + `\nGenerated .sha256 files:\n ${x64filename} -> ${hashes.x64}\n ${x86filename} -> ${hashes.x86}\n\n` + + "Remember to upload these *.sha256 files alongside your EXEs in the GitHub release if you want Scoop autoupdate to work." + ); +} diff --git a/build/deploy_windows.js b/build/deploy_windows.js new file mode 100644 index 0000000..5bfd849 --- /dev/null +++ b/build/deploy_windows.js @@ -0,0 +1,301 @@ +#!/usr/bin/env node + +/**************************************************************************** + * deploy_windows.js + * + * Builds windows distributable folder dist/win. + * Pass --installer to also build the windows installer + * Make sure to have set up utils/windows and the environment correctly by following windows.md + * + ****************************************************************************/ + +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +// --------------------------------------------------------------------- +// Project/Layout Configuration +// --------------------------------------------------------------------- +const ARCH = process.argv.includes('--x86') ? 'x86' : 'x64'; +const SOURCE_DIR = path.resolve(__dirname, '..'); +const BUILD_DIR = path.join(SOURCE_DIR, `cmake-build-release-${ARCH}`); +const DIST_DIR = path.join(SOURCE_DIR, 'dist', `win-${ARCH}`); +const CONFIG_DIR = path.join(SOURCE_DIR, 'dist', `win-${ARCH}`, 'portable_config'); +const PROJECT_NAME = 'stremio'; + +// Paths to Additional Dependencies +const MPV_DLL = ARCH === 'x86' + ? path.join(SOURCE_DIR, 'deps', 'libmpv', 'i686', 'libmpv-2.dll') + : path.join(SOURCE_DIR, 'deps', 'libmpv', 'x86_64', 'libmpv-2.dll'); +const SERVER_JS = path.join(SOURCE_DIR, 'utils', 'windows', 'server.js'); +const STREMIO_RUNTIME_EXE = path.join(SOURCE_DIR, 'utils', 'windows', 'stremio-runtime.exe'); +const FFMPEG_FOLDER = path.join(SOURCE_DIR, 'utils', 'windows', 'ffmpeg'); +const MPV_FOLDER = path.join(SOURCE_DIR, 'utils', 'mpv', 'anime4k'); +const DEFAULT_SETTINGS_FOLDER = path.join(SOURCE_DIR, 'utils', 'stremio'); + +// Default Paths +const DEFAULT_NSIS = 'C:\\Program Files (x86)\\NSIS\\makensis.exe'; +//VCPKG +const VCPKG_TRIPLET = ARCH === 'x86' ? 'x86-windows-static' : 'x64-windows-static'; +const VCPKG_CMAKE = 'C:/bin/vcpkg/scripts/buildsystems/vcpkg.cmake'; + +// --------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------- +(async function main() { + try { + console.log(`\n=== Building for ${ARCH.toUpperCase()} ===`); + const args = process.argv.slice(2); + const buildInstaller = args.includes('--installer'); + const buildPortable = args.includes('--portable'); + + // 3) Run CMake + Ninja in ../cmake-build-release (64-bit) + if (!fs.existsSync(BUILD_DIR)) { + fs.mkdirSync(BUILD_DIR, { recursive: true }); + } + + console.log('\n=== Running CMake in cmake-build-release ==='); + process.chdir(BUILD_DIR); + execSync( + `cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=${VCPKG_CMAKE} -DVCPKG_TARGET_TRIPLET=${VCPKG_TRIPLET} ..`, + { stdio: 'inherit' } + ); + console.log('=== Running Ninja in cmake-build-release ==='); + execSync('ninja', { stdio: 'inherit' }); + + // Return to script directory + process.chdir(__dirname); + + // 4) Prepare dist\win + console.log(`\n=== Cleaning and creating ${DIST_DIR} ===`); + safeRemove(DIST_DIR); + fs.mkdirSync(DIST_DIR, { recursive: true }); + + // 5) Copy main .exe + const builtExe = path.join(BUILD_DIR, `${PROJECT_NAME}.exe`); + const distExe = path.join(DIST_DIR, `${PROJECT_NAME}.exe`); + copyFile(builtExe, distExe); + + // 6) Copy mpv DLL, server.js, node.exe + copyFile(MPV_DLL, path.join(DIST_DIR, path.basename(MPV_DLL))); + copyFile(SERVER_JS, path.join(DIST_DIR, path.basename(SERVER_JS))); + + + + + // 8) Flatten stremio-runtime, ffmpeg + console.log('Flattening DS folder, stremio-runtime, ffmpeg...'); + copyFile(STREMIO_RUNTIME_EXE, path.join(DIST_DIR, 'stremio-runtime.exe')); + copyFolderContents(FFMPEG_FOLDER, DIST_DIR); + copyFolderContentsPreservingStructure(MPV_FOLDER, DIST_DIR); + copyFolderContentsPreservingStructure(DEFAULT_SETTINGS_FOLDER, CONFIG_DIR); + + console.log('\n=== dist\\win preparation complete. ==='); + + // 10) If --installer, parse version and build NSIS + if (buildInstaller) { + console.log('\n--installer detected: building NSIS installer...'); + // Extract the version first so we can set process.env before calling NSIS + const version = getPackageVersionFromCMake(); + process.env.package_version = version; + console.log(`Set package_version to: ${version}`); + buildNsisInstaller(); + } else if (buildPortable) { + console.log('\n--portable detected: building Portable...'); + buildPortableZip(); + } + + + console.log('\nAll done!'); + } catch (err) { + console.error('Error in deploy_windows.js:', err); + process.exit(1); + } +})(); + +/**************************************************************************** + * Helper Functions + ****************************************************************************/ + +function safeRemove(dirPath) { + if (fs.existsSync(dirPath)) { + fs.rmSync(dirPath, { recursive: true, force: true }); + } +} + +function copyFile(src, dest) { + if (!fs.existsSync(src)) { + console.warn(`Warning: missing file: ${src}`); + return; + } + fs.copyFileSync(src, dest); + console.log(`Copied: ${src} -> ${dest}`); +} + +/** + * Recursively copies only the contents of "src" into "dest" (flattened). + * If src has files/folders, they go directly into dest, rather than + * creating a subfolder named src. + */ +function copyFolderContents(src, dest) { + if (!fs.existsSync(src)) { + console.warn(`Warning: missing folder: ${src}`); + return; + } + const stats = fs.statSync(src); + if (!stats.isDirectory()) { + console.warn(`Warning: not a directory: ${src}`); + return; + } + for (const item of fs.readdirSync(src)) { + const srcItem = path.join(src, item); + const itemStats = fs.statSync(srcItem); + const destItem = path.join(dest, item); + if (itemStats.isDirectory()) { + copyFolderContents(srcItem, dest); + } else { + copyFile(srcItem, destItem); + } + } +} + +/** + * Copies the contents of `src` into `dest` without flattening. + * Subdirectories in `src` will be recreated in `dest`. + */ +function copyFolderContentsPreservingStructure(src, dest) { + if (!fs.existsSync(src)) { + console.warn(`Warning: missing folder: ${src}`); + return; + } + + const stats = fs.statSync(src); + if (!stats.isDirectory()) { + console.warn(`Warning: not a directory: ${src}`); + return; + } + + // Ensure destination directory exists + if (!fs.existsSync(dest)) { + fs.mkdirSync(dest, { recursive: true }); + } + + const items = fs.readdirSync(src); + + for (const item of items) { + const srcItem = path.join(src, item); + const destItem = path.join(dest, item); + const itemStats = fs.statSync(srcItem); + + if (itemStats.isDirectory()) { + // Recursively copy subdirectories + copyFolderContentsPreservingStructure(srcItem, destItem); + } else { + if (!srcItem.endsWith('zip') && !srcItem.endsWith('7z')) { + // Copy files + copyFile(srcItem, destItem); + } + } + } +} + +/** + * Retrieves version from CMakeLists.txt (handles quotes): + * project(stremio VERSION "5.0.2") + */ +function getPackageVersionFromCMake() { + const cmakeFile = path.join(SOURCE_DIR, 'CMakeLists.txt'); + let version = '0.0.0'; + if (fs.existsSync(cmakeFile)) { + const content = fs.readFileSync(cmakeFile, 'utf8'); + // Accept either quoted or unquoted numerical version + const match = content.match(/project\s*\(\s*stremio\s+VERSION\s+"?([\d.]+)"?\)/i); + if (match) { + version = match[1]; + } + } + return version; +} + +function buildNsisInstaller() { + if (!fs.existsSync(DEFAULT_NSIS)) { + console.warn(`NSIS not found at default path: ${DEFAULT_NSIS}. Skipping installer.`); + return; + } + try { + const arch = process.argv.includes('--x86') ? 'x86' : 'x64'; // Determine architecture + const distSubfolder = `win-${arch}`; + + const distFolder = path.join(SOURCE_DIR, 'dist', distSubfolder); + if (!fs.existsSync(distFolder)) { + console.error(`Error: Distribution folder does not exist: ${distFolder}`); + process.exit(1); + } + + + const nsiScript = path.join(SOURCE_DIR, 'utils', 'windows', 'installer', 'windows-installer.nsi'); + console.log(`Running makensis.exe with version: ${process.env.package_version} ...`); + process.env.arch = arch; + execSync(`"${DEFAULT_NSIS}" "${nsiScript}"`, { stdio: 'inherit' }); + console.log(`\nInstaller created: "Stremio ${process.env.package_version}.exe"`); + } catch (err) { + console.error('Failed to run NSIS (makensis.exe):', err); + } +} + +function buildPortableZip() { + const version = getPackageVersionFromCMake(); + const portableOutput = path.join(SOURCE_DIR, 'utils', `Stremio ${version}-${ARCH}.7z`); + const fixedEdgeWebView = path.join(SOURCE_DIR, 'utils', 'windows', 'WebviewRuntime', ARCH); + const portable_config = path.join(DIST_DIR, 'portable_config'); + const distContents = DIST_DIR; // Path to dist directory contents + + console.log(`\nCreating Portable ZIP: ${portableOutput}`); + + // Common 7-Zip paths + const common7zPaths = [ + 'C:\\Program Files\\7-Zip\\7z.exe', + 'C:\\Program Files (x86)\\7-Zip\\7z.exe' + ]; + + // Find 7-Zip executable + const sevenZipPath = common7zPaths.find(fs.existsSync); + if (!sevenZipPath) { + console.error('Error: 7-Zip executable not found in common paths.'); + console.error('Please install 7-Zip and ensure it is in one of the following paths:'); + console.error(common7zPaths.join('\n')); + process.exit(1); + } + + console.log(`Using 7-Zip at: ${sevenZipPath}`); + + // Ensure the DIST_DIR exists + if (!fs.existsSync(DIST_DIR)) { + console.error(`Error: DIST_DIR does not exist: ${DIST_DIR}`); + process.exit(1); + } + + copyFolderContentsPreservingStructure(fixedEdgeWebView, portable_config); + + // Command to create the 7z archive + const zipCommand = `"${sevenZipPath}" a -t7z -mx=9 "${portableOutput}" "${distContents}\\*"`; + + try { + // Run the 7-Zip command + console.log(`Running: ${zipCommand}`); + execSync(zipCommand, { stdio: 'inherit' }); + console.log(`\nPortable ZIP created: ${portableOutput}`); + //Clean UP + const portableConfigWebView = path.join(DIST_DIR, 'portable_config', 'EdgeWebView'); + if (fs.existsSync(portableConfigWebView)) { + console.log(`\nCleaning up: ${portableConfigWebView}`); + fs.rmSync(portableConfigWebView, { recursive: true, force: true }); + console.log(`Removed: ${portableConfigWebView}`); + } else { + console.log(`\nNo cleanup needed: ${portableConfigWebView} does not exist.`); + } + } catch (error) { + console.error('Error creating the Portable ZIP:', error); + process.exit(1); + } +} \ No newline at end of file diff --git a/docs/RELEASE.md b/docs/RELEASE.md new file mode 100644 index 0000000..733ff86 --- /dev/null +++ b/docs/RELEASE.md @@ -0,0 +1,24 @@ +# Releasing New Version + +--- + +## 🚀 Quick Overview + +1. Bump version in ``cmakelists`` +2. Build new ``runtime`` and `installer` +3. Make sure `installer` is in `/utils` and `server.js` in `/utils/windows` +4. Run ``build/build_checksums.js`` this will generate `version.json` and `version-details.json` needed for the auto updater +``` +node build_checksums.js +``` +``` +node build_checksums.js "C:\Program Files\OpenSSL-Win64\bin" "5.0.0-beta.1" "5.0.0" v4.20.8 +``` + +> **⏳Note:** Only Windows at the moment + +5. Commit Changes +6. Make new release with the Git tag used when running ``build_checksums.js`` + +> **⏳Note:** Alternatively u can separate the version bump commit. Instead: +> Commit - Release - Build Checksums - Commit Built Checksums \ No newline at end of file diff --git a/docs/WINDOWS.md b/docs/WINDOWS.md new file mode 100644 index 0000000..4ee645b --- /dev/null +++ b/docs/WINDOWS.md @@ -0,0 +1,141 @@ + +# Building on Windows + +--- + +## 🚀 Quick Overview + +This guide walks you through the process of building Stremio on Windows. Follow the steps carefully to set up the environment, build dependencies, and compile the project. + +--- + +## 🛠️ Requirements + +Ensure the following are installed on your system: + +- **Operating System**: Windows 7 or newer +- **Utilities**: [7zip](https://www.7-zip.org/) or similar +- **Tools**: + - [Git](https://git-scm.com/download/win) + - [Microsoft Visual Studio](https://visualstudio.microsoft.com/) + - [CMake](https://cmake.org/) + - [WebView2](https://developer.microsoft.com/de-de/microsoft-edge/webview2/?ch=1&form=MA13LH) + - [OpenSSL](https://slproweb.com/products/Win32OpenSSL.html) + - [Node.js](https://nodejs.org/) + - [FFmpeg](https://ffmpeg.org/download.html) + - [MPV](https://sourceforge.net/projects/mpv-player-windows/) + +--- + +## 📂 Setup Guide + +### 1️⃣ **Install Essential Tools** +- **Git**: [Download](https://git-scm.com/download/win) and install. +- **Visual Studio**: [Download Community 2022](https://visualstudio.microsoft.com/de/downloads/). +- **Node.js**: Get version [v8.17.0](https://nodejs.org/dist/v8.17.0/win-x86/node.exe). +- **FFmpeg**: [Download](https://ffmpeg.zeranoe.com/builds/win32/static/ffmpeg-3.3.4-win32-static.zip). + *(Other versions may also work)*. + +--- + +### 2️⃣ **Install dependency** + + +1. Download vcpkg [here](https://github.com/microsoft/vcpkg). + +2. Install using vcpkg ``openssl:x64-windows-static`` and ``nlohmann-json:x64-windows-static`` + +--- + +### 3️⃣ **Prepare the MPV Library** + +- Download the MPV library: [MPV libmpv](https://sourceforge.net/projects/mpv-player-windows/files/libmpv/). +- Use the `mpv-x86_64` version. +> **⏳ Note:** The submodule https://github.com/Zaarrg/libmpv already includes .lib, just make sure to unzip the actual .dll for x64 systems. +--- + +### 4️⃣ **Clone and Configure the Repository** + +1. Clone the repository: + ```cmd + git clone --recursive git@github.com:Zaarrg/stremio-desktop-v5.git + cd stremio-dekstop-v5 + ``` +2. Update CMake Options: + ```cmd + -DCMAKE_TOOLCHAIN_FILE=C:/bin/vcpkg/scripts/buildsystems/vcpkg.cmake + -DVCPKG_TARGET_TRIPLET=x64-windows-static + ``` + + ```cmd + -DCMAKE_TOOLCHAIN_FILE=C:\bin\vcpkg\scripts\buildsystems\vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x86-windows-static + ``` +3. Download the `server.js` file: + ```cmd + powershell -Command Start-BitsTransfer -Source "https://s3-eu-west-1.amazonaws.com/stremio-artifacts/four/v%package_version%/server.js" -Destination server.js + ``` + +> **⏳ Note:** To run the cmake project make sure to add `stremio-runtime.exe, server.js` beside the `stremio.exe` + +--- + +### 5️⃣ **Build / Deploying the Shell** + +1. Make sure to run the following in the `x64 Native Tools Command Prompt for VS 2022` or `x86 Native Tools Command Prompt for VS 2022` + + 2. Run the deployment script in the ``build`` folder. By default builds ``x64`` + ```cmd + node deploy_windows.js --installer + ``` + - For Portable (Needs ``7zip`` and ``/utils/WebviewRuntime/x64/EdgeWebView`` ) + ```cmd + node deploy_windows.js --portable + ``` + + - For x86 + ```cmd + node deploy_windows.js --x86 --installer + ``` + - For Portable x86 (Needs ``7zip`` and ``/utils/WebviewRuntime/x86/EdgeWebView`` ) + ```cmd + node deploy_windows.js --x86 --portable + ``` + + +> **⏳ Note:** This script uses common path for ``DCMAKE_TOOLCHAIN_FILE`` of vcpkg, make sure the script has the correct one. If running with ``--installer`` make sure u installed ``nsis`` with the needed ``nsprocess`` plugin at least once. + +3. Done. This will build the `installer` and ``dist/win`` folder. + +> **⏳ Note:** This will create `dist/win` with all necessary files like `node.exe`, `ffmpeg.exe`. Also make sure to have `stremio-runtime.exe, server.js` are in `utils\windows\` folder +--- + +## 📦 Installer (Optional) + +1. Download and install [NSIS](https://nsis.sourceforge.io/Download). + Default path: `C:\Program Files (x86)\NSIS`. + +2. Generate the installer: + ```cmd + FOR /F "tokens=4 delims=() " %i IN ('findstr /C:"project(stremio VERSION" CMakeLists.txt') DO @set "package_version=%~i" + set arch = "x64"; + "C:\Program Files (x86)\NSIS\makensis.exe" utils\windows\installer\windows-installer.nsi + ``` + - Result: `Stremio %package_version%.exe`. + +--- + +## 🔧 Silent Installation + +Run the installer with `/S` (silent mode) and configure via these options: + +- `/notorrentassoc`: Skip `.torrent` association. +- `/nodesktopicon`: Skip desktop shortcut. + +Silent uninstall: +```cmd +"%LOCALAPPDATA%\Programs\LNV\Stremio-5\Uninstall.exe" /S /keepdata +``` + +--- + +✨ **Happy Building!** diff --git a/images/stremio.ico b/images/stremio.ico new file mode 100644 index 0000000..17ef6fc Binary files /dev/null and b/images/stremio.ico differ diff --git a/images/stremio.png b/images/stremio.png new file mode 100644 index 0000000..28d3ccb Binary files /dev/null and b/images/stremio.png differ diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..b0fbf13 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,2721 @@ +#pragma comment(linker, "/SUBSYSTEM:WINDOWS") +#pragma comment(linker, "/ENTRY:mainCRTStartup") + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + + +#pragma comment(lib, "dwmapi.lib") +#pragma comment(lib, "uxtheme.lib") +#pragma comment(lib, "Msimg32.lib") + +#include +#pragma comment(lib, "gdiplus.lib") + + +// WebView2 +#include "WebView2.h" + +// mpv +#include "mpv/client.h" + +// png +#include "resource.h" + +// nlohmann/json +#include +#include +#include +#include + +#include "nlohmann/json.hpp" +using json = nlohmann::json; + +using namespace Microsoft::WRL; + +// ----------------------------------------------------------------------------- +// Globals +// ----------------------------------------------------------------------------- + +#define APP_TITLE "Stremio - Freedom to Stream" +#define APP_NAME "Stremio" +#define APP_CLASS L"Stremio" +#define APP_VERSION "5.0.0" + +static TCHAR szWindowClass[] = APP_NAME; +static TCHAR szTitle[] = APP_TITLE; + +static HINSTANCE g_hInst = nullptr; +static HWND g_hWnd = nullptr; +static HBRUSH g_darkBrush = nullptr; +static HANDLE g_hMutex = nullptr; // for single-instance +static HHOOK g_hMouseHook = nullptr; + +// URLS +std::wstring g_webuiUrl = L"https://zaarrg.github.io/stremio-web-shell-fixes/"; +std::string g_updateUrl = "https://raw.githubusercontent.com/Zaarrg/stremio-desktop-v5/refs/heads/webview-windows/version/version.json"; + +//Args +bool g_streamingServer = true; +bool g_autoupdaterForceFull = false; + +// mpv +static mpv_handle* g_mpv = nullptr; +static std::set g_observedProps; + +#define IDR_MAINFRAME 101 +#define WM_MPV_WAKEUP (WM_APP + 2) + +// Node server +static std::atomic_bool g_nodeRunning = false; +static std::thread g_nodeThread; +static HANDLE g_nodeProcess = nullptr; +static HANDLE g_nodeOutPipe = nullptr; +static HANDLE g_nodeInPipe = nullptr; + +// WebView2 +static wil::com_ptr g_webviewController; +static wil::com_ptr g_webviewProfile; +static wil::com_ptr g_webview; + +// Tray +#define WM_TRAYICON (WM_APP + 1) +#define ID_TRAY_SHOWWINDOW 1001 +#define ID_TRAY_ALWAYSONTOP 1002 +#define ID_TRAY_CLOSE_ON_EXIT 1003 +#define ID_TRAY_USE_DARK_THEME 1004 +#define ID_TRAY_PAUSE_MINIMIZED 1005 +#define ID_TRAY_PAUSE_FOCUS_LOST 1006 +#define ID_TRAY_PICTURE_IN_PICTURE 1007 +#define ID_TRAY_QUIT 1008 + +struct MenuItem +{ + UINT id; + bool checked; + bool separator; + std::wstring text; +}; +static std::vector g_menuItems; +static LRESULT CALLBACK DarkTrayMenuProc(HWND, UINT, WPARAM, LPARAM); + +// State Globals +static NOTIFYICONDATA g_nid = {0}; +static bool g_showWindow = true; +static bool g_alwaysOnTop= false; +static bool g_isFullscreen = false; +static bool g_closeOnExit = false; +static bool g_useDarkTheme = false; +static bool g_isPipMode = false; +static int g_thumbFastHeight = 0; +static int g_hoverIndex = -1; +static HFONT g_hMenuFont = nullptr; +static HANDLE g_serverJob = nullptr; +static HWND g_trayHwnd = nullptr; + +//Ini Settings +static bool g_pauseOnMinimize = true; +static bool g_pauseOnLostFocus = false; +static bool g_allowZoom = false; +// Tray Sizes +static int g_tray_itemH = 31; +static int g_tray_sepH = 8; +static int g_tray_w = 200; + +// Splash Screen +static HWND g_hSplash = nullptr; +static HBITMAP g_hSplashImage = nullptr; +static float g_splashOpacity = 1.0f; +static int g_pulseDirection = -1; // -1 decrease, +1 increase +static ULONG_PTR g_gdiplusToken = 0; + +// App Ready and Event Queue +static std::vector g_pendingMessages; +static bool g_isAppReady = false; + +// Updater +static std::atomic_bool g_updaterRunning = false; +static std::filesystem::path g_installerPath; +static std::thread g_updaterThread; +static const char* public_key_pem = R"(-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoXoJRQ81xOT3Gx6+hsWM +ZiD4PwtLdxxNhEdL/iK0yp6AdO/L0kcSHk9YCPPx0XPK9sssjSV5vCbNE/2IJxnh +/mV+3GAMmXgMvTL+DZgrHafnxe1K50M+8Z2z+uM5YC9XDLppgnC6OrUjwRqNHrKI +T1vcgKf16e/TdKj8xlgadoHBECjv6dr87nbHW115bw8PVn2tSk/zC+QdUud+p6KV +zA6+FT9ZpHJvdS3R0V0l7snr2cwapXF6J36aLGjJ7UviRFVWEEsQaKtAAtTTBzdD +4B9FJ2IJb/ifdnVzeuNTDYApCSE1F89XFWN9FoDyw7Jkk+7u4rsKjpcnCDTd9ziG +kwIDAQAB +-----END PUBLIC KEY-----)"; + +// Thumb Fast + +static std::atomic g_ignoreHover(false); +static std::chrono::steady_clock::time_point g_ignoreUntil; +constexpr std::chrono::milliseconds IGNORE_DURATION(200); + +// Forward Declares + +LRESULT CALLBACK SplashWndProc(HWND, UINT, WPARAM, LPARAM); +void CreateSplashScreen(HWND parent); +void HideSplash(); + + +static json mpvNodeToJson(const mpv_node* node); + +static HWND GetMainHwnd() { return g_hWnd; } +static void CreateTrayIcon(HWND hWnd); +static void RemoveTrayIcon(); +static void RunInstallerAndExit(); +static void StopNodeServer(); +static void ShowTrayMenu(HWND hWnd); +static void AppendToCrashLog(const std::wstring& message); +static void AppendToCrashLog(const std::string& message); +bool FileExists(const std::wstring& path); +bool DirectoryExists(const std::wstring& dirPath); + +LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); + +// JS injection Init Shell +static const wchar_t* INIT_SHELL_SCRIPT = LR"JS_CODE( +(function(){ + if (!window.initShellComm) { + window.initShellComm = function() { + console.log("[main.cpp injection] initShellComm() default called"); + }; + } +})(); +)JS_CODE"; + +// ----------------------------------------------------------------------------- +// Single-instance +// ----------------------------------------------------------------------------- +static bool FocusExistingInstance(const std::wstring &protocolArg) +{ + // Use the actual window class name variable instead of reinterpret_cast(APP_NAME) + HWND hExistingWnd = FindWindowW(APP_CLASS, nullptr); + if (hExistingWnd) { + if (IsIconic(hExistingWnd)) { + ShowWindow(hExistingWnd, SW_RESTORE); + } else if (!IsWindowVisible(hExistingWnd)) { + ShowWindow(hExistingWnd, SW_SHOW); + } + SetForegroundWindow(hExistingWnd); + + // Send protocolArg if available + if (!protocolArg.empty()) { + COPYDATASTRUCT cds; + cds.dwData = 1; // Custom identifier + cds.cbData = static_cast(protocolArg.size() * sizeof(wchar_t)); + cds.lpData = (PVOID)protocolArg.c_str(); + SendMessage(hExistingWnd, WM_COPYDATA, 0, (LPARAM)&cds); + } + return true; + } + return false; +} + + +static bool CheckSingleInstance(int argc, char* argv[]) +{ + g_hMutex = CreateMutexW(nullptr, FALSE, L"SingleInstanceMtx_StremioWebShell"); + if(!g_hMutex){ + std::wcerr << L"CreateMutex failed => fallback to multi.\n"; + AppendToCrashLog("CreateMutex failed => fallback to multi."); + return true; + } + + std::wstring protocolArg; + for (int i = 1; i < argc; ++i) { + int size_needed = MultiByteToWideChar(CP_UTF8, 0, argv[i], -1, NULL, 0); + if (size_needed > 0) { + std::wstring argW(size_needed - 1, 0); + MultiByteToWideChar(CP_UTF8, 0, argv[i], -1, &argW[0], size_needed); + + // Check for protocol or file argument + if (argW.rfind(L"stremio://", 0) == 0 || argW.rfind(L"magnet:", 0) == 0 || FileExists(argW) ) { + protocolArg = argW; + break; + } + } + } + + if(GetLastError()==ERROR_ALREADY_EXISTS){ + FocusExistingInstance(protocolArg); + return false; + } + return true; +} + +LRESULT CALLBACK LowLevelMouseProc(int nCode, WPARAM wParam, LPARAM lParam) +{ + if (nCode == HC_ACTION && (wParam == WM_LBUTTONDOWN || wParam == WM_RBUTTONDOWN)) + { + PMSLLHOOKSTRUCT pmsh = (PMSLLHOOKSTRUCT)lParam; + if (g_trayHwnd) + { + RECT rc; + if (GetWindowRect(g_trayHwnd, &rc)) + { + // If click point is outside our tray menu window, close the menu + if (pmsh->pt.x < rc.left || pmsh->pt.x > rc.right || + pmsh->pt.y < rc.top || pmsh->pt.y > rc.bottom) + { + PostMessage(g_trayHwnd, WM_CLOSE, 0, 0); + } + } + } + } + return CallNextHookEx(g_hMouseHook, nCode, wParam, lParam); +} + +// ----------------------------------------------------------------------------- +// Helper Functions +// ----------------------------------------------------------------------------- + +inline std::string WStringToUtf8(const std::wstring &wstr) +{ + if (wstr.empty()) { + return {}; + } + // Determine required size (in bytes, including null terminator). + int neededSize = WideCharToMultiByte( + CP_UTF8, + 0, + wstr.data(), + (int)wstr.size(), + nullptr, + 0, + nullptr, + nullptr + ); + if (neededSize <= 0) { + return {}; + } + + // Allocate and perform the conversion. + std::string result(neededSize, '\0'); + WideCharToMultiByte( + CP_UTF8, + 0, + wstr.data(), + (int)wstr.size(), + &result[0], + neededSize, + nullptr, + nullptr + ); + + // Remove any trailing null if present. + if (!result.empty() && result.back() == '\0') { + result.pop_back(); + } + return result; +} + +std::wstring Utf8ToWstring(const std::string& utf8Str) { + if (utf8Str.empty()) { + return std::wstring(); + } + + // Get the required buffer size for the conversion. + int size_needed = MultiByteToWideChar(CP_UTF8, 0, utf8Str.data(), (int)utf8Str.size(), NULL, 0); + if (size_needed == 0) { + // Handle error if needed. + return std::wstring(); + } + + // Convert and store into a wstring. + std::wstring wstr(size_needed, 0); + MultiByteToWideChar(CP_UTF8, 0, utf8Str.data(), (int)utf8Str.size(), &wstr[0], size_needed); + + return wstr; +} + +// Helper for nicer error formatting mpv +std::string capitalizeFirstLetter(const std::string& input) { + if (input.empty()) return input; + std::string result = input; + result[0] = std::toupper(result[0]); + return result; +} + +std::wstring HResultToErrorMessage(HRESULT hr) +{ + LPWSTR buffer = nullptr; + + DWORD flags = FORMAT_MESSAGE_ALLOCATE_BUFFER | + FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS; + + DWORD size = FormatMessageW( + flags, + nullptr, + hr, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + reinterpret_cast(&buffer), + 0, + nullptr + ); + + if (size == 0) + { + return L"Unknown error (HRESULT=0x" + std::to_wstring(hr) + L")"; + } + + std::wstring message(buffer); + + LocalFree(buffer); + + while (!message.empty() && + (message.back() == L'\r' || message.back() == L'\n' || message.back() == L' ')) + { + message.pop_back(); + } + + return message; +} + +bool FileExists(const std::wstring& path) { + DWORD attributes = GetFileAttributesW(path.c_str()); + return (attributes != INVALID_FILE_ATTRIBUTES && + !(attributes & FILE_ATTRIBUTE_DIRECTORY)); +} + +bool DirectoryExists(const std::wstring& dirPath) { + DWORD attributes = GetFileAttributesW(dirPath.c_str()); + return (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY)); +} + +// ----------------------------------------------------------------------------- +// Dark/Light theme +// ----------------------------------------------------------------------------- +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +static void UpdateTheme(HWND hWnd) { + if (g_useDarkTheme) { + DWMNCRENDERINGPOLICY ncrp = DWMNCRP_ENABLED; + DwmSetWindowAttribute(hWnd, DWMWA_NCRENDERING_POLICY, &ncrp, sizeof(ncrp)); + BOOL dark = TRUE; + DwmSetWindowAttribute(hWnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &dark, sizeof(dark)); + } else { + BOOL dark = FALSE; + DwmSetWindowAttribute(hWnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &dark, sizeof(dark)); + } +} + +static void LoadCustomMenuFont() +{ + if (g_hMenuFont) { + DeleteObject(g_hMenuFont); + g_hMenuFont = nullptr; + } + + LOGFONTW lf = { 0 }; + lf.lfHeight = -12; + lf.lfWeight = FW_MEDIUM; + wcscpy_s(lf.lfFaceName, L"Arial Rounded MT"); + lf.lfQuality = CLEARTYPE_QUALITY; + + g_hMenuFont = CreateFontIndirectW(&lf); + + // Fallback to system menu font if the custom font is not available + if (!g_hMenuFont) { + NONCLIENTMETRICSW ncm = { sizeof(ncm) }; + if (SystemParametersInfoW(SPI_GETNONCLIENTMETRICS, sizeof(ncm), &ncm, 0)) + { + ncm.lfMenuFont.lfQuality = CLEARTYPE_QUALITY; + g_hMenuFont = CreateFontIndirectW(&(ncm.lfMenuFont)); + } + } + + if (!g_hMenuFont) { + std::cerr << "Failed to load custom menu font.\n"; + AppendToCrashLog("[FONT]: Failed to load custom menu font"); + } +} + +// ----------------------------------------------------------------------------- +// Send JSON to front-end +// ----------------------------------------------------------------------------- +static void SendToJS(const json& j) +{ + if(!g_isAppReady) { + // If WebView is not ready, queue the message + g_pendingMessages.push_back(j); + return; + } + std::string payload=j.dump(); + std::wstring wpayload(payload.begin(),payload.end()); + g_webview->PostWebMessageAsJson(wpayload.c_str()); + #ifdef DEBUG_BUILD + std::cout<<"[Native->JS] "< JSON +// ----------------------------------------------------------------------------- +static json mpvNodeArrayToJson(const mpv_node_list* list) +{ + json j=json::array(); + if(!list)return j; + for(int i=0;inum;i++){ + j.push_back(mpvNodeToJson(&list->values[i])); + } + return j; +} +static json mpvNodeMapToJson(const mpv_node_list* list) +{ + json j=json::object(); + if(!list)return j; + for(int i=0;inum;i++){ + const char* key = (list->keys&&list->keys[i])? list->keys[i] : ""; + mpv_node &val = list->values[i]; + j[key]=mpvNodeToJson(&val); + } + return j; +} +static json mpvNodeToJson(const mpv_node* node) +{ + if(!node)return nullptr; + + switch(node->format) + { + case MPV_FORMAT_STRING: + if(node->u.string) return json(node->u.string); + else return json(""); + case MPV_FORMAT_INT64: + return json((long long)node->u.int64); + case MPV_FORMAT_DOUBLE: + return json(node->u.double_); + case MPV_FORMAT_FLAG: + return json((bool)node->u.flag); + case MPV_FORMAT_NODE_ARRAY: + return mpvNodeArrayToJson(node->u.list); + case MPV_FORMAT_NODE_MAP: + return mpvNodeMapToJson(node->u.list); + default: + return ""; + } +} + + +// ----------------------------------------------------------------------------- +// mpv wakeup callback +// ----------------------------------------------------------------------------- +static void MpvWakeup(void *ctx) +{ + PostMessage((HWND)ctx, WM_MPV_WAKEUP, 0, 0); +} + +// handle mpv events +static void HandleMpvEvents() +{ + if(!g_mpv)return; + while(true){ + mpv_event* ev=mpv_wait_event(g_mpv,0); + if(!ev||ev->event_id==MPV_EVENT_NONE)break; + + if(ev->error<0) + std::cout<<"mpv event error="<error)<<"\n"; + + switch(ev->event_id) + { + case MPV_EVENT_PROPERTY_CHANGE: + { + mpv_event_property* prop=(mpv_event_property*)ev->data; + if(!prop||!prop->name)break; + + json j; + j["type"] ="mpv-prop-change"; + j["id"] =(int64_t)ev->reply_userdata; + j["name"] = prop->name; + if(ev->error<0) + j["error"]=mpv_error_string(ev->error); + + switch(prop->format) + { + case MPV_FORMAT_INT64: + if(prop->data) + j["data"]=(long long)(*(int64_t*)prop->data); + else + j["data"]=nullptr; + break; + case MPV_FORMAT_DOUBLE: + if(prop->data) + j["data"]=*(double*)prop->data; + else + j["data"]=nullptr; + break; + case MPV_FORMAT_FLAG: + if(prop->data) + j["data"]=(*(int*)prop->data!=0); + else + j["data"]=false; + break; + case MPV_FORMAT_STRING: + if(prop->data){ + const char*s=*(char**)prop->data; + j["data"]=(s? s:""); + } else { + j["data"]=""; + } + break; + case MPV_FORMAT_NODE: + j["data"]=mpvNodeToJson((mpv_node*)prop->data); + break; + default: + j["data"]=nullptr; + break; + } + SendToJS(j); + break; + } + case MPV_EVENT_END_FILE: + { + mpv_event_end_file* ef=(mpv_event_end_file*)ev->data; + json j; + j["type"]="mpv-event-ended"; + switch(ef->reason){ + case MPV_END_FILE_REASON_EOF: + j["reason"]="quit"; + SendToJS(j); + break; + case MPV_END_FILE_REASON_ERROR: { + std::string errorString = mpv_error_string(ef->error); + std::string capitalizedErrorString = capitalizeFirstLetter(errorString); + j["reason"]="error"; + if(ef->error<0) + j["error"]= capitalizedErrorString; + SendToJS(j); + AppendToCrashLog("[MPV]: " + capitalizedErrorString); + break; + } + default: + j["reason"]="other"; + break; + } + break; + } + case MPV_EVENT_SHUTDOWN: + { + std::cout<<"mpv EVENT_SHUTDOWN => terminate\n"; + mpv_terminate_destroy(g_mpv); + g_mpv=nullptr; + break; + } + default: + // ignore + break; + } + } +} + +// ----------------------------------------------------------------------------- +// Inbound Mpv commands +// ----------------------------------------------------------------------------- +static void HandleMpvCommand(const std::vector& args) +{ + // e.g. ["loadfile","video.mp4"] + if(!g_mpv||args.empty())return; + // optional fix booleans => "true"/"false" => "yes"/"no" + std::vector cargs; + for(auto &s: args)cargs.push_back(s.c_str()); + cargs.push_back(nullptr); + mpv_command(g_mpv,cargs.data()); +} +static void HandleMpvSetProp(const std::vector& args) +{ + if(!g_mpv||args.size()<2)return; + std::string val=args[1]; + if(val=="true") val="yes"; + if(val=="false")val="no"; + mpv_set_property_string(g_mpv, args[0].c_str(), val.c_str()); +} +static void HandleMpvObserveProp(const std::vector& args) +{ + if(!g_mpv||args.empty())return; + std::string pname=args[0]; + g_observedProps.insert(pname); + mpv_observe_property(g_mpv,0,pname.c_str(),MPV_FORMAT_NODE); + std::cout<<"Observing prop="< pauseArgs = { + "pause", + "true", + }; + HandleMpvSetProp(pauseArgs); +} + +// mpv init +bool InitMPV(HWND hwnd) +{ + g_mpv=mpv_create(); + if(!g_mpv){ + std::cerr<<"mpv_create failed\n"; + AppendToCrashLog("[MPV]: Create failed"); + return false; + } + + // portable config + std::wstring exeDir=GetExeDirectory(); + std::wstring cfg=exeDir+L"\\portable_config"; + CreateDirectoryW(cfg.c_str(),nullptr); + + // convert wide => utf8 + int needed=WideCharToMultiByte(CP_UTF8,0,cfg.c_str(),-1,nullptr,0,nullptr,nullptr); + std::string utf8(needed,0); + WideCharToMultiByte(CP_UTF8,0,cfg.c_str(),-1,&utf8[0],needed,nullptr,nullptr); + + mpv_set_option_string(g_mpv,"config-dir",utf8.c_str()); + mpv_set_option_string(g_mpv,"load-scripts","yes"); + mpv_set_option_string(g_mpv,"config","yes"); + mpv_set_option_string(g_mpv,"terminal","yes"); + mpv_set_option_string(g_mpv,"msg-level","all=v"); + + // wid embedding + int64_t wid=(int64_t)hwnd; + mpv_set_option(g_mpv,"wid",MPV_FORMAT_INT64,&wid); + + mpv_set_wakeup_callback(g_mpv, MpvWakeup, hwnd); + + if(mpv_initialize(g_mpv)<0){ + std::cerr<<"mpv_initialize failed\n"; + AppendToCrashLog("[MPV]: Initialize failed"); + return false; + } + + // Set VO + mpv_set_option_string(g_mpv,"vo","gpu-next"); + + //Some sub settings + mpv_set_property_string(g_mpv, "sub-blur", "20"); + + // demux/caching + mpv_set_property_string(g_mpv,"demuxer-lavf-probesize", "524288"); + mpv_set_property_string(g_mpv,"demuxer-lavf-analyzeduration","0.5"); + mpv_set_property_string(g_mpv,"demuxer-max-bytes","300000000"); + mpv_set_property_string(g_mpv,"demuxer-max-packets","150000000"); + mpv_set_property_string(g_mpv,"cache","yes"); + mpv_set_property_string(g_mpv,"cache-pause","no"); + mpv_set_property_string(g_mpv,"cache-secs","60"); + mpv_set_property_string(g_mpv,"vd-lavc-threads","0"); + mpv_set_property_string(g_mpv,"ad-lavc-threads","0"); + mpv_set_property_string(g_mpv,"audio-fallback-to-null","yes"); + mpv_set_property_string(g_mpv,"audio-client-name",APP_NAME); + mpv_set_property_string(g_mpv,"title",APP_NAME); + + return true; +} + +void CleanupMPV() +{ + if(g_mpv){ + mpv_terminate_destroy(g_mpv); + g_mpv=nullptr; + } +} + +// ----------------------------------------------------------------------------- +// Inbound event handling +// ----------------------------------------------------------------------------- +static void ToggleFullScreen(HWND hWnd, bool enable) +{ + static WINDOWPLACEMENT prevPlc={sizeof(prevPlc)}; + if(enable==g_isFullscreen)return; + g_isFullscreen=enable; + + if(enable){ + // store old + GetWindowPlacement(hWnd,&prevPlc); + MONITORINFO mi={sizeof(mi)}; + if(GetMonitorInfoW(MonitorFromWindow(hWnd,MONITOR_DEFAULTTOPRIMARY),&mi)){ + SetWindowLongW(hWnd,GWL_STYLE,WS_POPUP|WS_VISIBLE); + SetWindowPos(hWnd,HWND_TOP, + mi.rcMonitor.left,mi.rcMonitor.top, + mi.rcMonitor.right-mi.rcMonitor.left, + mi.rcMonitor.bottom-mi.rcMonitor.top, + SWP_FRAMECHANGED|SWP_SHOWWINDOW); + } + } else { + // restore + SetWindowLongW(hWnd,GWL_STYLE,WS_OVERLAPPEDWINDOW|WS_VISIBLE); + SetWindowPlacement(hWnd,&prevPlc); + SetWindowPos(hWnd,nullptr,0,0,0,0,SWP_NOMOVE|SWP_NOSIZE|SWP_FRAMECHANGED|SWP_SHOWWINDOW); + } +} + +// app-ready inbound event +static void AppStart() +{ + std::cout<<"App initialize.\n"; + json j; + j["type"] ="shellVersion"; + j["value"] =APP_VERSION; + SendToJS(j); + HideSplash(); + + for(const auto& pendingMsg : g_pendingMessages) { + SendToJS(pendingMsg); + } + g_pendingMessages.clear(); +} + +// For local files +std::string decodeURIComponent(const std::string& encoded) { + std::string result; + result.reserve(encoded.size()); + + for (size_t i = 0; i < encoded.size(); ++i) { + char c = encoded[i]; + if (c == '%' && i + 2 < encoded.size() && + std::isxdigit(static_cast(encoded[i + 1])) && + std::isxdigit(static_cast(encoded[i + 2]))) { + // Convert the two hex digits to a character + std::string hex = encoded.substr(i + 1, 2); + char decodedChar = static_cast(std::strtol(hex.c_str(), nullptr, 16)); + result.push_back(decodedChar); + i += 2; + } else { + result.push_back(c); + } + } + return result; +} + +// parse inbound Events JSON +static void HandleInboundJSON(const std::string &msg) +{ + try + { + + #ifdef DEBUG_BUILD + std::cout<<"[JS->NATIVE] "<(); + + // else mpv commands + std::vector argVec; + if(j.contains("args")){ + if(j["args"].is_string()){ + argVec.push_back(j["args"].get()); + } else if(j["args"].is_array()){ + for(auto& x: j["args"]){ + if(x.is_string()) argVec.push_back(x.get()); + else argVec.push_back(x.dump()); + } + } + } + + if(ev=="mpv-command"){ + if(!argVec.empty() && argVec[0] == "loadfile" && argVec.size() > 1) { + argVec[1] = decodeURIComponent(argVec[1]); + } + HandleMpvCommand(argVec); + } else if(ev=="mpv-set-prop"){ + HandleMpvSetProp(argVec); + } else if(ev=="mpv-observe-prop"){ + HandleMpvObserveProp(argVec); + } else if (ev=="app-ready") { + g_isAppReady = true; + AppStart(); + } else if (ev=="update-requested") { + RunInstallerAndExit(); + } else if (ev=="start-drag") { + ReleaseCapture(); + SendMessageW(GetMainHwnd(), WM_NCLBUTTONDOWN, HTCAPTION, 0); + } else if(ev == "seek-hover") { + if (g_thumbFastHeight == 0) return; + if(g_ignoreHover) { + auto now = std::chrono::steady_clock::now(); + if(now < g_ignoreUntil) { + return; + } + g_ignoreHover = false; + } + + // Expecting arguments: hovered_seconds, x, y + if(argVec.size() < 3) { + std::cerr << "seek-hover requires at least 3 arguments.\n"; + return; + } + + // Convert the y-coordinate from string to an integer + int yCoord = 0; + try { + yCoord = std::stoi(argVec[2]); + } catch(const std::exception &e) { + std::cerr << "Error converting y coordinate: " << e.what() << "\n"; + return; + } + + // Subtract the thumb fast height from y + int adjustedY = yCoord - g_thumbFastHeight; + + // Prepare command for thumbfast with adjusted y-coordinate + std::vector cmdArgs = { + "script-message-to", + "thumbfast", + "thumb", + argVec[0], // hovered_seconds + argVec[1], // x + std::to_string(adjustedY) // y with offset + }; + + HandleMpvCommand(cmdArgs); + } + else if(ev == "seek-leave") { + if (g_thumbFastHeight == 0) return; + // Set ignore flag and calculate ignore-until timestamp + g_ignoreHover = true; + g_ignoreUntil = std::chrono::steady_clock::now() + IGNORE_DURATION; + + std::vector cmdArgs = { + "script-message-to", + "thumbfast", + "clear" + }; + HandleMpvCommand(cmdArgs); + } else { + std::cout<<"Unknown event="< screenWidth) { + posX = cursor.x - w; + } + + // If repositioning off left edge, clamp to screen left + if (posX < 0) { + posX = 0; + } + + // Ensure vertical position is within screen bounds + if(posY < 0) posY = 0; + if(posY + totalH > screenHeight) posY = screenHeight - totalH; + + // Set window position and size + SetWindowPos(hMenuWnd, HWND_TOPMOST, posX, posY, w, totalH, SWP_SHOWWINDOW); + + CreateRoundedRegion(hMenuWnd, w, totalH, 10); + ShowWindow(hMenuWnd, SW_SHOW); + UpdateWindow(hMenuWnd); + + g_hMouseHook = SetWindowsHookExW(WH_MOUSE_LL, LowLevelMouseProc, NULL, 0); +} + +static LRESULT CALLBACK DarkTrayMenuProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) +{ + switch(msg) + { + // Force close if user clicks away + case WM_ACTIVATE: + if (LOWORD(wParam) == WA_INACTIVE) { + DestroyWindow(hWnd); + } + break; + + case WM_KILLFOCUS: + case WM_CAPTURECHANGED: + DestroyWindow(hWnd); + break; + + case WM_ERASEBKGND: + return 1; + + case WM_PAINT: + { + PAINTSTRUCT ps; + HDC hdc = BeginPaint(hWnd, &ps); + + RECT rcClient; + GetClientRect(hWnd, &rcClient); + + // Decide background colors depending on g_useDarkTheme + COLORREF bgBase, bgHover, txtNormal, txtCheck, lineColor; + if (g_useDarkTheme) + { + bgBase = RGB(30,30,30); + bgHover = RGB(50,50,50); + txtNormal= RGB(200,200,200); + txtCheck = RGB(200,200,200); + lineColor= RGB(80,80,80); + } + else + { + bgBase = RGB(240,240,240); + bgHover = RGB(200,200,200); + txtNormal= RGB(0,0,0); + txtCheck = RGB(0,0,0); + lineColor= RGB(160,160,160); + } + + // Fill entire background + HBRUSH bgBrush = CreateSolidBrush(bgBase); + FillRect(hdc, &rcClient, bgBrush); + DeleteObject(bgBrush); + + // item metrics + int y = 0; + int itemH = g_tray_itemH; + int sepH = g_tray_sepH; + + // For each menu item + for (int i=0; i<(int)g_menuItems.size(); i++) + { + auto &it = g_menuItems[i]; + if (it.separator) + { + // draw a line + int midY = y + sepH/2; + HPEN oldPen = (HPEN)SelectObject(hdc, CreatePen(PS_SOLID,1, lineColor)); + MoveToEx(hdc, 5, midY, nullptr); + LineTo(hdc, rcClient.right-5, midY); + DeleteObject(SelectObject(hdc, oldPen)); + y += sepH; + } + else + { + bool hovered = (i == g_hoverIndex); + // Fill item background + RECT itemRc = {0, y, rcClient.right, y+itemH}; + COLORREF itemColor = hovered ? bgHover : bgBase; + HBRUSH itemBg = CreateSolidBrush(itemColor); + FillRect(hdc, &itemRc, itemBg); + DeleteObject(itemBg); + + // Check area (16 px) + bool checked = it.checked; + RECT cbox = { 4, y, 4+16, y+itemH }; + if (checked) + { + // draw check glyph => “\u2714” or “\u2713” + SetTextColor(hdc, txtCheck); + SetBkMode(hdc, TRANSPARENT); + DrawTextW(hdc, L"\u2713", -1, &cbox, DT_SINGLELINE | DT_CENTER | DT_VCENTER); + } + + // draw label + SetBkMode(hdc, TRANSPARENT); + SetTextColor(hdc, txtNormal); + + // select custom font + HFONT oldFnt = (HFONT)SelectObject(hdc, g_hMenuFont); + + RECT textRc = { 24, y, rcClient.right-5, y+itemH }; + DrawTextW(hdc, it.text.c_str(), -1, &textRc, DT_SINGLELINE | DT_VCENTER | DT_LEFT); + + SelectObject(hdc, oldFnt); + + y += itemH; + } + } + + EndPaint(hWnd, &ps); + return 0; + } + case WM_MOUSEMOVE: + { + int xPos = GET_X_LPARAM(lParam); + int yPos = GET_Y_LPARAM(lParam); + int itemH = g_tray_itemH, sepH = g_tray_sepH; + int curY = 0, hover = -1; + + for(int i = 0; i < (int)g_menuItems.size(); i++) + { + auto &it = g_menuItems[i]; + int h = it.separator ? sepH : itemH; + if(!it.separator && yPos >= curY && yPos < (curY + h)) + { + hover = i; + break; + } + curY += h; + } + if(hover != g_hoverIndex) + { + g_hoverIndex = hover; + InvalidateRect(hWnd, nullptr, FALSE); + } + // Change cursor to pointer if hovering over a clickable item, otherwise default arrow + if(g_hoverIndex != -1) { + SetCursor(LoadCursor(nullptr, IDC_HAND)); + } else { + SetCursor(LoadCursor(nullptr, IDC_ARROW)); + } + break; + } + case WM_LBUTTONUP: + { + // Unhook the low-level mouse hook to prevent interference + if (g_hMouseHook) { + UnhookWindowsHookEx(g_hMouseHook); + g_hMouseHook = nullptr; + } + + // Check if there's a hovered, non-separator item + if (g_hoverIndex >= 0 && g_hoverIndex < (int)g_menuItems.size()) + { + auto &it = g_menuItems[g_hoverIndex]; + if (!it.separator) + { + PostMessage(GetMainHwnd(), WM_COMMAND, it.id, 0); + } + } + DestroyWindow(hWnd); + break; + } + case WM_DESTROY: + g_hoverIndex = -1; + if (g_hMouseHook) { + UnhookWindowsHookEx(g_hMouseHook); + g_hMouseHook = nullptr; + } + ReleaseCapture(); + break; + } + return DefWindowProcW(hWnd, msg, wParam, lParam); +} + +static void ShowTrayMenu(HWND /*hWnd*/) +{ + ShowDarkTrayMenu(); +} + +static void CreateTrayIcon(HWND hWnd) +{ + g_nid.cbSize=sizeof(NOTIFYICONDATA); + g_nid.hWnd=hWnd; + g_nid.uID=1; + g_nid.uFlags=NIF_ICON|NIF_MESSAGE|NIF_TIP; + g_nid.uCallbackMessage=WM_TRAYICON; + + HICON hIcon = LoadIcon(g_hInst, MAKEINTRESOURCE(IDR_MAINFRAME)); + g_nid.hIcon = hIcon; + + _tcscpy_s(g_nid.szTip,_T("Stremio SingleInstance")); + + Shell_NotifyIcon(NIM_ADD,&g_nid); +} + +static void RemoveTrayIcon() +{ + Shell_NotifyIcon(NIM_DELETE,&g_nid); + if(g_nid.hIcon){ + DestroyIcon(g_nid.hIcon); + g_nid.hIcon=nullptr; + } +} + + +// ----------------------------------------------------------------------------- +// Splash Screen +// ----------------------------------------------------------------------------- +void CreateSplashScreen(HWND parent) +{ + WNDCLASSEXW splashWcex = {0}; + splashWcex.cbSize = sizeof(WNDCLASSEXW); + splashWcex.lpfnWndProc = SplashWndProc; + splashWcex.hInstance = g_hInst; + splashWcex.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH); + splashWcex.lpszClassName = L"SplashScreenClass"; + RegisterClassExW(&splashWcex); + + // Get client area of the main window + RECT rcClient; + GetClientRect(parent, &rcClient); + int width = rcClient.right - rcClient.left; + int height = rcClient.bottom - rcClient.top; + + // Create child window of the main window + g_hSplash = CreateWindowExW( + 0, + L"SplashScreenClass", + nullptr, + WS_CHILD | WS_VISIBLE, + 0, 0, + width, height, + parent, + nullptr, + g_hInst, + nullptr + ); + + if(!g_hSplash) { + DWORD errorCode = GetLastError(); + std::string errorMessage = std::string("[SPLASH]: Failed to load custom menu font. Error=") + + std::to_string(errorCode); + std::cerr << errorMessage << "\n"; + AppendToCrashLog(errorMessage); + return; + } + + // Load the PNG image resource. + { + HRSRC hRes = FindResource(g_hInst, MAKEINTRESOURCE(IDR_SPLASH_PNG), RT_RCDATA); + if(!hRes) { + std::cerr << "Could not find PNG resource.\n"; + } else { + HGLOBAL hData = LoadResource(g_hInst, hRes); + DWORD size = SizeofResource(g_hInst, hRes); + void* pData = LockResource(hData); + if(!pData) { + std::cerr << "LockResource returned null.\n"; + } else { + // Create an IStream on this resource memory + IStream* pStream = nullptr; + if(CreateStreamOnHGlobal(nullptr, TRUE, &pStream) == S_OK) + { + ULONG written = 0; + pStream->Write(pData, size, &written); + LARGE_INTEGER liZero = {}; + pStream->Seek(liZero, STREAM_SEEK_SET, nullptr); + + // Create GDI+ bitmap from the IStream + Gdiplus::Bitmap bitmap(pStream); + if(bitmap.GetLastStatus() == Gdiplus::Ok) + { + HBITMAP hBmp = NULL; + if(bitmap.GetHBITMAP(Gdiplus::Color(0,0,0,0), &hBmp) == Gdiplus::Ok) { + g_hSplashImage = hBmp; + } else { + std::cerr << "Failed to create HBITMAP from embedded PNG.\n"; + } + } else { + std::cerr << "Failed to decode embedded PNG data.\n"; + } + pStream->Release(); + } + } + } + } + + // Animation update speed + SetTimer(g_hSplash, 1, 4, nullptr); + + // Bring splash window to top + SetWindowPos( + g_hSplash, + HWND_TOP, + 0, 0, width, height, + SWP_SHOWWINDOW + ); + + // Force an initial repaint + InvalidateRect(g_hSplash, nullptr, TRUE); +} + +LRESULT CALLBACK SplashWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + switch(message) + { + case WM_TIMER: + { + // Use "g_splashSpeed" to scale the animation movement + const float baseStep = 0.01f; + const float splashSpeed = 1.1f; + float actualStep = baseStep * splashSpeed; + + g_splashOpacity += actualStep * g_pulseDirection; + + if (g_splashOpacity <= 0.3f) + { + g_splashOpacity = 0.3f; + g_pulseDirection = 1; + } + else if (g_splashOpacity >= 1.0f) + { + g_splashOpacity = 1.0f; + g_pulseDirection = -1; + } + + InvalidateRect(hWnd, nullptr, FALSE); + break; + } + + case WM_PAINT: + { + PAINTSTRUCT ps; + HDC hdc = BeginPaint(hWnd, &ps); + + RECT rc; + GetClientRect(hWnd, &rc); + int winW = rc.right - rc.left; + int winH = rc.bottom - rc.top; + + // Create an offscreen DC and bitmap the size of the window + HDC memDC = CreateCompatibleDC(hdc); + HBITMAP memBmp = CreateCompatibleBitmap(hdc, winW, winH); + HGDIOBJ oldMemBmp = SelectObject(memDC, memBmp); + + // Fill background in offscreen DC + HBRUSH bgBrush = CreateSolidBrush(RGB(12, 11, 17)); // "#0c0b11" + FillRect(memDC, &rc, bgBrush); + DeleteObject(bgBrush); + + // Draw the PNG at current opacity + if(g_hSplashImage) + { + BITMAP bm; + GetObject(g_hSplashImage, sizeof(bm), &bm); + + int imgWidth = bm.bmWidth; + int imgHeight = bm.bmHeight; + int destX = (winW - imgWidth) / 2; + int destY = (winH - imgHeight) / 2; + + // Create a DC for the image + HDC imgDC = CreateCompatibleDC(memDC); + HGDIOBJ oldImgBmp = SelectObject(imgDC, g_hSplashImage); + + BLENDFUNCTION blend = {}; + blend.BlendOp = AC_SRC_OVER; + blend.SourceConstantAlpha = (BYTE)(g_splashOpacity * 255); + blend.AlphaFormat = 0; + blend.AlphaFormat = AC_SRC_ALPHA; + + // Create a temp DC/bitmap for alpha compositing + HBITMAP tempBmp = CreateCompatibleBitmap(memDC, imgWidth, imgHeight); + HDC tempDC = CreateCompatibleDC(memDC); + HGDIOBJ oldTempBmp = SelectObject(tempDC, tempBmp); + + // Copy the image into tempDC + BitBlt(tempDC, 0, 0, imgWidth, imgHeight, imgDC, 0, 0, SRCCOPY); + + // Blend from tempDC onto memDC + AlphaBlend(memDC, destX, destY, imgWidth, imgHeight, + tempDC, 0, 0, imgWidth, imgHeight, blend); + + // Cleanup + SelectObject(tempDC, oldTempBmp); + DeleteObject(tempBmp); + DeleteDC(tempDC); + + SelectObject(imgDC, oldImgBmp); + DeleteDC(imgDC); + } + + // Finally, blit the offscreen to the real device + BitBlt(hdc, 0, 0, winW, winH, memDC, 0, 0, SRCCOPY); + + // Cleanup + SelectObject(memDC, oldMemBmp); + DeleteObject(memBmp); + DeleteDC(memDC); + + EndPaint(hWnd, &ps); + break; + } + + case WM_DESTROY: + KillTimer(hWnd, 1); + break; + + default: + return DefWindowProc(hWnd, message, wParam, lParam); + } + return 0; +} + +void HideSplash() +{ + if (g_hSplash) + { + KillTimer(g_hSplash, 1); + DestroyWindow(g_hSplash); + g_hSplash = nullptr; + } + if (g_hSplashImage) { + DeleteObject(g_hSplashImage); + g_hSplashImage = nullptr; + } +} + + +// ----------------------------------------------------------------------------- +// PictureInPicture / PiP +// ----------------------------------------------------------------------------- +void TogglePictureInPicture(HWND hWnd, bool enable) { + LONG style = GetWindowLong(hWnd, GWL_STYLE); + if (enable) { + g_alwaysOnTop = true; + style &= ~WS_CAPTION; + SetWindowLong(hWnd, GWL_STYLE, style); + SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 0, 0,SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_FRAMECHANGED); + } else { + g_alwaysOnTop = false; + style |= WS_CAPTION; + SetWindowLong(hWnd, GWL_STYLE, style); + SetWindowPos(hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_FRAMECHANGED); + } + g_isPipMode = enable; + + if(g_webview) { + if(enable) { + json j; + j["type"] ="showPictureInPicture"; + SendToJS(j); + } else { + json j; + j["type"] ="hidePictureInPicture"; + SendToJS(j); + } + } +} + + +// ----------------------------------------------------------------------------- +// Exception Handler +// ----------------------------------------------------------------------------- +void Cleanup() { + RemoveTrayIcon(); + CleanupMPV(); + StopNodeServer(); + + if(g_gdiplusToken) { + Gdiplus::GdiplusShutdown(g_gdiplusToken); + } +} + +static std::wstring GetDailyCrashLogPath() +{ + std::time_t t = std::time(nullptr); + std::tm localTime; + localtime_s(&localTime, &t); + + std::wstringstream filename; + filename << L"\\errors-" + << localTime.tm_mday << L"." + << (localTime.tm_mon + 1) << L"." + << (localTime.tm_year + 1900) << L".txt"; + + std::wstring exeDir = GetExeDirectory(); + std::wstring pcDir = exeDir + L"\\portable_config"; + return pcDir + filename.str(); +} + +static void AppendToCrashLog(const std::wstring& message) +{ + std::wofstream logFile; + logFile.open(GetDailyCrashLogPath(), std::ios::app); + if(!logFile.is_open()) { + return; + } + + std::time_t t = std::time(nullptr); + std::tm localTime; + localtime_s(&localTime, &t); + + // Write timestamp and message + logFile << L"[" << std::put_time(&localTime, L"%H:%M:%S") << L"] " + << message << std::endl; +} + +static void AppendToCrashLog(const std::string& message) +{ + // Convert std::string to std::wstring and call the existing function + std::wstring wideMessage = Utf8ToWstring(message); + AppendToCrashLog(wideMessage); +} + +LONG WINAPI ExceptionFilter(EXCEPTION_POINTERS* ExceptionInfo) { + std::cout<<"WM_DESTROY_CRASH => stopping mpv + node + tray.\n"; + std::wstringstream ws; + ws << L"Unhandled exception occurred! Exception code: 0x" + << std::hex << ExceptionInfo->ExceptionRecord->ExceptionCode; + AppendToCrashLog(ws.str()); + Cleanup(); + + return EXCEPTION_EXECUTE_HANDLER; +} + + +// ----------------------------------------------------------------------------- +// WebView2 +// ----------------------------------------------------------------------------- + +static void SetupWebMessageHandler() +{ + if(!g_webview)return; + EventRegistrationToken navToken; + g_webview->add_NavigationCompleted( + Callback( + [](ICoreWebView2* snd, ICoreWebView2NavigationCompletedEventArgs* args)->HRESULT + { + snd->ExecuteScript(L"initShellComm();",nullptr); + return S_OK; + } + ).Get(), + &navToken + ); + + EventRegistrationToken contextMenuToken; + g_webview->add_ContextMenuRequested( + Callback( + [](ICoreWebView2* sender, ICoreWebView2ContextMenuRequestedEventArgs* args) -> HRESULT { + wil::com_ptr items; + HRESULT hr = args->get_MenuItems(&items); + if (FAILED(hr) || !items) { + return hr; + } + + #ifdef DEBUG_BUILD + return S_OK; //DEV TOOLS DEBUG ONLY + #endif + wil::com_ptr target; + hr = args->get_ContextMenuTarget(&target); + BOOL isEditable = FALSE; + if (SUCCEEDED(hr) && target) { + hr = target->get_IsEditable(&isEditable); + } + if (FAILED(hr)) { + return hr; + } + + UINT count = 0; + items->get_Count(&count); + + if (!isEditable) { + while(count > 0) { + wil::com_ptr item; + items->GetValueAtIndex(0, &item); + if(item) { + items->RemoveValueAtIndex(0); + } + items->get_Count(&count); + } + return S_OK; + } + + // Define allowed command IDs for filtering + std::set allowedCommandIds = { + 50151, // Cut + 50150, // Copy + 50152, // Paste + 50157, // Paste as plain text + 50156 // Select all + }; + + for (UINT i = 0; i < count; ) + { + wil::com_ptr item; + hr = items->GetValueAtIndex(i, &item); + if (FAILED(hr) || !item) { + ++i; + continue; + } + + INT32 commandId = 0; + hr = item->get_CommandId(&commandId); + if (FAILED(hr)) { + ++i; + continue; + } + + // If the commandId is not in the allowed list, remove the item + if (allowedCommandIds.find(commandId) == allowedCommandIds.end()) { + hr = items->RemoveValueAtIndex(i); + if (FAILED(hr)) { + std::wcerr << L"Failed to remove item at index " << i << std::endl; + return hr; + } + // After removal, the collection size reduces, so update count and don't increment i + items->get_Count(&count); + continue; + } + ++i; + } + return S_OK; + } + ).Get(), + &contextMenuToken + ); + + + EventRegistrationToken messageToken; + g_webview->add_WebMessageReceived( + Callback( + [](ICoreWebView2* /*sender*/, ICoreWebView2WebMessageReceivedEventArgs* args)->HRESULT + { + wil::unique_cotaskmem_string msgRaw; + args->TryGetWebMessageAsString(&msgRaw); + if(!msgRaw)return S_OK; + std::wstring wstr(msgRaw.get()); + std::string strUtf8 = WStringToUtf8(wstr); + HandleInboundJSON(strUtf8); + return S_OK; + } + ).Get(),&messageToken + ); + + EventRegistrationToken newWindowToken; + g_webview->add_NewWindowRequested( + Microsoft::WRL::Callback( + [](ICoreWebView2* /*sender*/, ICoreWebView2NewWindowRequestedEventArgs* args) -> HRESULT + { + // Mark the event as handled to prevent default behavior + args->put_Handled(TRUE); + + wil::unique_cotaskmem_string uri; + if (SUCCEEDED(args->get_Uri(&uri)) && uri) + { + std::wstring wuri(uri.get()); + // Check if the URI is a local file (starts with "file://") + if (wuri.rfind(L"file://", 0) == 0) + { + std::wstring filePath = wuri.substr(8); + std::string utf8FilePath = WStringToUtf8(filePath); + json j; + j["type"] = "FileDropped"; + j["path"] = utf8FilePath; + SendToJS(j); + return S_OK; + } + + // For non-file URIs, open externally + ShellExecuteW(nullptr, L"open", uri.get(), nullptr, nullptr, SW_SHOWNORMAL); + } + return S_OK; + } + ).Get(), + &newWindowToken + ); + + + EventRegistrationToken cfeToken; + g_webview->add_ContainsFullScreenElementChanged( + Microsoft::WRL::Callback( + [](ICoreWebView2* sender, IUnknown* /*args*/) -> HRESULT + { + // FullScreen Toggle Handle + BOOL inFull = FALSE; + sender->get_ContainsFullScreenElement(&inFull); + + ToggleFullScreen(g_hWnd, inFull != FALSE); + + return S_OK; + } + ).Get(), + &cfeToken + ); +} + +std::vector GetExtensionPaths(const std::wstring& extensionsRoot) { + namespace fs = std::filesystem; + std::vector paths; + try { + for (const auto& entry : fs::directory_iterator(extensionsRoot)) { + if (entry.is_directory()) { + paths.push_back(entry.path().wstring()); + std::cout<<"PATH EXTENSION"<AddBrowserExtension( + extPath.c_str(), + Microsoft::WRL::Callback( + [extPath](HRESULT result, ICoreWebView2BrowserExtension* extension) -> HRESULT + { + if (SUCCEEDED(result)) + { + std::cout << "[EXTENSIONS]: Added Browser Extension: " << WStringToUtf8(extPath) << std::endl; + } + else + { + std::wstring error = L"[EXTENSIONS]: Failed to add Browser Extension: " + HResultToErrorMessage(result) + L" | " + extPath; + std::cout << WStringToUtf8(error) << std::endl; + AppendToCrashLog(error); + } + return S_OK; + } + ).Get() + ); + + if (FAILED(hr)) + { + std::wstring error = L"[EXTENSIONS]: Failed Add Browser Extension Callback: " + HResultToErrorMessage(hr); + std::cout << WStringToUtf8(error) << std::endl; + AppendToCrashLog(error); + } + } +} + +static ComPtr setupEnvironment() { + auto options = Microsoft::WRL::Make(); + ComPtr options6; + if (options.As(&options6) == S_OK) + { + options6->put_AreBrowserExtensionsEnabled(TRUE); + } + ComPtr options5; + if (options.As(&options5) == S_OK) + { + options5->put_EnableTrackingPrevention(TRUE); + } + return options; +} + + + +static void InitWebView2(HWND hWnd) +{ + ComPtr options = setupEnvironment(); + std::wstring exeDir = GetExeDirectory(); + std::wstring browserDir = exeDir + L"\\portable_config" + L"\\EdgeWebView"; + + const wchar_t* browserExecutableFolder = nullptr; + if (DirectoryExists(browserDir)) { + browserExecutableFolder = browserDir.c_str(); + std::wcout << L"[WEBVIEW]: Using Found Browser directory: " << browserDir << std::endl; + } else { + std::wcout << L"[WEBVIEW]: Browser directory does not exist, using default." << std::endl; + } + + HRESULT hr = CreateCoreWebView2EnvironmentWithOptions( + browserExecutableFolder,nullptr, options.Get(), + Callback( + [hWnd](HRESULT res, ICoreWebView2Environment* env)->HRESULT + { + if(!env)return E_FAIL; + env->CreateCoreWebView2Controller( + hWnd, + Callback( + [hWnd](HRESULT result, ICoreWebView2Controller* rawController)->HRESULT + { + if (FAILED(result) || !rawController) return E_FAIL; + + wil::com_ptr m_webviewController = rawController; + if (!m_webviewController) return E_FAIL; + + g_webviewController = m_webviewController.try_query(); + if (!g_webviewController) return E_FAIL; + + wil::com_ptr coreWebView; + g_webviewController->get_CoreWebView2(&coreWebView); + g_webview = coreWebView.try_query(); + if (!g_webview) return E_FAIL; + + wil::com_ptr webView2Profile; + g_webview->get_Profile(&webView2Profile); + g_webviewProfile = webView2Profile.try_query(); + if (!g_webviewProfile) return E_FAIL; + + wil::com_ptr webView2Settings; + g_webview->get_Settings(&webView2Settings); + auto settings = webView2Settings.try_query(); + if (!settings) return E_FAIL; + + + // Setup General Settings + #ifndef DEBUG_BUILD + settings->put_AreDevToolsEnabled(FALSE); + #endif + settings->put_IsStatusBarEnabled(FALSE); + std::wstring customUserAgent = L"StremioShell/" + Utf8ToWstring(APP_VERSION); + settings->put_UserAgent(customUserAgent.c_str()); + if (!g_allowZoom) { + settings->put_IsZoomControlEnabled(FALSE); + settings->put_IsPinchZoomEnabled(FALSE); + } + + COREWEBVIEW2_COLOR col={0,0,0,0}; + g_webviewController->put_DefaultBackgroundColor(col); + + BOOL allowExternalDrop; + g_webviewController->get_AllowExternalDrop(&allowExternalDrop); + + g_webviewController->put_AllowExternalDrop(TRUE); + + RECT rc;GetClientRect(hWnd,&rc); + g_webviewController->put_Bounds(rc); + + g_webview->AddScriptToExecuteOnDocumentCreated(INIT_SHELL_SCRIPT,nullptr); + + SetupExtensions(); + SetupWebMessageHandler(); + + g_webview->Navigate(g_webuiUrl.c_str()); + + return S_OK; + } + ).Get() + ); + + return S_OK; + } + ).Get() + ); + + if (FAILED(hr)) { + std::wstring error = L"[WEBVIEW]: Failed to create Web View, make sure WebView2 runtime is installed or provide a portable WebView2 runtime exe in portable_config/EdgeWebView - Error: " + HResultToErrorMessage(hr); + std::cout << WStringToUtf8(error) << std::endl; + AppendToCrashLog(error); + // Show error in a message box + MessageBoxW( + nullptr, + error.c_str(), + L"WebView2 Initialization Error", + MB_ICONERROR | MB_OK + ); + PostQuitMessage(1); + exit(1); + } +} + + +// ----------------------------------------------------------------------------- +// Node server +// ----------------------------------------------------------------------------- +void StopNodeServer() +{ + if(g_nodeRunning){ + g_nodeRunning=false; + if(g_nodeProcess){ + TerminateProcess(g_nodeProcess,0); + WaitForSingleObject(g_nodeProcess,INFINITE); + CloseHandle(g_nodeProcess);g_nodeProcess=nullptr; + } + if(g_nodeThread.joinable())g_nodeThread.join(); + if(g_nodeOutPipe){CloseHandle(g_nodeOutPipe);g_nodeOutPipe=nullptr;} + if(g_nodeInPipe){CloseHandle(g_nodeInPipe);g_nodeInPipe=nullptr;} + std::cout<<"Node server stopped.\n"; + } +} + +static void NodeOutputThreadProc() +{ + char buf[1024]; + DWORD readSz=0; + while(g_nodeRunning){ + BOOL ok=ReadFile(g_nodeOutPipe,buf,sizeof(buf)-1,&readSz,nullptr); + if(!ok||readSz==0) break; + buf[readSz]='\0'; + std::cout<<"[node] "<(userp); + s->append(reinterpret_cast(contents), size * nmemb); + return size * nmemb; +} + +// Download file content as string using CURL +static bool DownloadString(const std::string& url, std::string& outData) { + CURL* curl = curl_easy_init(); + if (!curl) return false; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &outData); + CURLcode res = curl_easy_perform(curl); + curl_easy_cleanup(curl); + return res == CURLE_OK; +} + +// Download file and save to path +static bool DownloadFile(const std::string& url, const std::filesystem::path& dest) { + CURL* curl = curl_easy_init(); + if (!curl) return false; + + FILE* fp = _wfopen(dest.c_str(), L"wb"); + if(!fp) { + curl_easy_cleanup(curl); + return false; + } + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); // Follow redirects + curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); + + CURLcode res = curl_easy_perform(curl); + + fclose(fp); + curl_easy_cleanup(curl); + + return res == CURLE_OK; +} + +// Compute SHA256 checksum of a file +static std::string FileChecksum(const std::filesystem::path& filepath) { + std::ifstream file(filepath, std::ios::binary); + if (!file) return ""; + SHA256_CTX ctx; + SHA256_Init(&ctx); + char buf[4096]; + while(file.read(buf, sizeof(buf))) { + SHA256_Update(&ctx, buf, file.gcount()); + } + if(file.gcount() > 0) { + SHA256_Update(&ctx, buf, file.gcount()); + } + unsigned char hash[SHA256_DIGEST_LENGTH]; + SHA256_Final(hash, &ctx); + std::ostringstream oss; + for (int i = 0; i < SHA256_DIGEST_LENGTH; ++i) + oss << std::hex << std::setw(2) << std::setfill('0') << (int)hash[i]; + return oss.str(); +} + +// Verify signature +static bool VerifySignature(const std::string& data, const std::string& signatureBase64) { + // Load public key from embedded PEM + BIO* bio = BIO_new_mem_buf(public_key_pem, -1); + EVP_PKEY* pubKey = PEM_read_bio_PUBKEY(bio, nullptr, nullptr, nullptr); + BIO_free(bio); + if(!pubKey) return false; + + // Remove whitespace from the Base64 signature + std::string cleanedSig; + for(char c : signatureBase64) { + if(!isspace(static_cast(c))) { + cleanedSig.push_back(c); + } + } + + // Base64 decode signature + BIO* b64 = BIO_new(BIO_f_base64()); + // Configure decoder to not expect newlines + BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); + BIO* bmem = BIO_new_mem_buf(cleanedSig.data(), static_cast(cleanedSig.size())); + bmem = BIO_push(b64, bmem); + + std::vector signature(512); + int sig_len = BIO_read(bmem, signature.data(), static_cast(signature.size())); + BIO_free_all(bmem); + + if(sig_len <= 0) { + EVP_PKEY_free(pubKey); + return false; + } + + EVP_MD_CTX* ctx = EVP_MD_CTX_new(); + EVP_PKEY_CTX* pctx = nullptr; + bool result = false; + if(EVP_DigestVerifyInit(ctx, &pctx, EVP_sha256(), NULL, pubKey) == 1) { + if(EVP_DigestVerifyUpdate(ctx, data.data(), data.size()) == 1) { + result = EVP_DigestVerifyFinal(ctx, signature.data(), sig_len) == 1; + } + } + EVP_MD_CTX_free(ctx); + EVP_PKEY_free(pubKey); + return result; +} + + +// ----------------------------------------------------------------------------- +// Auto Updater +// ----------------------------------------------------------------------------- +static void RunAutoUpdaterOnce() { + g_updaterRunning = true; + std::cout<<"Checking for Updates.\n"; + // Endpoint URLs + const std::string versionUrl = g_updateUrl; + + std::string versionContent; + if(!DownloadString(versionUrl, versionContent)) { + AppendToCrashLog(L"[UPDATER]: Failed to download version.json"); + return; + } + + // Parse version.json + json versionJson; + try { versionJson = json::parse(versionContent); } catch(...) { return; } + + std::string versionDescUrl = versionJson["versionDesc"].get(); + std::string signatureBase64 = versionJson["signature"].get(); + + // Download version-details.json + std::string detailsContent; + if(!DownloadString(versionDescUrl, detailsContent)) { + AppendToCrashLog(L"[UPDATER]:Failed to download version details"); + return; + } + + // Verify signature + if(!VerifySignature(detailsContent, signatureBase64)) { + AppendToCrashLog(L"[UPDATER]:Signature verification failed"); + return; + } + + // Parse version-details.json + json detailsJson; + try { detailsJson = json::parse(detailsContent); } catch(...) { return; } + + // Compare shellVersion + std::string remoteShellVersion = detailsJson["shellVersion"].get(); + bool needsFullUpdate = (remoteShellVersion != APP_VERSION); + + auto files = detailsJson["files"]; + std::vector partialUpdateKeys = { "server.js" }; + + std::wstring exeDir = GetExeDirectory(); + std::filesystem::path tempDir = std::filesystem::temp_directory_path() / L"stremio_updater"; + std::filesystem::create_directories(tempDir); + + + // Handle full update scenario + if(needsFullUpdate || g_autoupdaterForceFull) { + bool allDownloadsSuccessful = true; + + std::string key = "windows"; + SYSTEM_INFO systemInfo; + GetNativeSystemInfo(&systemInfo); + + if (systemInfo.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_AMD64) { + key = "windows-x64"; + } else if (systemInfo.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_INTEL) { + key = "windows-x86"; + } else { + // Log error if architecture cannot be determined + std::cerr << "Error: Unsupported processor architecture detected." << std::endl; + AppendToCrashLog(L"[UPDATER]: Error: Unsupported processor architecture detected."); + return; + } + + std::cout<<"Processing full update for: " << key << "\n"; + + if(files.contains(key) && files[key].contains("url") && files[key].contains("checksum")) { + std::string url = files[key]["url"].get(); + std::string expectedChecksum = files[key]["checksum"].get(); + + // Determine filename from URL + std::string filename = url.substr(url.find_last_of('/') + 1); + std::filesystem::path installerPath = tempDir / std::wstring(filename.begin(), filename.end()); + + std::cout<<"Downloading full update for: " << key << "\n"; + // Check if file already exists with correct checksum + if(std::filesystem::exists(installerPath)) { + if(FileChecksum(installerPath) != expectedChecksum) { + // File exists but checksum mismatches (possibly incomplete or corrupted) + // Remove the faulty file before re-downloading + std::filesystem::remove(installerPath); + + // Attempt re-download after removal + if(!DownloadFile(url, installerPath)) { + AppendToCrashLog(L"[UPDATER]: Failed to re-download installer"); + allDownloadsSuccessful = false; + } + } + } else { + // File doesn't exist; download + if(!DownloadFile(url, installerPath)) { + AppendToCrashLog(L"[UPDATER]: Failed to download installer"); + allDownloadsSuccessful = false; + } + } + + + if(allDownloadsSuccessful) { + g_installerPath = installerPath; + } + } else { + allDownloadsSuccessful = false; + } + + if(allDownloadsSuccessful) { + std::cout<<"Full update needed!\n"; + std::cout<(); + std::string expectedChecksum = files[key]["checksum"].get(); + + // Target path: application's root directory with the same filename as key + std::filesystem::path localFilePath = std::filesystem::path(exeDir) / std::wstring(key.begin(), key.end()); + + std::cout<<"Processing partial update for: " << key << "\n"; + + // Compare checksums + if(std::filesystem::exists(localFilePath)) { + if(FileChecksum(localFilePath) == expectedChecksum) { + std::cout<<"No update needed for " << key << "\n"; + continue; + } + } + + // Attempt to download the file + if(!DownloadFile(url, localFilePath)) { + AppendToCrashLog((L"[UPDATER]: Failed to download " + std::wstring(key.begin(), key.end())).c_str()); + } else { + std::cout<<"Downloaded " << key << " successfully.\n"; + // Perform update actions based on the file key + if(key == "server.js") { + StopNodeServer(); + StartNodeServer(); + } + } + } + } + } + +} + +static void RunInstallerAndExit() { + if(g_installerPath.empty()) { + AppendToCrashLog(L"[UPDATER]: Installer path not set."); + return; + } + + // Use ShellExecute to run the installer + HINSTANCE result = ShellExecuteW( + nullptr, + L"open", + g_installerPath.c_str(), + nullptr, + nullptr, + SW_HIDE + ); + + // Check if ShellExecute failed + if ((INT_PTR)result <= 32) { + AppendToCrashLog(L"[UPDATER]: Failed to start installer."); + } + + // Clean up and exit the application gracefully + PostQuitMessage(0); + exit(0); +} + + +// ----------------------------------------------------------------------------- +// WndProc +// ----------------------------------------------------------------------------- +LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + + switch(message) + { + case WM_CREATE: { + HICON hIconBig = LoadIcon(g_hInst, MAKEINTRESOURCE(IDR_MAINFRAME)); + HICON hIconSmall = LoadIcon(g_hInst, MAKEINTRESOURCE(IDR_MAINFRAME)); + + SendMessage(hWnd, WM_SETICON, ICON_BIG, (LPARAM)hIconBig); + SendMessage(hWnd, WM_SETICON, ICON_SMALL, (LPARAM)hIconSmall); + + CreateTrayIcon(hWnd); + UpdateTheme(g_hWnd); + break; + } + + case WM_SETTINGCHANGE: + UpdateTheme(g_hWnd); + break; + + case WM_TRAYICON: + if(LOWORD(lParam)==WM_RBUTTONUP) ShowTrayMenu(hWnd); + if (lParam == WM_LBUTTONDBLCLK) { + ShowWindow(hWnd, SW_RESTORE); + SetForegroundWindow(hWnd); + } + break; + + case WM_COMMAND: + { + switch(LOWORD(wParam)) + { + case ID_TRAY_SHOWWINDOW: + g_showWindow=!g_showWindow; + ShowWindow(hWnd,g_showWindow?SW_SHOW:SW_HIDE); + break; + case ID_TRAY_ALWAYSONTOP: + g_alwaysOnTop=!g_alwaysOnTop; + SetWindowPos(hWnd, + g_alwaysOnTop?HWND_TOPMOST:HWND_NOTOPMOST, + 0,0,0,0, + SWP_NOMOVE|SWP_NOSIZE); + break; + case ID_TRAY_CLOSE_ON_EXIT: + g_closeOnExit = !g_closeOnExit; + SaveSettings(); + break; + case ID_TRAY_USE_DARK_THEME: + g_useDarkTheme = !g_useDarkTheme; + SaveSettings(); + UpdateTheme(g_hWnd); + break; + case ID_TRAY_PICTURE_IN_PICTURE: + TogglePictureInPicture(hWnd, !g_isPipMode); + break; + case ID_TRAY_PAUSE_FOCUS_LOST: + g_pauseOnLostFocus = !g_pauseOnLostFocus; + SaveSettings(); + break; + case ID_TRAY_PAUSE_MINIMIZED: + g_pauseOnMinimize = !g_pauseOnMinimize; + SaveSettings(); + break; + case ID_TRAY_QUIT: + if(g_mpv) mpv_command_string(g_mpv,"quit"); + DestroyWindow(hWnd); + break; + } + } + break; + + case WM_COPYDATA: + { + PCOPYDATASTRUCT pcds = (PCOPYDATASTRUCT)lParam; + if (pcds && pcds->dwData == 1 && pcds->lpData) { + // Assuming data is a wide string containing the URL or file path + std::wstring receivedUrl((wchar_t*)pcds->lpData, pcds->cbData / sizeof(wchar_t)); + std::wcout << L"Received URL in main instance: " << receivedUrl << std::endl; + + // Check if received URL is a file and exists + if (FileExists(receivedUrl)) { + // Extract file extension + size_t dotPos = receivedUrl.find_last_of(L"."); + std::wstring extension = (dotPos != std::wstring::npos) ? receivedUrl.substr(dotPos) : L""; + + if (extension == L".torrent") { + // Handle .torrent files + std::string utf8FilePath = WStringToUtf8(receivedUrl); + + std::ifstream ifs(utf8FilePath, std::ios::binary); + if (!ifs) { + std::cerr << "Error: Could not open torrent file.\n"; + break; + } + std::vector fileBuffer( + (std::istreambuf_iterator(ifs)), + (std::istreambuf_iterator()) + ); + + json j; + j["type"] = "OpenTorrent"; + j["data"] = fileBuffer; + SendToJS(j); + } else { + // Handle other media files + std::string utf8FilePath = WStringToUtf8(receivedUrl); + json j; + j["type"] = "OpenFile"; + j["path"] = utf8FilePath; + SendToJS(j); + } + } else if (receivedUrl.rfind(L"stremio://", 0) == 0) { + // Handle stremio:// protocol + std::string utf8Url = WStringToUtf8(receivedUrl); + json j; + j["type"] = "AddonInstall"; + j["path"] = utf8Url; + SendToJS(j); + } else if (receivedUrl.rfind(L"magnet:", 0) == 0) { + std::string utf8Url = WStringToUtf8(receivedUrl); + json j; + j["type"] = "OpenTorrent"; + j["magnet"] = utf8Url; + SendToJS(j); + } else { + std::wcout << L"Received URL is neither a valid file nor a stremio:// protocol." << std::endl; + } + } + return 0; + } + + case WM_CLOSE: + if (g_closeOnExit) { + DestroyWindow(hWnd); + } else { + ShowWindow(hWnd, SW_HIDE); + pauseMPV(g_pauseOnMinimize); + g_showWindow = false; + } + return 0; + + case WM_ACTIVATE: + if (LOWORD(wParam) == WA_INACTIVE) { + pauseMPV(g_pauseOnLostFocus); + } + break; + + case WM_SIZE: + if (wParam == SIZE_MINIMIZED) { + pauseMPV(g_pauseOnMinimize); + } + if(g_webviewController){ + RECT rc;GetClientRect(hWnd,&rc); + g_webviewController->put_Bounds(rc); + } + if (g_hSplash) + { + int newWidth = LOWORD(lParam); + int newHeight = HIWORD(lParam); + SetWindowPos(g_hSplash, nullptr, 0, 0, newWidth, newHeight, SWP_NOZORDER); + } + break; + + case WM_MPV_WAKEUP: + HandleMpvEvents(); + break; + + case WM_DESTROY: + std::cout<<"WM_DESTROY => stopping mpv + node + tray.\n"; + // release mutex + if(g_hMutex){CloseHandle(g_hMutex);g_hMutex=nullptr;} + PostQuitMessage(0); + break; + + default: + return DefWindowProc(hWnd,message,wParam,lParam); + } + return 0; +} + +void ParseCommandLineArgs(int argc, char* argv[]) { + const std::string webuiPrefix = "--webui-url="; + const std::string autoupdaterPrefix = "--autoupdater-endpoint="; + const std::string streamingServerFlag = "--streaming-server-disabled"; + const std::string autoupdaterForceFlag = "--autoupdater-force-full"; + + for (int i = 1; i < argc; ++i) { + std::string arg = argv[i]; + + if (arg.find(webuiPrefix) == 0) { + std::string urlPart = arg.substr(webuiPrefix.length()); + g_webuiUrl = Utf8ToWstring(urlPart); + std::cout<<"g_webuiUrl="< +#include "src/resource.h" + +#define IDR_MAINFRAME 101 + +IDR_MAINFRAME ICON "images/stremio.ico" +IDR_SPLASH_PNG RCDATA "images/stremio.png" diff --git a/utils/chocolatey/stremio.nuspec b/utils/chocolatey/stremio.nuspec new file mode 100644 index 0000000..5209e45 --- /dev/null +++ b/utils/chocolatey/stremio.nuspec @@ -0,0 +1,17 @@ + + + + stremio-desktop-v5 + 5.0.7 + Stremio Desktop v5 + Zaarrg + Zaarrg + https://github.com/Zaarrg/stremio-desktop-v5 + Stremio Desktop v5 Community + stremio video streaming media + https://github.com/Zaarrg/stremio-desktop-v5/blob/master/LICENSE + false + + + + \ No newline at end of file diff --git a/utils/chocolatey/tools/chocolateyUninstall.ps1 b/utils/chocolatey/tools/chocolateyUninstall.ps1 new file mode 100644 index 0000000..3c5f3ed --- /dev/null +++ b/utils/chocolatey/tools/chocolateyUninstall.ps1 @@ -0,0 +1,6 @@ +$packageName = 'stremio-desktop-v5' +$uninstallPath = "$env:LOCALAPPDATA\Programs\LNV\Stremio-5\Uninstall.exe" + +If (Test-Path $uninstallPath) { + Start-Process -FilePath $uninstallPath -ArgumentList '/S' -Wait +} diff --git a/utils/chocolatey/tools/chocolateyinstall.ps1 b/utils/chocolatey/tools/chocolateyinstall.ps1 new file mode 100644 index 0000000..2a1f3b6 --- /dev/null +++ b/utils/chocolatey/tools/chocolateyinstall.ps1 @@ -0,0 +1,16 @@ +$packageName = 'stremio-desktop-v5' +$toolsDir = Split-Path $MyInvocation.MyCommand.Definition +$packageArgs = @{ + packageName = $packageName + fileType = 'exe' + silentArgs = '/S' + validExitCodes= @(0) +} + +if ([Environment]::Is64BitOperatingSystem) { + $packageArgs['url'] = 'https://github.com/Zaarrg/stremio-desktop-v5/releases/download/5.0.0-beta.7/Stremio.5.0.7-x64.exe''' +} else { + $packageArgs['url'] = 'https://github.com/Zaarrg/stremio-desktop-v5/releases/download/5.0.0-beta.7/Stremio.5.0.7-x86.exe''' +} + +Install-ChocolateyPackage @packageArgs diff --git a/utils/mpv/README.md b/utils/mpv/README.md new file mode 100644 index 0000000..e6561b6 --- /dev/null +++ b/utils/mpv/README.md @@ -0,0 +1,21 @@ +## 🎬 Stremio Ready-to-Go Portable Configs for MPV + +### 🎥 Anime4K +- ✅ **No Special Configuration Needed** +- 📦 Drag and drop the contents of ``anime4k-High-end.zip`` into the ``portable_config`` folder located at: `%localAppData%\Programs\LNV\Stremio-5\` + + + +### 📸 ThumbFast +- ✅ **No Special Configuration Needed** +- 📦 Drag and drop the contents of ``thumbfast.7z`` into the ``portable_config`` folder located at: ``%localAppData%\Programs\LNV\Stremio-5\`` +- ⚙️ **Configuration Tip:** If you change the ``max-height`` in ``thumbfast.conf``, ensure you also update it in ``stremio-settings.ini``. Set it to `0` to disable ThumbFast handling. + + +### 🎨 AnimeJaNai +- ✅ **Optimized for Stremio** +- 📥 Download the custom build of ``stremio-animejanai`` from the [Releases Tab](https://github.com/Zaarrg/stremio-desktop-v5/releases) + - 🛠️ **Changes Made**: + - ❌ Removed `mpvnet.exe` since Stremio serves as the player. + - 🔧 Adjusted ``mpv.conf`` to work seamlessly with Stremio. + - 🔧 Updated ``input.conf`` for compatibility with Stremio. \ No newline at end of file diff --git a/utils/mpv/anime4k/anime4k-High-end.zip b/utils/mpv/anime4k/anime4k-High-end.zip new file mode 100644 index 0000000..699c367 Binary files /dev/null and b/utils/mpv/anime4k/anime4k-High-end.zip differ diff --git a/utils/mpv/stremio-animejanai/Download from Release Tab b/utils/mpv/stremio-animejanai/Download from Release Tab new file mode 100644 index 0000000..e69de29 diff --git a/utils/mpv/thumbfast/thumbfast.7z b/utils/mpv/thumbfast/thumbfast.7z new file mode 100644 index 0000000..3c8f0d2 Binary files /dev/null and b/utils/mpv/thumbfast/thumbfast.7z differ diff --git a/utils/scoop/stremio-desktop-v5.json b/utils/scoop/stremio-desktop-v5.json new file mode 100644 index 0000000..dc3c68c --- /dev/null +++ b/utils/scoop/stremio-desktop-v5.json @@ -0,0 +1,59 @@ +{ + "version": "5.0.7", + "description": "Stremio Desktop v5 Community", + "homepage": "https://github.com/Zaarrg/stremio-desktop-v5", + "license": "GPL-3.0", + "architecture": { + "64bit": { + "url": "https://github.com/Zaarrg/stremio-desktop-v5/releases/download/5.0.0-beta.7/Stremio.5.0.7-x64.exe", + "hash": "a7b71844a12f23272f8a5658fefd0d94bdf9f7e3c9a820e7d1a07436b8927bdb", + "installer": { + "args": [ + "/S" + ] + }, + "uninstaller": { + "file": "%LOCALAPPDATA%\\Programs\\LNV\\Stremio-5\\Uninstall.exe", + "args": [ + "/S" + ] + } + }, + "32bit": { + "url": "https://github.com/Zaarrg/stremio-desktop-v5/releases/download/5.0.0-beta.7/Stremio.5.0.7-x86.exe", + "hash": "65bf17a67c22d67bf602a8023ac94ae63f8a685f32e63d1d28d3a6b0cb0932ef", + "installer": { + "args": [ + "/S" + ] + }, + "uninstaller": { + "file": "%LOCALAPPDATA%\\Programs\\LNV\\Stremio-5\\Uninstall.exe", + "args": [ + "/S" + ] + } + } + }, + "checkver": { + "github": "Zaarrg/stremio-desktop-v5", + "regex": "Stremio\\.([\\d.]+)-x64\\.exe" + }, + "autoupdate": { + "architecture": { + "64bit": { + "url": "https://github.com/Zaarrg/stremio-desktop-v5/releases/download/$version/Stremio.$version-x64.exe", + "hash": { + "url": "$url.sha256" + } + }, + "32bit": { + "url": "https://github.com/Zaarrg/stremio-desktop-v5/releases/download/$version/Stremio.$version-x86.exe", + "hash": { + "url": "$url.sha256" + } + } + } + }, + "notes": "Stremio Desktop v5 using silent install via /S. Built-in auto-updater may override Scoop-managed updates." +} \ No newline at end of file diff --git a/utils/stremio/stremio-settings.ini b/utils/stremio/stremio-settings.ini new file mode 100644 index 0000000..bc94657 --- /dev/null +++ b/utils/stremio/stremio-settings.ini @@ -0,0 +1,7 @@ +[General] +CloseOnExit=0 +UseDarkTheme=1 +ThumbFastHeight=0 +PauseOnMinimize=1 +PauseOnLostFocus=0 +AllowZoom=0 diff --git a/utils/windows/MicrosoftEdgeWebview2Setup.exe b/utils/windows/MicrosoftEdgeWebview2Setup.exe new file mode 100644 index 0000000..2f1217e Binary files /dev/null and b/utils/windows/MicrosoftEdgeWebview2Setup.exe differ diff --git a/utils/windows/NsProcess/Include/nsProcess.nsh b/utils/windows/NsProcess/Include/nsProcess.nsh new file mode 100644 index 0000000..9ef6098 --- /dev/null +++ b/utils/windows/NsProcess/Include/nsProcess.nsh @@ -0,0 +1,28 @@ +!define nsProcess::FindProcess `!insertmacro nsProcess::FindProcess` + +!macro nsProcess::FindProcess _FILE _ERR + nsProcess::_FindProcess /NOUNLOAD `${_FILE}` + Pop ${_ERR} +!macroend + + +!define nsProcess::KillProcess `!insertmacro nsProcess::KillProcess` + +!macro nsProcess::KillProcess _FILE _ERR + nsProcess::_KillProcess /NOUNLOAD `${_FILE}` + Pop ${_ERR} +!macroend + +!define nsProcess::CloseProcess `!insertmacro nsProcess::CloseProcess` + +!macro nsProcess::CloseProcess _FILE _ERR + nsProcess::_CloseProcess /NOUNLOAD `${_FILE}` + Pop ${_ERR} +!macroend + + +!define nsProcess::Unload `!insertmacro nsProcess::Unload` + +!macro nsProcess::Unload + nsProcess::_Unload +!macroend diff --git a/utils/windows/NsProcess/Plugin/x86-ansi/nsProcess.dll b/utils/windows/NsProcess/Plugin/x86-ansi/nsProcess.dll new file mode 100644 index 0000000..4ce0121 Binary files /dev/null and b/utils/windows/NsProcess/Plugin/x86-ansi/nsProcess.dll differ diff --git a/utils/windows/NsProcess/Plugin/x86-unicode/nsProcess.dll b/utils/windows/NsProcess/Plugin/x86-unicode/nsProcess.dll new file mode 100644 index 0000000..2478624 Binary files /dev/null and b/utils/windows/NsProcess/Plugin/x86-unicode/nsProcess.dll differ diff --git a/utils/windows/ffmpeg/avcodec-58.dll b/utils/windows/ffmpeg/avcodec-58.dll new file mode 100644 index 0000000..ec783a5 Binary files /dev/null and b/utils/windows/ffmpeg/avcodec-58.dll differ diff --git a/utils/windows/ffmpeg/avdevice-58.dll b/utils/windows/ffmpeg/avdevice-58.dll new file mode 100644 index 0000000..6fa85d4 Binary files /dev/null and b/utils/windows/ffmpeg/avdevice-58.dll differ diff --git a/utils/windows/ffmpeg/avfilter-7.dll b/utils/windows/ffmpeg/avfilter-7.dll new file mode 100644 index 0000000..32baf9d Binary files /dev/null and b/utils/windows/ffmpeg/avfilter-7.dll differ diff --git a/utils/windows/ffmpeg/avformat-58.dll b/utils/windows/ffmpeg/avformat-58.dll new file mode 100644 index 0000000..4ac7b34 Binary files /dev/null and b/utils/windows/ffmpeg/avformat-58.dll differ diff --git a/utils/windows/ffmpeg/avutil-56.dll b/utils/windows/ffmpeg/avutil-56.dll new file mode 100644 index 0000000..ff8d066 Binary files /dev/null and b/utils/windows/ffmpeg/avutil-56.dll differ diff --git a/utils/windows/ffmpeg/ffmpeg.exe b/utils/windows/ffmpeg/ffmpeg.exe new file mode 100644 index 0000000..d15417d Binary files /dev/null and b/utils/windows/ffmpeg/ffmpeg.exe differ diff --git a/utils/windows/ffmpeg/ffprobe.exe b/utils/windows/ffmpeg/ffprobe.exe new file mode 100644 index 0000000..2cf0e54 Binary files /dev/null and b/utils/windows/ffmpeg/ffprobe.exe differ diff --git a/utils/windows/ffmpeg/postproc-55.dll b/utils/windows/ffmpeg/postproc-55.dll new file mode 100644 index 0000000..1355a2b Binary files /dev/null and b/utils/windows/ffmpeg/postproc-55.dll differ diff --git a/utils/windows/ffmpeg/swresample-3.dll b/utils/windows/ffmpeg/swresample-3.dll new file mode 100644 index 0000000..8a7d66b Binary files /dev/null and b/utils/windows/ffmpeg/swresample-3.dll differ diff --git a/utils/windows/ffmpeg/swscale-5.dll b/utils/windows/ffmpeg/swscale-5.dll new file mode 100644 index 0000000..f386223 Binary files /dev/null and b/utils/windows/ffmpeg/swscale-5.dll differ diff --git a/utils/windows/installer/fileassoc.nsh b/utils/windows/installer/fileassoc.nsh new file mode 100644 index 0000000..42597bb --- /dev/null +++ b/utils/windows/installer/fileassoc.nsh @@ -0,0 +1,119 @@ +; fileassoc.nsh +; File association helper macros +; Written by Saivert +; +; Features automatic backup system and UPDATEFILEASSOC macro for +; shell change notification. +; +; |> How to use <| +; To associate a file with an application so you can double-click it in explorer, use +; the APP_ASSOCIATE macro like this: +; +; Example: +; !insertmacro APP_ASSOCIATE "txt" "myapp.textfile" "Description of txt files" \ +; "$INSTDIR\myapp.exe,0" "Open with myapp" "$INSTDIR\myapp.exe $\"%1$\"" +; +; Never insert the APP_ASSOCIATE macro multiple times, it is only ment +; to associate an application with a single file and using the +; the "open" verb as default. To add more verbs (actions) to a file +; use the APP_ASSOCIATE_ADDVERB macro. +; +; Example: +; !insertmacro APP_ASSOCIATE_ADDVERB "myapp.textfile" "edit" "Edit with myapp" \ +; "$INSTDIR\myapp.exe /edit $\"%1$\"" +; +; To have access to more options when registering the file association use the +; APP_ASSOCIATE_EX macro. Here you can specify the verb and what verb is to be the +; standard action (default verb). +; +; And finally: To remove the association from the registry use the APP_UNASSOCIATE +; macro. Here is another example just to wrap it up: +; !insertmacro APP_UNASSOCIATE "txt" "myapp.textfile" +; +; |> Note <| +; When defining your file class string always use the short form of your application title +; then a period (dot) and the type of file. This keeps the file class sort of unique. +; Examples: +; Winamp.Playlist +; NSIS.Script +; Photoshop.JPEGFile +; +; |> Tech info <| +; The registry key layout for a file association is: +; HKEY_CLASSES_ROOT +; = <"description"> +; shell +; = <"menu-item text"> +; command = <"command string"> +; + +!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND + ; Backup the previously associated file class + ReadRegStr $R0 HKCU "Software\Classes\.${EXT}" "" + WriteRegStr HKCU "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0" + + WriteRegStr HKCU "Software\Classes\.${EXT}" "" "${FILECLASS}" + + WriteRegStr HKCU "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}` + WriteRegStr HKCU "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}` + WriteRegStr HKCU "Software\Classes\${FILECLASS}\shell" "" "open" + WriteRegStr HKCU "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}` + WriteRegStr HKCU "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}` +!macroend + +!macro APP_ASSOCIATE_EX EXT FILECLASS DESCRIPTION ICON VERB DEFAULTVERB SHELLNEW COMMANDTEXT COMMAND + ; Backup the previously associated file class + ReadRegStr $R0 HKCU "Software\Classes\.${EXT}" "" + WriteRegStr HKCU "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0" + + WriteRegStr HKCU "Software\Classes\.${EXT}" "" "${FILECLASS}" + StrCmp "${SHELLNEW}" "0" +2 + WriteRegStr HKCU "Software\Classes\.${EXT}\ShellNew" "NullFile" "" + + WriteRegStr HKCU "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}` + WriteRegStr HKCU "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}` + WriteRegStr HKCU "Software\Classes\${FILECLASS}\shell" "" `${DEFAULTVERB}` + WriteRegStr HKCU "Software\Classes\${FILECLASS}\shell\${VERB}" "" `${COMMANDTEXT}` + WriteRegStr HKCU "Software\Classes\${FILECLASS}\shell\${VERB}\command" "" `${COMMAND}` +!macroend + +!macro APP_ASSOCIATE_ADDVERB FILECLASS VERB COMMANDTEXT COMMAND + WriteRegStr HKCU "Software\Classes\${FILECLASS}\shell\${VERB}" "" `${COMMANDTEXT}` + WriteRegStr HKCU "Software\Classes\${FILECLASS}\shell\${VERB}\command" "" `${COMMAND}` +!macroend + +!macro APP_ASSOCIATE_REMOVEVERB FILECLASS VERB + DeleteRegKey HKCU `Software\Classes\${FILECLASS}\shell\${VERB}` +!macroend + + +!macro APP_UNASSOCIATE EXT FILECLASS + ; Backup the previously associated file class + ReadRegStr $R0 HKCU "Software\Classes\.${EXT}" `${FILECLASS}_backup` + WriteRegStr HKCU "Software\Classes\.${EXT}" "" "$R0" + + DeleteRegKey HKCU `Software\Classes\${FILECLASS}` +!macroend + +!macro APP_ASSOCIATE_GETFILECLASS OUTPUT EXT + ReadRegStr ${OUTPUT} HKCU ".${EXT}" "" +!macroend + + +; !defines for use with SHChangeNotify +!ifdef SHCNE_ASSOCCHANGED +!undef SHCNE_ASSOCCHANGED +!endif +!define SHCNE_ASSOCCHANGED 0x08000000 +!ifdef SHCNF_FLUSH +!undef SHCNF_FLUSH +!endif +!define SHCNF_FLUSH 0x1000 + +!macro UPDATEFILEASSOC +; Using the system.dll plugin to call the SHChangeNotify Win32 API function so we +; can update the shell. + System::Call "shell32::SHChangeNotify(i,i,i,i) (${SHCNE_ASSOCCHANGED}, ${SHCNF_FLUSH}, 0, 0)" +!macroend + +;EOF \ No newline at end of file diff --git a/utils/windows/installer/windows-installer-header.bmp b/utils/windows/installer/windows-installer-header.bmp new file mode 100644 index 0000000..02b3b9f Binary files /dev/null and b/utils/windows/installer/windows-installer-header.bmp differ diff --git a/utils/windows/installer/windows-installer.bmp b/utils/windows/installer/windows-installer.bmp new file mode 100644 index 0000000..c4aec08 Binary files /dev/null and b/utils/windows/installer/windows-installer.bmp differ diff --git a/utils/windows/installer/windows-installer.nsi b/utils/windows/installer/windows-installer.nsi new file mode 100644 index 0000000..549f355 --- /dev/null +++ b/utils/windows/installer/windows-installer.nsi @@ -0,0 +1,611 @@ + +;Stremio +;Installer Source for NSIS 3.0 or higher + +Unicode True + +#Tells the compiler whether or not to do datablock optimizations. +SetDatablockOptimize on + +;Include Modern UI +!include "MUI2.nsh" +!include "FileFunc.nsh" +!include "fileassoc.nsh" +!include "nsProcess.nsh" +!include "LogicLib.nsh" +!include "x64.nsh" + +;Parse package.json + +!define APP_NAME "Stremio" +!define PRODUCT_VERSION "$%package_version%" +!define ARCH "$%arch%" +!searchparse "${PRODUCT_VERSION}" `` VERSION_MAJOR `.` VERSION_MINOR `.` VERSION_REVISION +!define APP_URL "https://www.stremio.com" +!define DATA_FOLDER "stremio" + +!define COMPANY_NAME "Smart Code Ltd" + + +; ------------------- ; +; Settings ; +; ------------------- ; +;General Settings +Name "${APP_NAME}" +Caption "${APP_NAME} ${PRODUCT_VERSION} - Installer" +BrandingText "${APP_NAME} ${PRODUCT_VERSION}" +VIAddVersionKey "ProductName" "${APP_NAME}" +VIAddVersionKey "ProductVersion" "${PRODUCT_VERSION}" +VIAddVersionKey "FileDescription" "${APP_NAME} ${PRODUCT_VERSION} Installer" +VIAddVersionKey "FileVersion" "${PRODUCT_VERSION}" +VIAddVersionKey "CompanyName" "${COMPANY_NAME}" +VIAddVersionKey "LegalCopyright" "${APP_URL}" +VIProductVersion "${PRODUCT_VERSION}.0" +OutFile "../../${APP_NAME} ${PRODUCT_VERSION}-${ARCH}.exe" +ShowInstDetails "nevershow" +ShowUninstDetails "nevershow" +CRCCheck on +;SetCompressor /SOLID lzma +;SetCompressorDictSize 4 +;SetCompressor lzma +;SetCompressorDictSize 1 +SetCompressor /SOLID lzma +SetCompressorDictSize 128 + +;Default installation folder +InstallDir "$LOCALAPPDATA\Programs\LNV\${APP_NAME}-${VERSION_MAJOR}" +InstallDirRegKey HKLM Software\SmartCode\Stremio InstallLocation + +;Request application privileges +;RequestExecutionLevel highest +RequestExecutionLevel user +;RequestExecutionLevel admin + +!define APP_LAUNCHER "Stremio.exe" +!define UNINSTALL_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}" + +; ------------------- ; +; UI Settings ; +; ------------------- ; +;Define UI settings + +;!define MUI_UI_HEADERIMAGE_RIGHT "../../../images/icon.png" +!define MUI_ICON "../../../images/stremio.ico" +!define MUI_UNICON "../../../images/stremio.ico" + +; WARNING; these bmps have to be generated in BMP3 - convert SMTH BMP3:SMTH.bmp +!define MUI_WELCOMEFINISHPAGE_BITMAP "windows-installer.bmp" +!define MUI_UNWELCOMEFINISHPAGE_BITMAP "windows-installer.bmp" +!define MUI_ABORTWARNING +!define MUI_FINISHPAGE_LINK "www.stremio.com" +!define MUI_FINISHPAGE_LINK_LOCATION "${APP_URL}" +!define MUI_FINISHPAGE_RUN "$INSTDIR\stremio.exe" + +; Hack... +!define MUI_FINISHPAGE_SHOWREADME "" +;!define MUI_FINISHPAGE_SHOWREADME_NOTCHECKED +!define MUI_FINISHPAGE_SHOWREADME_TEXT "$(desktopShortcut)" +!define MUI_FINISHPAGE_SHOWREADME_FUNCTION finishpageaction +!define MUI_FINISHPAGE_TITLE "Completing the ${APP_NAME} Setup" + +; Define header image +!define MUI_HEADERIMAGE +!define MUI_HEADERIMAGE_BITMAP "windows-installer-header.bmp" +!define MUI_HEADERIMAGE_BITMAP_NOSTRETCH +!define MUI_HEADER_TRANSPARENT_TEXT +; also consider MUI_WELCOMEFINISHPAGE_BITMAP + +; Beautiful progress bar +XPStyle off +!define MUI_INSTALLCOLORS "000000 643F9E" +!define MUI_INSTFILESPAGE_PROGRESSBAR colored + + +# Include Sections header so that we can manipulate section properties in .onInit +!include "Sections.nsh" + +;ReserveFile /plugin InstallOptions.dll + +; Pages +;!insertmacro MUI_PAGE_WELCOME +; !insertmacro MUI_PAGE_LICENSE "LICENSE.txt" +;!insertmacro MUI_PAGE_DIRECTORY + +# Perform installation (executes each enabled Section) +!insertmacro MUI_PAGE_INSTFILES +!define MUI_PAGE_CUSTOMFUNCTION_SHOW fin_pg_options +!define MUI_PAGE_CUSTOMFUNCTION_LEAVE fin_pg_leave +!insertmacro MUI_PAGE_FINISH + +; Uninstall pages +!insertmacro MUI_UNPAGE_WELCOME +!insertmacro MUI_UNPAGE_INSTFILES +!insertmacro MUI_UNPAGE_FINISH + +; Load Language Files +!insertmacro MUI_LANGUAGE "English" + +; Progress bar - part 2 +!define MUI_PAGE_CUSTOMFUNCTION_SHOW InstShow + +; ------------------- ; +; Localization ; +; ------------------- ; +LangString removeDataFolder ${LANG_ENGLISH} "Remove all data and configuration?" +LangString noRoot ${LANG_ENGLISH} "You cannot install Stremio in a directory that requires administrator permissions" +LangString desktopShortcut ${LANG_ENGLISH} "Desktop Shortcut" +LangString appIsRunning ${LANG_ENGLISH} "${APP_NAME} is running. Do you want to close it?" +LangString appIsRunningInstallError ${LANG_ENGLISH} "${APP_NAME} cannot be installed while another instance is running." +LangString appIsRunningUninstallError ${LANG_ENGLISH} "${APP_NAME} cannot be uninstalled while another instance is running." + +Var Parameters + +# Finish page custom options +Var AssociateMagnetCheckbox +Var AssociateMediaCheckbox +Var AssociateTorrentCheckbox +Var checkbox_value + +Function fin_pg_options + ${NSD_CreateCheckbox} 180 -100 100% 8u "Associate ${APP_NAME} with .torrent files" + Pop $AssociateTorrentCheckbox + SetCtlColors $AssociateTorrentCheckbox '0xFF0000' '0xFFFFFF' + ${NSD_Check} $AssociateTorrentCheckbox + + ${NSD_CreateCheckbox} 180 -80 100% 8u "Associate ${APP_NAME} with magnet links" + Pop $AssociateMagnetCheckbox + SetCtlColors $AssociateMagnetCheckbox '0xFF0000' '0xFFFFFF' + ${NSD_Check} $AssociateMagnetCheckbox + + ${NSD_CreateCheckbox} 180 -60 100% 8u "Associate ${APP_NAME} as media player" + Pop $AssociateMediaCheckbox + SetCtlColors $AssociateMediaCheckbox '0xFF0000' '0xFFFFFF' + ${NSD_Check} $AssociateMediaCheckbox +FunctionEnd + +Function fin_pg_leave + ${NSD_GetState} $AssociateTorrentCheckbox $checkbox_value + ${If} $checkbox_value == ${BST_CHECKED} + !insertmacro APP_ASSOCIATE "torrent" "stremio" "BitTorrent file" "$INSTDIR\stremio.exe,0" "Play with Stremio" "$INSTDIR\stremio.exe $\"%1$\"" + ${EndIf} + + ; Set friendly name for Stremio in "Open With" menu + WriteRegStr HKCU "Software\Classes\stremio" "FriendlyTypeName" "${APP_NAME}" + WriteRegStr HKCU "Software\Classes\stremio\shell\open" "FriendlyAppName" "${APP_NAME}" + + + + ${NSD_GetState} $AssociateMagnetCheckbox $checkbox_value + ${If} $checkbox_value == ${BST_CHECKED} + WriteRegStr HKCU "Software\Classes\magnet" "" "Magnet Protocol" + WriteRegStr HKCU "Software\Classes\magnet" "URL Protocol" "" + WriteRegStr HKCU "Software\Classes\magnet\DefaultIcon" "" "$INSTDIR\stremio.exe,0" + WriteRegStr HKCU "Software\Classes\magnet\shell\open\command" "" '"$INSTDIR\stremio.exe" "%1"' + ${EndIf} + + ${NSD_GetState} $AssociateMediaCheckbox $checkbox_value + + !macro APP_ASSOCIATE_EXTENSIONS + ; Define the list of extensions + Var /GLOBAL FileExtensions + StrCpy $FileExtensions "mp4 mkv avi mov wmv flv webm mpg mpeg 3gp m4v ts vob f4v m2ts asf divx ogv rm rmvb" + + ; Start of the loop + Var /GLOBAL CurrentExtension + LoopStart: + ; Extract the first extension from the list + StrCpy $CurrentExtension $FileExtensions 4 ; Copy up to the first space + StrCpy $FileExtensions $FileExtensions 4 - ; Remove the extracted extension from the list + ${If} $CurrentExtension != "" + ; Associate the current extension + !insertmacro APP_ASSOCIATE "$CurrentExtension" "stremio" "Media File" "$INSTDIR\stremio.exe,0" "Play with Stremio" "$INSTDIR\stremio.exe $\"%1$\"" + ; Continue the loop + Goto LoopStart + ${EndIf} + ; End of the loop + ${EndIf} + !macroend + ${If} $checkbox_value == ${BST_CHECKED} + !insertmacro APP_ASSOCIATE "mp4" "stremio" "Media File" "$INSTDIR\stremio.exe,0" "Play with Stremio" "$INSTDIR\stremio.exe $\"%1$\"" + !insertmacro APP_ASSOCIATE "mkv" "stremio" "Media File" "$INSTDIR\stremio.exe,0" "Play with Stremio" "$INSTDIR\stremio.exe $\"%1$\"" + !insertmacro APP_ASSOCIATE "avi" "stremio" "Media File" "$INSTDIR\stremio.exe,0" "Play with Stremio" "$INSTDIR\stremio.exe $\"%1$\"" + !insertmacro APP_ASSOCIATE "mov" "stremio" "Media File" "$INSTDIR\stremio.exe,0" "Play with Stremio" "$INSTDIR\stremio.exe $\"%1$\"" + !insertmacro APP_ASSOCIATE "wmv" "stremio" "Media File" "$INSTDIR\stremio.exe,0" "Play with Stremio" "$INSTDIR\stremio.exe $\"%1$\"" + !insertmacro APP_ASSOCIATE "flv" "stremio" "Media File" "$INSTDIR\stremio.exe,0" "Play with Stremio" "$INSTDIR\stremio.exe $\"%1$\"" + !insertmacro APP_ASSOCIATE "webm" "stremio" "Media File" "$INSTDIR\stremio.exe,0" "Play with Stremio" "$INSTDIR\stremio.exe $\"%1$\"" + !insertmacro APP_ASSOCIATE "mpg" "stremio" "Media File" "$INSTDIR\stremio.exe,0" "Play with Stremio" "$INSTDIR\stremio.exe $\"%1$\"" + !insertmacro APP_ASSOCIATE "mpeg" "stremio" "Media File" "$INSTDIR\stremio.exe,0" "Play with Stremio" "$INSTDIR\stremio.exe $\"%1$\"" + !insertmacro APP_ASSOCIATE "3gp" "stremio" "Media File" "$INSTDIR\stremio.exe,0" "Play with Stremio" "$INSTDIR\stremio.exe $\"%1$\"" + !insertmacro APP_ASSOCIATE "m4v" "stremio" "Media File" "$INSTDIR\stremio.exe,0" "Play with Stremio" "$INSTDIR\stremio.exe $\"%1$\"" + !insertmacro APP_ASSOCIATE "ts" "stremio" "Media File" "$INSTDIR\stremio.exe,0" "Play with Stremio" "$INSTDIR\stremio.exe $\"%1$\"" + !insertmacro APP_ASSOCIATE "vob" "stremio" "Media File" "$INSTDIR\stremio.exe,0" "Play with Stremio" "$INSTDIR\stremio.exe $\"%1$\"" + !insertmacro APP_ASSOCIATE "f4v" "stremio" "Media File" "$INSTDIR\stremio.exe,0" "Play with Stremio" "$INSTDIR\stremio.exe $\"%1$\"" + !insertmacro APP_ASSOCIATE "m2ts" "stremio" "Media File" "$INSTDIR\stremio.exe,0" "Play with Stremio" "$INSTDIR\stremio.exe $\"%1$\"" + !insertmacro APP_ASSOCIATE "asf" "stremio" "Media File" "$INSTDIR\stremio.exe,0" "Play with Stremio" "$INSTDIR\stremio.exe $\"%1$\"" + !insertmacro APP_ASSOCIATE "divx" "stremio" "Media File" "$INSTDIR\stremio.exe,0" "Play with Stremio" "$INSTDIR\stremio.exe $\"%1$\"" + !insertmacro APP_ASSOCIATE "ogv" "stremio" "Media File" "$INSTDIR\stremio.exe,0" "Play with Stremio" "$INSTDIR\stremio.exe $\"%1$\"" + !insertmacro APP_ASSOCIATE "rm" "stremio" "Media File" "$INSTDIR\stremio.exe,0" "Play with Stremio" "$INSTDIR\stremio.exe $\"%1$\"" + !insertmacro APP_ASSOCIATE "rmvb" "stremio" "Media File" "$INSTDIR\stremio.exe,0" "Play with Stremio" "$INSTDIR\stremio.exe $\"%1$\"" + ${EndIf} +FunctionEnd + + +; --------------------------------------------------- +; Removes everything from $INSTDIR except the +; "stremio.exe.WebView2" folder. +; --------------------------------------------------- +!macro RemoveAllExceptWebView2 un +Function ${un}RemoveAllExceptWebView2 + ; Hardcoded values for your scenario + StrCpy $R0 "stremio.exe.WebView2" ; Directory to exclude + StrCpy $R1 "$INSTDIR" ; Root directory to operate on + + Push $R2 + Push $R3 + Push $R4 + + ClearErrors + FindFirst $R3 $R2 "$R1\*.*" + IfErrors Exit + + Top: + ; Skip special directories "." and ".." + StrCmp $R2 "." Next + StrCmp $R2 ".." Next + ; Skip the excluded directory + StrCmp $R2 $R0 Next + + ; Build full path for the current item + StrCpy $R4 "$R1\$R2" + + ; Check if the current item is a directory + IfFileExists "$R4\*.*" isDir notDir + + notDir: + ; It's a file, so delete it + Delete "$R4" + Goto Next + + isDir: + ; It's a directory, remove it recursively + RMDir /r "$R4" + Next: + ClearErrors + FindNext $R3 $R2 + IfErrors Exit + Goto Top + + Exit: + FindClose $R3 + + Pop $R4 + Pop $R3 + Pop $R2 +FunctionEnd +!macroend + +!insertmacro RemoveAllExceptWebView2 "" +!insertmacro RemoveAllExceptWebView2 "un." + +!macro checkIfAppIsRunning AppIsRunningErrorMsg + ; Check if stremio.exe is running + ${nsProcess::FindProcess} ${APP_LAUNCHER} $R0 + + ${If} $R0 == 0 + IfSilent killapp + MessageBox MB_YESNO|MB_ICONQUESTION "$(appIsRunning)" IDYES killapp + ; Check if stremio.exe is still running. + ; No need to abort if the user manually closes Stremio and answer NO on the prompt + ${nsProcess::FindProcess} ${APP_LAUNCHER} $R0 + ${If} $R0 == 0 + ; Hide the progress bar + FindWindow $0 "#32770" "" $HWNDPARENT + GetDlgItem $1 $0 0x3ec + ShowWindow $1 ${SW_HIDE} + ; Abort install + Abort "${AppIsRunningErrorMsg}" + ${EndIf} + killapp: + ${nsProcess::CloseProcess} "${APP_LAUNCHER}" $R0 + Sleep 2000 + ${EndIf} + + ${nsProcess::Unload} +!macroend + +; ------------------- ; +; WebView Check ; +; ------------------- ; + +Function CheckWebView2 + ClearErrors + StrCpy $0 "" + + ${If} ${RunningX64} + ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 == "" + ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${EndIf} + ${Else} + ReadRegStr $0 HKLM "SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 == "" + ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${EndIf} + ${EndIf} + + StrCmp $0 "" NotInstalled 0 + StrCmp $0 "0.0.0.0" NotInstalled 0 + Goto WebViewPresent + +NotInstalled: + ; Switch details printing to text-only mode to display custom messages + SetDetailsPrint textonly + DetailPrint "WebView2 Runtime not found. Extracting setup..." + + ; Extract MicrosoftEdgeWebview2Setup.exe only when needed + File "/oname=$PLUGINSDIR\MicrosoftEdgeWebview2Setup.exe" "..\MicrosoftEdgeWebview2Setup.exe" + DetailPrint "Extracted WebView2 setup to $PLUGINSDIR." + + DetailPrint "Installing WebView2 Runtime..." + SetDetailsPrint none + ExecWait '"$PLUGINSDIR\MicrosoftEdgeWebview2Setup.exe" /silent /install' $R0 + SetDetailsPrint textonly + + ${If} $R0 != 0 + DetailPrint "Failed to install WebView2 Runtime. Error code: $R0." + MessageBox MB_OK|MB_ICONEXCLAMATION "Error installing WebView2 Runtime (error code: $R0)." + ${EndIf} + + DetailPrint "Finished installing WebView2 Continuing installation..." + ; Restore previous details printing mode (last used setting) + SetDetailsPrint lastused + +WebViewPresent: +FunctionEnd + + +; ------------------- ; +; Install code ; +; ------------------- ; +Function .onInit ; check for previous version + ReadRegStr $0 HKCU "${UNINSTALL_KEY}" "InstallString" + StrCmp $0 "" done + StrCpy $INSTDIR $0 + + ${GetParameters} $Parameters + ClearErrors + ${GetOptions} $Parameters "/addon" $R1 + + FileOpen $0 "$INSTDIR\addons.txt" w + FileWrite $0 "$R1" + FileClose $0 +done: +FunctionEnd + +Section ; App Files + !insertmacro checkIfAppIsRunning "$(appIsRunningInstallError)" + + ; Check and install WebView2 before proceeding + Call CheckWebView2 + + ; Hide details + SetDetailsPrint None + Call RemoveAllExceptWebView2 + + ;Set output path to InstallDir + SetOutPath "$INSTDIR" + + ;Add the files + File /r "..\..\..\dist\win-${ARCH}\*" + + ;Create uninstaller + WriteUninstaller "$INSTDIR\Uninstall.exe" + +SectionEnd + +; ------------------- ; +; Shortcuts ; +; ------------------- ; +Section ; Shortcuts + ; Hide details + SetDetailsPrint none + + ;Working Directory + SetOutPath "$INSTDIR" + + ;Start Menu Shortcut + RMDir /r "$SMPROGRAMS\${APP_NAME}" + CreateDirectory "$SMPROGRAMS\${APP_NAME}" + CreateShortCut "$SMPROGRAMS\${APP_NAME}\${APP_NAME}.lnk" "$INSTDIR\stremio.exe" "" "$INSTDIR\stremio.exe" "" "" "" "${APP_NAME} ${PRODUCT_VERSION}" + CreateShortCut "$SMPROGRAMS\${APP_NAME}\Uninstall ${APP_NAME}.lnk" "$INSTDIR\Uninstall.exe" "" "$INSTDIR\stremio.exe" "" "" "" "Uninstall ${APP_NAME}" + + ;Desktop Shortcut + Delete "$DESKTOP\${APP_NAME}.lnk" + + ;Add/remove programs uninstall entry + ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 + IntFmt $0 "0x%08X" $0 + WriteRegDWORD HKCU "${UNINSTALL_KEY}" "EstimatedSize" "$0" + WriteRegStr HKCU "${UNINSTALL_KEY}" "DisplayName" "${APP_NAME}" + WriteRegStr HKCU "${UNINSTALL_KEY}" "DisplayVersion" "${PRODUCT_VERSION}" + WriteRegStr HKCU "${UNINSTALL_KEY}" "DisplayIcon" "$INSTDIR\stremio.exe" + WriteRegStr HKCU "${UNINSTALL_KEY}" "Publisher" "${COMPANY_NAME}" + WriteRegStr HKCU "${UNINSTALL_KEY}" "UninstallString" "$INSTDIR\Uninstall.exe" + WriteRegStr HKCU "${UNINSTALL_KEY}" "InstallString" "$INSTDIR" + WriteRegStr HKCU "${UNINSTALL_KEY}" "URLInfoAbout" "${APP_URL}" + WriteRegStr HKCU "${UNINSTALL_KEY}" "NoModify" 1 + WriteRegStr HKCU "${UNINSTALL_KEY}" "NoRepair" 1 + + ; Register stremio:// protocol handler + WriteRegStr HKCU "Software\Classes\stremio" "" "URL:Stremio Protocol" + WriteRegStr HKCU "Software\Classes\stremio" "URL Protocol" "" + WriteRegStr HKCU "Software\Classes\stremio\DefaultIcon" "" "$INSTDIR\stremio.exe,1" + WriteRegStr HKCU "Software\Classes\stremio\shell" "" "open" + WriteRegStr HKCU "Software\Classes\stremio\shell\open\command" "" '"$INSTDIR\stremio.exe" "%1"' + + IfSilent 0 end + Call fin_pg_leave + ${GetOptions} $Parameters /nodesktopicon $R1 + IfErrors 0 end + Call finishpageaction + end: +SectionEnd + +; ------------------- ; +; Uninstaller ; +; ------------------- ; +; ------------------- ; +; Uninstaller ; +; ------------------- ; +Section "uninstall" + + ; Macro to check if application is running + !insertmacro checkIfAppIsRunning "$(appIsRunningUninstallError)" + + SetDetailsPrint none + + ; Remove shortcuts + RMDir /r "$SMPROGRAMS\${APP_NAME}" + Delete "$DESKTOP\${APP_NAME}.lnk" + + ; Remove registry entries + DeleteRegKey HKCU "${UNINSTALL_KEY}" + DeleteRegKey HKCU Software\Classes\stremio + DeleteRegKey HKCU Software\Classes\magnet + + ; Remove friendly name registry entry + DeleteRegKey HKCR Applications\stremio.exe + + !insertmacro APP_UNASSOCIATE "torrent" "stremio" + !insertmacro APP_UNASSOCIATE "mp4" "stremio" + !insertmacro APP_UNASSOCIATE "mkv" "stremio" + !insertmacro APP_UNASSOCIATE "avi" "stremio" + !insertmacro APP_UNASSOCIATE "mov" "stremio" + !insertmacro APP_UNASSOCIATE "wmv" "stremio" + !insertmacro APP_UNASSOCIATE "flv" "stremio" + !insertmacro APP_UNASSOCIATE "webm" "stremio" + !insertmacro APP_UNASSOCIATE "mpg" "stremio" + !insertmacro APP_UNASSOCIATE "mpeg" "stremio" + !insertmacro APP_UNASSOCIATE "3gp" "stremio" + !insertmacro APP_UNASSOCIATE "m4v" "stremio" + !insertmacro APP_UNASSOCIATE "ts" "stremio" + !insertmacro APP_UNASSOCIATE "vob" "stremio" + !insertmacro APP_UNASSOCIATE "f4v" "stremio" + !insertmacro APP_UNASSOCIATE "m2ts" "stremio" + !insertmacro APP_UNASSOCIATE "asf" "stremio" + !insertmacro APP_UNASSOCIATE "divx" "stremio" + !insertmacro APP_UNASSOCIATE "ogv" "stremio" + !insertmacro APP_UNASSOCIATE "rm" "stremio" + !insertmacro APP_UNASSOCIATE "rmvb" "stremio" + + + ; Prompt user to see if they want to remove data + IfSilent +3 + MessageBox MB_YESNO|MB_ICONQUESTION "$(removeDataFolder)" IDNO keepUserData + Goto removeData + ${GetParameters} $Parameters + ClearErrors + ${GetOptions} $Parameters "/keepdata" $R1 + IfErrors 0 keepUserData + + removeData: + ; User chose to remove data - remove entire install folder (including .WebView2) + RMDir /r "$INSTDIR" + Goto done + + keepUserData: + ; User chose to keep data - remove all but the WebView2 folder + Call un.RemoveAllExceptWebView2 + + done: + ; Optionally open a farewell page + IfSilent +2 + ExecShell "open" "https://github.com/Zaarrg/stremio-desktop-v5/blob/master/docs/GOODBYE.md" + +SectionEnd + +; ------------------- ; +; Check if writable ; +; ------------------- ; +Function IsWritable + + !define IsWritable `!insertmacro IsWritableCall` + + !macro IsWritableCall _PATH _RESULT + Push `${_PATH}` + Call IsWritable + Pop ${_RESULT} + !macroend + + Exch $R0 + Push $R1 + +start: + StrLen $R1 $R0 + StrCmp $R1 0 exit + ${GetFileAttributes} $R0 "DIRECTORY" $R1 + StrCmp $R1 1 direxists + ${GetParent} $R0 $R0 + Goto start + +direxists: + ${GetFileAttributes} $R0 "DIRECTORY" $R1 + StrCmp $R1 0 ok + + StrCmp $R0 $PROGRAMFILES64 notok + StrCmp $R0 $WINDIR notok + + ${GetFileAttributes} $R0 "READONLY" $R1 + + Goto exit + +notok: + StrCpy $R1 1 + Goto exit + +ok: + StrCpy $R1 0 + +exit: + Exch + Pop $R0 + Exch $R1 + +FunctionEnd + +; ------------------- ; +; Check install dir ; +; ------------------- ; +Function CloseBrowseForFolderDialog + !ifmacrodef "_P<>" ; NSIS 3+ + System::Call 'USER32::GetActiveWindow()p.r0' + ${If} $0 P<> $HwndParent + !else + System::Call 'USER32::GetActiveWindow()i.r0' + ${If} $0 <> $HwndParent + !endif + SendMessage $0 ${WM_CLOSE} 0 0 + ${EndIf} +FunctionEnd + +Function .onVerifyInstDir + + Push $R1 + ${IsWritable} $INSTDIR $R1 + IntCmp $R1 0 pathgood + Pop $R1 + Call CloseBrowseForFolderDialog + MessageBox MB_OK|MB_USERICON "$(noRoot)" /SD IDOK + Abort + +pathgood: + Pop $R1 + +FunctionEnd + +; ------------------ ; +; Desktop Shortcut ; +; ------------------ ; +Function finishpageaction + CreateShortCut "$DESKTOP\${APP_NAME}.lnk" "$INSTDIR\stremio.exe" "" "$INSTDIR\stremio.exe" "" "" "" "${APP_NAME} ${PRODUCT_VERSION}" +FunctionEnd diff --git a/utils/windows/stremio-runtime.exe b/utils/windows/stremio-runtime.exe new file mode 100644 index 0000000..2360561 Binary files /dev/null and b/utils/windows/stremio-runtime.exe differ diff --git a/version/version-details.json b/version/version-details.json new file mode 100644 index 0000000..beaa077 --- /dev/null +++ b/version/version-details.json @@ -0,0 +1,17 @@ +{ + "shellVersion": "5.0.7", + "files": { + "windows-x64": { + "url": "https://github.com/Zaarrg/stremio-desktop-v5/releases/download/5.0.0-beta.7/Stremio.5.0.7-x64.exe", + "checksum": "a7b71844a12f23272f8a5658fefd0d94bdf9f7e3c9a820e7d1a07436b8927bdb" + }, + "windows-x86": { + "url": "https://github.com/Zaarrg/stremio-desktop-v5/releases/download/5.0.0-beta.7/Stremio.5.0.7-x86.exe", + "checksum": "65bf17a67c22d67bf602a8023ac94ae63f8a685f32e63d1d28d3a6b0cb0932ef" + }, + "server.js": { + "url": "https://dl.strem.io/server/v4.20.8/desktop/server.js", + "checksum": "7113200f5775c958fd141bc502a808ab00ebfbb53799a13c3ab0aca84c5fb476" + } + } +} \ No newline at end of file diff --git a/version/version.json b/version/version.json new file mode 100644 index 0000000..f844e99 --- /dev/null +++ b/version/version.json @@ -0,0 +1,5 @@ +{ + "upToDate": false, + "versionDesc": "https://raw.githubusercontent.com/Zaarrg/stremio-desktop-v5/refs/heads/master/version/version-details.json", + "signature": "f5b4X5+QImy0SmCyU3iVGTnqX/xbg7B/d55G7YN/96dLnV0d0vmXU+OijfNA6pq8mw21uolrUQmkPnyPuASVxx4x98AyjVFQVYoulPfArR/ME1UnNDbfdyWZBKhR5EXuCeCqmIHGjFgH59kOp1hHsCaKhTqPFcwE/L9PLvGy0RxL3IhTSgNnadg6BRhf0izcOusc+xRFKIKQuAc3/eYLTGTJTYjHykAdl/ok1ufttHv6ZCEhjo2ki44VsnanSeYmXa3vlFriUsdhrschxrE4RApE/fqQl4VhZh/3PRwUNJX/E25b7Yyqxy1hLJd4mkaY0Ag98KsynQBdmJp8zWSYPA==" +} \ No newline at end of file