From f6020ba96d85472ee74c1a7c5521da16279d24f7 Mon Sep 17 00:00:00 2001 From: Chris Punches Date: Wed, 31 Dec 2025 22:25:19 -0500 Subject: [PATCH] first commit --- .idea/.gitignore | 8 + .idea/.name | 1 + .idea/Scallywag-CC.iml | 2 + .idea/editor.xml | 580 +++++++++++++++++++++++++++++++ .idea/misc.xml | 7 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + CMakeLists.txt | 76 ++++ LICENSE.md | 595 ++++++++++++++++++++++++++++++++ README.md | 47 +++ scallywag-ng.c | 130 +++++++ src/app.c | 530 ++++++++++++++++++++++++++++ src/app.h | 105 ++++++ src/config.c | 65 ++++ src/config.h | 33 ++ src/gui/callbacks.c | 95 +++++ src/gui/callbacks.h | 55 +++ src/gui/loading.c | 162 +++++++++ src/gui/loading.h | 54 +++ src/gui/logo.svg | 4 + src/gui/resources.gresource.xml | 6 + src/gui/window.c | 246 +++++++++++++ src/gui/window.h | 31 ++ src/http.c | 128 +++++++ src/http.h | 46 +++ src/scraper/proxylister.c | 169 +++++++++ src/scraper/proxylister.h | 43 +++ src/scraper/searcher.c | 348 +++++++++++++++++++ src/scraper/searcher.h | 60 ++++ src/util/log.h | 34 ++ src/util/strutil.c | 190 ++++++++++ src/util/strutil.h | 42 +++ 32 files changed, 3906 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/.name create mode 100644 .idea/Scallywag-CC.iml create mode 100644 .idea/editor.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 CMakeLists.txt create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 scallywag-ng.c create mode 100644 src/app.c create mode 100644 src/app.h create mode 100644 src/config.c create mode 100644 src/config.h create mode 100644 src/gui/callbacks.c create mode 100644 src/gui/callbacks.h create mode 100644 src/gui/loading.c create mode 100644 src/gui/loading.h create mode 100644 src/gui/logo.svg create mode 100644 src/gui/resources.gresource.xml create mode 100644 src/gui/window.c create mode 100644 src/gui/window.h create mode 100644 src/http.c create mode 100644 src/http.h create mode 100644 src/scraper/proxylister.c create mode 100644 src/scraper/proxylister.h create mode 100644 src/scraper/searcher.c create mode 100644 src/scraper/searcher.h create mode 100644 src/util/log.h create mode 100644 src/util/strutil.c create mode 100644 src/util/strutil.h diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..f01c525 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +scallywag \ No newline at end of file diff --git a/.idea/Scallywag-CC.iml b/.idea/Scallywag-CC.iml new file mode 100644 index 0000000..f08604b --- /dev/null +++ b/.idea/Scallywag-CC.iml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/.idea/editor.xml b/.idea/editor.xml new file mode 100644 index 0000000..1f0ef49 --- /dev/null +++ b/.idea/editor.xml @@ -0,0 +1,580 @@ + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..0b76fe5 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..547177c --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..d9b6d80 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..6f24040 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,76 @@ +cmake_minimum_required(VERSION 3.16) +project(scallywag C) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) + +# Find required packages +find_package(PkgConfig REQUIRED) + +pkg_check_modules(GTK3 REQUIRED gtk+-3.0) +pkg_check_modules(CURL REQUIRED libcurl) +pkg_check_modules(LIBXML2 REQUIRED libxml-2.0) + +# Compile GResource for embedded assets +find_program(GLIB_COMPILE_RESOURCES glib-compile-resources REQUIRED) +set(GRESOURCE_XML ${CMAKE_SOURCE_DIR}/src/gui/resources.gresource.xml) +set(GRESOURCE_C ${CMAKE_BINARY_DIR}/resources.c) + +add_custom_command( + OUTPUT ${GRESOURCE_C} + COMMAND ${GLIB_COMPILE_RESOURCES} + --target=${GRESOURCE_C} + --sourcedir=${CMAKE_SOURCE_DIR}/src/gui + --generate-source + ${GRESOURCE_XML} + DEPENDS ${GRESOURCE_XML} ${CMAKE_SOURCE_DIR}/src/gui/logo.svg + COMMENT "Compiling GResource" +) + +# Source files +set(SOURCES + scallywag-ng.c + src/app.c + src/config.c + src/http.c + src/scraper/proxylister.c + src/scraper/searcher.c + src/gui/window.c + src/gui/callbacks.c + src/gui/loading.c + src/util/strutil.c + ${GRESOURCE_C} +) + +# Executable +add_executable(scallywag ${SOURCES}) + +# Include directories +target_include_directories(scallywag PRIVATE + ${CMAKE_SOURCE_DIR}/src + ${GTK3_INCLUDE_DIRS} + ${CURL_INCLUDE_DIRS} + ${LIBXML2_INCLUDE_DIRS} +) + +# Link libraries +target_link_libraries(scallywag + ${GTK3_LIBRARIES} + ${CURL_LIBRARIES} + ${LIBXML2_LIBRARIES} +) + +# Compiler flags +target_compile_options(scallywag PRIVATE + ${GTK3_CFLAGS_OTHER} + ${CURL_CFLAGS_OTHER} + ${LIBXML2_CFLAGS_OTHER} + -Wall -Wextra -Wpedantic +) + +# Debug build option +option(DEBUG_BUILD "Enable debug logging" OFF) +if(DEBUG_BUILD) + target_compile_definitions(scallywag PRIVATE DEBUG) +endif() + 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 new file mode 100644 index 0000000..9c65186 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# Scallywag-NG + +A GTK3 torrent/magnet search application that automatically locates the PirateBay reverse proxies and allows users to search for and select items to open via your XDG default set torrent/magnet client. + +## History +Scallywag-NG is a C port of the famous Python utility Scallywag, now with half the scally and three times the wag. + +## Dependencies + +### Fedora + +```bash +sudo dnf install cmake gcc gtk3-devel libcurl-devel libxml2-devel glib2-devel +``` + +### Ubuntu / Debian + +```bash +sudo apt install cmake gcc libgtk-3-dev libcurl4-openssl-dev libxml2-dev libglib2.0-dev +``` + +## Building + +```bash +mkdir build +cd build +cmake .. +make +``` + +## Running + +```bash +./scallywag +``` + +Configuration is stored in `~/.config/scallywag/config.ini` and is auto-created with defaults on first run. + +## License + +Copyright (C) SILO GROUP LLC + +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 . diff --git a/scallywag-ng.c b/scallywag-ng.c new file mode 100644 index 0000000..b129828 --- /dev/null +++ b/scallywag-ng.c @@ -0,0 +1,130 @@ +/* + * Scallywag-NG: Half the scally, and twice the wag. + * Copyright (C) 2025 SILO GROUP LLC + * + * 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 . + */ + +#include "src/app.h" +#include "src/http.h" +#include "src/util/log.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#define CONFIG_DIR_NAME "scallywag" +#define CONFIG_FILE_NAME "config.ini" + +static const char *DEFAULT_CONFIG = + "[list_source]\n" + "piratebay_proxy_list = https://piratebayproxy.info/\n"; + +static char *get_config_path(void) { + const char *home = getenv("HOME"); + if (!home) { + LOG_ERROR("HOME environment variable not set"); + return NULL; + } + + // Build paths - use PATH_MAX or reasonable limit + char config_dir[512]; + char config_path[512]; + char parent_dir[512]; + int n = snprintf(config_dir, sizeof(config_dir), "%s/.config/%s", home, CONFIG_DIR_NAME); + if (n < 0 || (size_t)n >= sizeof(config_dir)) { + LOG_ERROR("Config path too long"); + return NULL; + } + n = snprintf(config_path, sizeof(config_path), "%s/%s", config_dir, CONFIG_FILE_NAME); + if (n < 0 || (size_t)n >= sizeof(config_path)) { + LOG_ERROR("Config path too long"); + return NULL; + } + + // Check if config exists + if (access(config_path, F_OK) == 0) { + return strdup(config_path); + } + + // Create config directory if needed + snprintf(parent_dir, sizeof(parent_dir), "%s/.config", home); + mkdir(parent_dir, 0755); // May already exist + if (mkdir(config_dir, 0755) != 0 && errno != EEXIST) { + LOG_ERROR("Failed to create config directory: %s", config_dir); + return NULL; + } + + // Write default config + FILE *f = fopen(config_path, "w"); + if (!f) { + LOG_ERROR("Failed to create config file: %s", config_path); + return NULL; + } + fprintf(f, "%s", DEFAULT_CONFIG); + fclose(f); + + LOG_INFO("Created default config at: %s", config_path); + return strdup(config_path); +} + +int main(int argc, char *argv[]) { + // Print GPL license notice + printf("Scallywag Copyright (C) 2025 SILO GROUP LLC\n"); + printf("This program comes with ABSOLUTELY NO WARRANTY.\n"); + printf("This is free software, and you are welcome to redistribute it\n"); + printf("under certain conditions.\n\n"); + + // Initialize GTK + gtk_init(&argc, &argv); + + // Initialize libxml2 + xmlInitParser(); + + // Initialize HTTP (curl) + if (http_global_init() != 0) { + LOG_ERROR("Failed to initialize HTTP"); + return 1; + } + + // Get config file path + char *config_path = get_config_path(); + LOG_INFO("Using config: %s", config_path); + + // Create application + App *app = app_new(config_path); + free(config_path); + + if (!app) { + LOG_ERROR("Failed to create application"); + http_global_cleanup(); + xmlCleanupParser(); + return 1; + } + + // Run application + app_run(app); + + // Cleanup + app_free(app); + http_global_cleanup(); + xmlCleanupParser(); + + LOG_INFO("Scallywag exited cleanly"); + return 0; +} diff --git a/src/app.c b/src/app.c new file mode 100644 index 0000000..dcb001d --- /dev/null +++ b/src/app.c @@ -0,0 +1,530 @@ +/* + * Scallywag-NG: Half the scally, and twice the wag. + * Copyright (C) 2025 SILO GROUP LLC + * + * 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 . + */ + +#include "app.h" +#include "gui/window.h" +#include "gui/callbacks.h" +#include "gui/loading.h" +#include "util/log.h" +#include +#include +#include + +// Forward declaration +GtkWidget *window_create(App *app); + +// Child setup function to detach spawned process +static void child_detach(gpointer user_data) { + (void)user_data; + setsid(); +} + +// Global app pointer for loading dialog callbacks +static App *g_app = NULL; + +void app_loading_log(const char *message) { + if (g_app && g_app->loading_dialog) { + loading_dialog_log(g_app->loading_dialog, message); + } +} + +void app_loading_status(const char *status) { + if (g_app && g_app->loading_dialog) { + loading_dialog_set_status(g_app->loading_dialog, status); + } +} + +void app_loading_pulse(void) { + if (g_app && g_app->loading_dialog) { + loading_dialog_set_progress(g_app->loading_dialog, -1); + } +} + +void app_toggle_loading_dialog(App *app) { + if (!app || !app->loading_dialog || !app->loading_dialog->window) return; + + GtkWindow *win = GTK_WINDOW(app->loading_dialog->window); + + if (gtk_widget_get_visible(app->loading_dialog->window)) { + // Save position before hiding + gtk_window_get_position(win, &loading_dialog_saved_x, &loading_dialog_saved_y); + gtk_widget_hide(app->loading_dialog->window); + } else { + // Restore position if we have one + if (loading_dialog_saved_x >= 0 && loading_dialog_saved_y >= 0) { + gtk_window_move(win, loading_dialog_saved_x, loading_dialog_saved_y); + } + gtk_widget_show(app->loading_dialog->window); + gtk_window_present(win); + } +} + +void app_log(App *app, const char *message) { + if (!app || !message) return; + + // Log to loading dialog + if (app->loading_dialog) { + loading_dialog_log(app->loading_dialog, message); + } + + // Also set as status bar message + app_set_status(app, message); +} + +App *app_new(const char *config_path) { + // Allocate app first + App *app = malloc(sizeof(App)); + if (!app) { + LOG_ERROR("Failed to allocate App"); + return NULL; + } + + // Initialize all fields to NULL/0 first + memset(app, 0, sizeof(App)); + + // Set global app pointer + g_app = app; + + // Show loading dialog + app->loading_dialog = loading_dialog_new(); + if (!app->loading_dialog) { + LOG_ERROR("Failed to create loading dialog"); + free(app); + g_app = NULL; + return NULL; + } + + loading_dialog_set_status(app->loading_dialog, "Initializing..."); + loading_dialog_log(app->loading_dialog, "[INFO] Starting Scallywag..."); + + // Load configuration + loading_dialog_set_status(app->loading_dialog, "Loading configuration..."); + loading_dialog_log(app->loading_dialog, "[INFO] Loading config from: "); + loading_dialog_log(app->loading_dialog, config_path); + + app->config = config_load(config_path); + if (!app->config) { + loading_dialog_log(app->loading_dialog, "[ERROR] Failed to load configuration"); + loading_dialog_set_status(app->loading_dialog, "Failed to load config"); + LOG_ERROR("Failed to load configuration"); + app_free(app); + return NULL; + } + loading_dialog_log(app->loading_dialog, "[OK] Configuration loaded"); + + // Create results list + app->results = result_list_new(); + if (!app->results) { + loading_dialog_log(app->loading_dialog, "[ERROR] Failed to create results list"); + LOG_ERROR("Failed to create results list"); + app_free(app); + return NULL; + } + + // Create results store + app->results_store = gtk_list_store_new(6, + G_TYPE_INT, G_TYPE_INT, G_TYPE_STRING, + G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING); + + // Fetch proxy list (this is the slow part) + loading_dialog_set_status(app->loading_dialog, "Fetching proxy list..."); + loading_dialog_log(app->loading_dialog, "[INFO] Fetching proxies from:"); + loading_dialog_log(app->loading_dialog, app->config->proxylist_url); + loading_dialog_set_progress(app->loading_dialog, -1); // Start pulsing + + app->proxies = proxylister_fetch(app->config->proxylist_url); + + if (app->proxies && app->proxies->count > 0) { + char msg[128]; + snprintf(msg, sizeof(msg), "[OK] Found %zu proxies", app->proxies->count); + loading_dialog_log(app->loading_dialog, msg); + loading_dialog_set_progress(app->loading_dialog, 1.0); + } else { + loading_dialog_log(app->loading_dialog, "[WARN] No proxies found - check your connection"); + loading_dialog_set_progress(app->loading_dialog, 1.0); + } + + // Create main window + loading_dialog_set_status(app->loading_dialog, "Creating main window..."); + loading_dialog_log(app->loading_dialog, "[INFO] Initializing UI..."); + + app->window = window_create(app); + if (!app->window) { + loading_dialog_log(app->loading_dialog, "[ERROR] Failed to create window"); + LOG_ERROR("Failed to create window"); + app_free(app); + return NULL; + } + + // Get statusbar context + app->statusbar_context = gtk_statusbar_get_context_id( + GTK_STATUSBAR(app->statusbar), "main"); + + // Populate proxy combo box + if (app->proxies) { + for (size_t i = 0; i < app->proxies->count; i++) { + gtk_combo_box_text_append_text( + GTK_COMBO_BOX_TEXT(app->combo_proxy), + app->proxies->proxies[i]); + } + if (app->proxies->count > 0) { + gtk_combo_box_set_active(GTK_COMBO_BOX(app->combo_proxy), 0); + } + } + + // Set initial status + if (app->proxies && app->proxies->count > 0) { + app_set_status(app, "Ready to search"); + } else { + app_set_status(app, "No proxies available - click Refresh"); + } + + // Set focus to search entry + gtk_widget_grab_focus(app->entry_search); + + loading_dialog_log(app->loading_dialog, "[OK] Startup complete"); + loading_dialog_set_status(app->loading_dialog, "Ready!"); + loading_dialog_set_progress(app->loading_dialog, 1.0); + + // Hide loading dialog (keep it for later use - click logo to show) + gtk_widget_hide(app->loading_dialog->window); + + return app; +} + +void app_free(App *app) { + if (!app) return; + + // Clear global app pointer + if (g_app == app) { + g_app = NULL; + } + + if (app->loading_dialog) { + loading_dialog_free(app->loading_dialog); + } + + free(app->last_query); + + if (app->results) { + result_list_free(app->results); + } + + if (app->proxies) { + proxy_list_free(app->proxies); + } + + if (app->config) { + config_free(app->config); + } + + // Note: GTK widgets are cleaned up by GTK when window is destroyed + // results_store is also managed by GTK when associated with treeview + + free(app); +} + +void app_run(App *app) { + if (!app || !app->window) return; + + gtk_widget_show_all(app->window); + gtk_main(); +} + +void app_set_status(App *app, const char *message) { + if (!app || !message) return; + + // Update statusbar + if (app->statusbar) { + gtk_statusbar_pop(GTK_STATUSBAR(app->statusbar), app->statusbar_context); + gtk_statusbar_push(GTK_STATUSBAR(app->statusbar), app->statusbar_context, message); + } + + // Also log to loading dialog + if (app->loading_dialog) { + loading_dialog_log(app->loading_dialog, message); + } +} + +void app_refresh_proxies(App *app) { + if (!app || !app->config) return; + + // Clear existing proxies + if (app->proxies) { + proxy_list_free(app->proxies); + } + + // Fetch new proxy list + app->proxies = proxylister_fetch(app->config->proxylist_url); + + // Update combo box + if (app->combo_proxy) { + gtk_combo_box_text_remove_all(GTK_COMBO_BOX_TEXT(app->combo_proxy)); + + if (app->proxies) { + for (size_t i = 0; i < app->proxies->count; i++) { + gtk_combo_box_text_append_text( + GTK_COMBO_BOX_TEXT(app->combo_proxy), + app->proxies->proxies[i]); + } + + if (app->proxies->count > 0) { + gtk_combo_box_set_active(GTK_COMBO_BOX(app->combo_proxy), 0); + app_set_status(app, "Proxies loaded. Ready to search."); + } else { + app_set_status(app, "No proxies found!"); + } + } else { + app_set_status(app, "Failed to fetch proxies!"); + } + } +} + +const char *app_get_current_proxy(App *app) { + if (!app || !app->combo_proxy) return NULL; + + gchar *text = gtk_combo_box_text_get_active_text(GTK_COMBO_BOX_TEXT(app->combo_proxy)); + if (text) { + // Store for later use (GTK string needs to be freed) + app->current_proxy = text; + return app->current_proxy; + } + return NULL; +} + +void app_search(App *app, const char *query) { + if (!app || !query || strlen(query) == 0) return; + + if (!app->proxies || app->proxies->count == 0) { + app_set_status(app, "No proxies available!"); + return; + } + + // Store query for pagination + free(app->last_query); + app->last_query = g_strdup(query); + app->current_page = 1; + + // Clear results store + gtk_list_store_clear(app->results_store); + + // Clear results list + result_list_clear(app->results); + + // Try every proxy in the list + for (size_t i = 0; i < app->proxies->count; i++) { + const char *proxy = app->proxies->proxies[i]; + + char status[256]; + snprintf(status, sizeof(status), "Trying %s (%zu/%zu)...", proxy, i + 1, app->proxies->count); + app_set_status(app, status); + + // Update combo box to show current proxy + gtk_combo_box_set_active(GTK_COMBO_BOX(app->combo_proxy), (gint)i); + + // Process pending GTK events to update UI + while (gtk_events_pending()) gtk_main_iteration(); + + // Perform search + ResultList *results = searcher_search(proxy, query, 1); + + // Check if we got valid results + if (results && results->count > 0) { + // Success - populate store + GtkTreeIter iter; + for (size_t j = 0; j < results->count; j++) { + Result *r = &results->items[j]; + + gtk_list_store_append(app->results_store, &iter); + gtk_list_store_set(app->results_store, &iter, + 0, r->seeders, + 1, r->leechers, + 2, r->size, + 3, r->title, + 4, r->author, + 5, r->url, + -1); + } + + snprintf(status, sizeof(status), "%zu results found via %s (page 1)", results->count, proxy); + app_set_status(app, status); + + // Update page counter + app->total_pages = results->total_pages; + if (app->lbl_page_count) { + char page_str[32]; + snprintf(page_str, sizeof(page_str), "pages %d/%d", app->current_page, app->total_pages); + gtk_label_set_text(GTK_LABEL(app->lbl_page_count), page_str); + } + + // Grey out "Get More Results" if on last page + if (app->btn_more) { + gtk_widget_set_sensitive(app->btn_more, app->current_page < app->total_pages); + } + + // Transfer ownership + result_list_free(app->results); + app->results = results; + return; + } + + // Failed or empty - log and continue to next + if (results) { + result_list_free(results); + } + + LOG_WARN("Proxy %s failed, trying next...", proxy); + } + + app_set_status(app, "All proxies failed to return results!"); +} + +void app_get_more_results(App *app) { + if (!app || !app->last_query || strlen(app->last_query) == 0) { + app_set_status(app, "No search to continue!"); + return; + } + + if (!app->proxies || app->proxies->count == 0) { + app_set_status(app, "No proxies available!"); + return; + } + + int next_page = app->current_page + 1; + + // Try every proxy in the list + for (size_t i = 0; i < app->proxies->count; i++) { + const char *proxy = app->proxies->proxies[i]; + + char status[256]; + snprintf(status, sizeof(status), "Loading page %d from %s...", next_page, proxy); + app_set_status(app, status); + + // Update combo box to show current proxy + gtk_combo_box_set_active(GTK_COMBO_BOX(app->combo_proxy), (gint)i); + + // Process pending GTK events to update UI + while (gtk_events_pending()) gtk_main_iteration(); + + // Perform search for next page + ResultList *results = searcher_search(proxy, app->last_query, next_page); + + // Check if we got valid results + if (results && results->count > 0) { + // Success - append to store + GtkTreeIter iter; + for (size_t j = 0; j < results->count; j++) { + Result *r = &results->items[j]; + + gtk_list_store_append(app->results_store, &iter); + gtk_list_store_set(app->results_store, &iter, + 0, r->seeders, + 1, r->leechers, + 2, r->size, + 3, r->title, + 4, r->author, + 5, r->url, + -1); + } + + app->current_page = next_page; + + // Update page counter + if (app->lbl_page_count) { + char page_str[32]; + snprintf(page_str, sizeof(page_str), "pages %d/%d", app->current_page, app->total_pages); + gtk_label_set_text(GTK_LABEL(app->lbl_page_count), page_str); + } + + // Grey out "Get More Results" if on last page + if (app->btn_more) { + gtk_widget_set_sensitive(app->btn_more, app->current_page < app->total_pages); + } + + // Count total results in store + int total = gtk_tree_model_iter_n_children(GTK_TREE_MODEL(app->results_store), NULL); + snprintf(status, sizeof(status), "%d total results (page %d)", total, next_page); + app_set_status(app, status); + + result_list_free(results); + return; + } + + // Failed or empty - log and continue to next + if (results) { + result_list_free(results); + } + + LOG_WARN("Proxy %s failed for page %d, trying next...", proxy, next_page); + } + + app_set_status(app, "No more results found!"); +} + +void app_download_selected(App *app) { + if (!app || !app->treeview_results) return; + + GtkTreeSelection *selection = gtk_tree_view_get_selection( + GTK_TREE_VIEW(app->treeview_results)); + + GtkTreeModel *model; + GtkTreeIter iter; + + if (!gtk_tree_selection_get_selected(selection, &model, &iter)) { + app_set_status(app, "No torrent selected!"); + return; + } + + // Get URL from column 5 + gchar *url = NULL; + gtk_tree_model_get(model, &iter, 5, &url, -1); + + if (!url) { + app_set_status(app, "Failed to get torrent URL!"); + return; + } + + app_set_status(app, "Fetching magnet link..."); + + // Get magnet link + char *magnet = searcher_get_magnet(url); + g_free(url); + + if (!magnet) { + app_set_status(app, "Failed to get magnet link!"); + return; + } + + // Launch with xdg-open (detached so it survives if scallywag exits) + GError *error = NULL; + gchar *argv[] = {"xdg-open", magnet, NULL}; + + g_spawn_async(NULL, argv, NULL, + G_SPAWN_SEARCH_PATH | G_SPAWN_STDOUT_TO_DEV_NULL | G_SPAWN_STDERR_TO_DEV_NULL, + child_detach, NULL, NULL, &error); + + if (error) { + LOG_ERROR("Failed to launch xdg-open: %s", error->message); + app_set_status(app, "Failed to open magnet link!"); + g_error_free(error); + } else { + app_set_status(app, "Opening magnet with xdg-open..."); + } + + free(magnet); +} diff --git a/src/app.h b/src/app.h new file mode 100644 index 0000000..f62c0b4 --- /dev/null +++ b/src/app.h @@ -0,0 +1,105 @@ +/* + * Scallywag-NG: Half the scally, and twice the wag. + * Copyright (C) 2025 SILO GROUP LLC + * + * 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 . + */ + +#ifndef SCALLYWAG_APP_H +#define SCALLYWAG_APP_H + +#include +#include "config.h" +#include "scraper/proxylister.h" +#include "scraper/searcher.h" +#include "gui/loading.h" + +typedef struct App { + // Configuration + Config *config; + + // Current proxy (not owned, points to proxies list) + const char *current_proxy; + + // Pagination state + char *last_query; + int current_page; + int total_pages; + + // Data + ProxyList *proxies; + ResultList *results; + + // GTK Widgets (not owned, GTK manages lifecycle) + GtkWidget *window; + GtkWidget *combo_proxy; + GtkWidget *entry_search; + GtkWidget *treeview_results; + GtkWidget *btn_search; + GtkWidget *btn_refresh; + GtkWidget *btn_download; + GtkWidget *btn_more; + GtkWidget *lbl_page_count; + GtkWidget *statusbar; + + // GTK Models (owned) + GtkListStore *results_store; + + // Statusbar context + guint statusbar_context; + + // Loading dialog (persistent, can be toggled) + LoadingDialog *loading_dialog; +} App; + +// Create application instance +// Returns NULL on failure +App *app_new(const char *config_path); + +// Free application resources +void app_free(App *app); + +// Run the application (enters GTK main loop) +void app_run(App *app); + +// Set status bar message +void app_set_status(App *app, const char *message); + +// Refresh proxy list +void app_refresh_proxies(App *app); + +// Perform search (starts at page 1) +void app_search(App *app, const char *query); + +// Get more results (next page) +void app_get_more_results(App *app); + +// Get currently selected proxy +const char *app_get_current_proxy(App *app); + +// Download selected torrent (open magnet link) +void app_download_selected(App *app); + +// Loading dialog callbacks (used during startup and runtime) +void app_loading_log(const char *message); +void app_loading_status(const char *status); +void app_loading_pulse(void); + +// Toggle loading dialog visibility +void app_toggle_loading_dialog(App *app); + +// Log message to both statusbar and loading dialog +void app_log(App *app, const char *message); + +#endif diff --git a/src/config.c b/src/config.c new file mode 100644 index 0000000..49e45d5 --- /dev/null +++ b/src/config.c @@ -0,0 +1,65 @@ +/* + * Scallywag-NG: Half the scally, and twice the wag. + * Copyright (C) 2025 SILO GROUP LLC + * + * 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 . + */ + +#include "config.h" +#include "util/log.h" +#include +#include + +#define DEFAULT_PROXYLIST_URL "https://piratebayproxy.info/" + +Config *config_load(const char *filepath) { + Config *config = malloc(sizeof(Config)); + if (!config) { + LOG_ERROR("Failed to allocate config"); + return NULL; + } + + config->proxylist_url = NULL; + + GKeyFile *keyfile = g_key_file_new(); + GError *error = NULL; + + if (!g_key_file_load_from_file(keyfile, filepath, G_KEY_FILE_NONE, &error)) { + LOG_WARN("Failed to load config file '%s': %s", filepath, error->message); + LOG_INFO("Using default configuration"); + g_error_free(error); + config->proxylist_url = g_strdup(DEFAULT_PROXYLIST_URL); + g_key_file_free(keyfile); + return config; + } + + gchar *url = g_key_file_get_string(keyfile, "list_source", "piratebay_proxy_list", &error); + if (!url) { + LOG_WARN("Missing 'piratebay_proxy_list' in config: %s", error->message); + g_error_free(error); + config->proxylist_url = g_strdup(DEFAULT_PROXYLIST_URL); + } else { + config->proxylist_url = url; // g_key_file_get_string returns allocated string + } + + g_key_file_free(keyfile); + return config; +} + +void config_free(Config *config) { + if (!config) return; + + g_free(config->proxylist_url); + free(config); +} diff --git a/src/config.h b/src/config.h new file mode 100644 index 0000000..49c9ca9 --- /dev/null +++ b/src/config.h @@ -0,0 +1,33 @@ +/* + * Scallywag-NG: Half the scally, and twice the wag. + * Copyright (C) 2025 SILO GROUP LLC + * + * 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 . + */ + +#ifndef SCALLYWAG_CONFIG_H +#define SCALLYWAG_CONFIG_H + +typedef struct { + char *proxylist_url; // URL to fetch proxy list +} Config; + +// Load configuration from INI file +// Returns NULL on failure +Config *config_load(const char *filepath); + +// Free configuration +void config_free(Config *config); + +#endif diff --git a/src/gui/callbacks.c b/src/gui/callbacks.c new file mode 100644 index 0000000..0028f49 --- /dev/null +++ b/src/gui/callbacks.c @@ -0,0 +1,95 @@ +/* + * Scallywag-NG: Half the scally, and twice the wag. + * Copyright (C) 2025 SILO GROUP LLC + * + * 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 . + */ + +#include "callbacks.h" +#include "../app.h" +#include "../util/log.h" + +void on_window_destroy(GtkWidget *widget, gpointer data) { + (void)widget; + (void)data; + LOG_INFO("Window destroyed, quitting"); + gtk_main_quit(); +} + +void on_proxy_changed(GtkComboBox *combo, gpointer data) { + (void)combo; + App *app = (App *)data; + + const char *proxy = app_get_current_proxy(app); + if (proxy) { + char status[256]; + snprintf(status, sizeof(status), "Changed proxy to: %s", proxy); + app_set_status(app, status); + } +} + +void on_search_clicked(GtkButton *button, gpointer data) { + (void)button; + App *app = (App *)data; + + const gchar *query = gtk_entry_get_text(GTK_ENTRY(app->entry_search)); + if (query && strlen(query) > 0) { + app_search(app, query); + } else { + app_set_status(app, "Enter a search term"); + } +} + +void on_search_activate(GtkEntry *entry, gpointer data) { + (void)entry; + // Same as search button click + on_search_clicked(NULL, data); +} + +void on_refresh_clicked(GtkButton *button, gpointer data) { + (void)button; + App *app = (App *)data; + + app_set_status(app, "Refreshing proxies..."); + app_refresh_proxies(app); +} + +void on_download_clicked(GtkButton *button, gpointer data) { + (void)button; + App *app = (App *)data; + app_download_selected(app); +} + +void on_more_clicked(GtkButton *button, gpointer data) { + (void)button; + App *app = (App *)data; + app_get_more_results(app); +} + +void on_row_activated(GtkTreeView *tree, GtkTreePath *path, + GtkTreeViewColumn *col, gpointer data) { + (void)tree; + (void)path; + (void)col; + // Same as download button click + on_download_clicked(NULL, data); +} + +gboolean on_logo_clicked(GtkWidget *widget, GdkEventButton *event, gpointer data) { + (void)widget; + (void)event; + App *app = (App *)data; + app_toggle_loading_dialog(app); + return TRUE; // Event handled +} diff --git a/src/gui/callbacks.h b/src/gui/callbacks.h new file mode 100644 index 0000000..db30596 --- /dev/null +++ b/src/gui/callbacks.h @@ -0,0 +1,55 @@ +/* + * Scallywag-NG: Half the scally, and twice the wag. + * Copyright (C) 2025 SILO GROUP LLC + * + * 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 . + */ + +#ifndef SCALLYWAG_CALLBACKS_H +#define SCALLYWAG_CALLBACKS_H + +#include + +// Forward declare App struct (defined in app.h) +struct App; + +// Window destroy handler +void on_window_destroy(GtkWidget *widget, gpointer data); + +// Proxy combo box changed +void on_proxy_changed(GtkComboBox *combo, gpointer data); + +// Search button clicked +void on_search_clicked(GtkButton *button, gpointer data); + +// Search entry activated (Enter key) +void on_search_activate(GtkEntry *entry, gpointer data); + +// Refresh button clicked +void on_refresh_clicked(GtkButton *button, gpointer data); + +// Download button clicked +void on_download_clicked(GtkButton *button, gpointer data); + +// More results button clicked +void on_more_clicked(GtkButton *button, gpointer data); + +// Row activated (double-click or Enter on row) +void on_row_activated(GtkTreeView *tree, GtkTreePath *path, + GtkTreeViewColumn *col, gpointer data); + +// Logo clicked - toggle loading dialog +gboolean on_logo_clicked(GtkWidget *widget, GdkEventButton *event, gpointer data); + +#endif diff --git a/src/gui/loading.c b/src/gui/loading.c new file mode 100644 index 0000000..5b08eec --- /dev/null +++ b/src/gui/loading.c @@ -0,0 +1,162 @@ +/* + * Scallywag-NG: Half the scally, and twice the wag. + * Copyright (C) 2025 SILO GROUP LLC + * + * 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 . + */ + +#include "loading.h" +#include +#include + +#define LOGO_RESOURCE_PATH "/org/scallywag/logo.svg" + +// Saved position (shared with app.c via extern) +gint loading_dialog_saved_x = -1; +gint loading_dialog_saved_y = -1; + +// Hide window instead of destroying on close +static gboolean on_loading_delete(GtkWidget *widget, GdkEvent *event, gpointer data) { + (void)event; + (void)data; + // Save position before hiding + gtk_window_get_position(GTK_WINDOW(widget), &loading_dialog_saved_x, &loading_dialog_saved_y); + gtk_widget_hide(widget); + return TRUE; // Prevent destruction +} + +LoadingDialog *loading_dialog_new(void) { + LoadingDialog *dialog = malloc(sizeof(LoadingDialog)); + if (!dialog) return NULL; + + // Create window + dialog->window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + gtk_window_set_title(GTK_WINDOW(dialog->window), "Scallywag - Log"); + gtk_window_set_default_size(GTK_WINDOW(dialog->window), 500, 400); + gtk_window_set_position(GTK_WINDOW(dialog->window), GTK_WIN_POS_CENTER); + gtk_window_set_resizable(GTK_WINDOW(dialog->window), FALSE); + g_signal_connect(dialog->window, "delete-event", G_CALLBACK(on_loading_delete), NULL); + + // Main container + GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); + gtk_container_set_border_width(GTK_CONTAINER(vbox), 15); + gtk_container_add(GTK_CONTAINER(dialog->window), vbox); + + // Logo + GError *error = NULL; + GdkPixbuf *pixbuf = gdk_pixbuf_new_from_resource_at_scale( + LOGO_RESOURCE_PATH, 150, -1, TRUE, &error); + if (pixbuf) { + GtkWidget *logo = gtk_image_new_from_pixbuf(pixbuf); + gtk_widget_set_halign(logo, GTK_ALIGN_CENTER); + gtk_box_pack_start(GTK_BOX(vbox), logo, FALSE, FALSE, 5); + g_object_unref(pixbuf); + } + if (error) g_error_free(error); + + // Program name label + GtkWidget *title_label = gtk_label_new(NULL); + gtk_label_set_markup(GTK_LABEL(title_label), "Scallywag"); + gtk_widget_set_halign(title_label, GTK_ALIGN_CENTER); + gtk_box_pack_start(GTK_BOX(vbox), title_label, FALSE, FALSE, 0); + + // Subtitle with link + GtkWidget *subtitle_label = gtk_label_new(NULL); + gtk_label_set_markup(GTK_LABEL(subtitle_label), + "brought to you by SILO GROUP"); + gtk_widget_set_halign(subtitle_label, GTK_ALIGN_CENTER); + gtk_widget_set_margin_bottom(subtitle_label, 10); + gtk_box_pack_start(GTK_BOX(vbox), subtitle_label, FALSE, FALSE, 0); + + // Status label + dialog->status_label = gtk_label_new("Initializing..."); + gtk_label_set_xalign(GTK_LABEL(dialog->status_label), 0.0); + gtk_box_pack_start(GTK_BOX(vbox), dialog->status_label, FALSE, FALSE, 5); + + // Progress bar + dialog->progress_bar = gtk_progress_bar_new(); + gtk_progress_bar_set_show_text(GTK_PROGRESS_BAR(dialog->progress_bar), FALSE); + gtk_box_pack_start(GTK_BOX(vbox), dialog->progress_bar, FALSE, FALSE, 5); + + // Scrolled window for log + GtkWidget *scrolled = gtk_scrolled_window_new(NULL, NULL); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled), + GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_IN); + gtk_box_pack_start(GTK_BOX(vbox), scrolled, TRUE, TRUE, 5); + + // Text view for log + dialog->log_view = gtk_text_view_new(); + gtk_text_view_set_editable(GTK_TEXT_VIEW(dialog->log_view), FALSE); + gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(dialog->log_view), FALSE); + gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(dialog->log_view), GTK_WRAP_WORD_CHAR); + dialog->log_buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(dialog->log_view)); + gtk_container_add(GTK_CONTAINER(scrolled), dialog->log_view); + + // Show the window + gtk_widget_show_all(dialog->window); + loading_dialog_update(); + + return dialog; +} + +void loading_dialog_free(LoadingDialog *dialog) { + if (!dialog) return; + + if (dialog->window) { + gtk_widget_destroy(dialog->window); + } + free(dialog); +} + +void loading_dialog_set_progress(LoadingDialog *dialog, double fraction) { + if (!dialog || !dialog->progress_bar) return; + + if (fraction < 0) { + gtk_progress_bar_pulse(GTK_PROGRESS_BAR(dialog->progress_bar)); + } else { + gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(dialog->progress_bar), fraction); + } + loading_dialog_update(); +} + +void loading_dialog_set_status(LoadingDialog *dialog, const char *status) { + if (!dialog || !dialog->status_label) return; + + gtk_label_set_text(GTK_LABEL(dialog->status_label), status); + loading_dialog_update(); +} + +void loading_dialog_log(LoadingDialog *dialog, const char *message) { + if (!dialog || !dialog->log_buffer) return; + + GtkTextIter end; + gtk_text_buffer_get_end_iter(dialog->log_buffer, &end); + gtk_text_buffer_insert(dialog->log_buffer, &end, message, -1); + gtk_text_buffer_insert(dialog->log_buffer, &end, "\n", -1); + + // Scroll to bottom + gtk_text_buffer_get_end_iter(dialog->log_buffer, &end); + GtkTextMark *mark = gtk_text_buffer_create_mark(dialog->log_buffer, NULL, &end, FALSE); + gtk_text_view_scroll_to_mark(GTK_TEXT_VIEW(dialog->log_view), mark, 0.0, FALSE, 0.0, 1.0); + gtk_text_buffer_delete_mark(dialog->log_buffer, mark); + + loading_dialog_update(); +} + +void loading_dialog_update(void) { + while (gtk_events_pending()) { + gtk_main_iteration(); + } +} diff --git a/src/gui/loading.h b/src/gui/loading.h new file mode 100644 index 0000000..62730dd --- /dev/null +++ b/src/gui/loading.h @@ -0,0 +1,54 @@ +/* + * Scallywag-NG: Half the scally, and twice the wag. + * Copyright (C) 2025 SILO GROUP LLC + * + * 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 . + */ + +#ifndef SCALLYWAG_LOADING_H +#define SCALLYWAG_LOADING_H + +#include + +typedef struct { + GtkWidget *window; + GtkWidget *progress_bar; + GtkWidget *status_label; + GtkWidget *log_view; + GtkTextBuffer *log_buffer; +} LoadingDialog; + +// Create loading dialog +LoadingDialog *loading_dialog_new(void); + +// Free loading dialog +void loading_dialog_free(LoadingDialog *dialog); + +// Update progress (0.0 to 1.0, or -1 for pulse) +void loading_dialog_set_progress(LoadingDialog *dialog, double fraction); + +// Set status text +void loading_dialog_set_status(LoadingDialog *dialog, const char *status); + +// Append log message +void loading_dialog_log(LoadingDialog *dialog, const char *message); + +// Process pending GTK events +void loading_dialog_update(void); + +// Saved window position (for show/hide) +extern gint loading_dialog_saved_x; +extern gint loading_dialog_saved_y; + +#endif diff --git a/src/gui/logo.svg b/src/gui/logo.svg new file mode 100644 index 0000000..115a7f2 --- /dev/null +++ b/src/gui/logo.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/gui/resources.gresource.xml b/src/gui/resources.gresource.xml new file mode 100644 index 0000000..d367ba1 --- /dev/null +++ b/src/gui/resources.gresource.xml @@ -0,0 +1,6 @@ + + + + logo.svg + + diff --git a/src/gui/window.c b/src/gui/window.c new file mode 100644 index 0000000..f214b32 --- /dev/null +++ b/src/gui/window.c @@ -0,0 +1,246 @@ +/* + * Scallywag-NG: Half the scally, and twice the wag. + * Copyright (C) 2025 SILO GROUP LLC + * + * 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 . + */ + +#include "window.h" +#include "callbacks.h" +#include "../app.h" +#include + +#define LOGO_RESOURCE_PATH "/org/scallywag/logo.svg" + +// Column indices for results store +enum { + COL_SEEDERS = 0, + COL_LEECHERS, + COL_SIZE, + COL_TITLE, + COL_AUTHOR, + COL_URL, + NUM_COLS +}; + +// Callback for proxy section toggle +static void on_proxy_toggle(GtkToggleButton *toggle, gpointer user_data) { + GtkWidget *proxy_box = GTK_WIDGET(user_data); + gboolean active = gtk_toggle_button_get_active(toggle); + + if (active) { + gtk_widget_show(proxy_box); + // Show all children explicitly + GList *children = gtk_container_get_children(GTK_CONTAINER(proxy_box)); + for (GList *l = children; l != NULL; l = l->next) { + gtk_widget_show(GTK_WIDGET(l->data)); + } + g_list_free(children); + } else { + gtk_widget_hide(proxy_box); + } + gtk_button_set_label(GTK_BUTTON(toggle), active ? "\342\226\274 Proxy" : "\342\226\266 Proxy"); +} + +// Handle horizontal scroll wheel events +static gboolean on_scroll_event(GtkWidget *widget, GdkEventScroll *event, gpointer data) { + (void)widget; + GtkScrolledWindow *scrolled = GTK_SCROLLED_WINDOW(data); + GtkAdjustment *hadj = gtk_scrolled_window_get_hadjustment(scrolled); + gdouble step = gtk_adjustment_get_step_increment(hadj); + gdouble value = gtk_adjustment_get_value(hadj); + + // Handle Shift+scroll or horizontal scroll for horizontal movement + if (event->state & GDK_SHIFT_MASK || + event->direction == GDK_SCROLL_LEFT || + event->direction == GDK_SCROLL_RIGHT) { + + if (event->direction == GDK_SCROLL_UP || event->direction == GDK_SCROLL_LEFT) { + gtk_adjustment_set_value(hadj, value - step * 3); + } else if (event->direction == GDK_SCROLL_DOWN || event->direction == GDK_SCROLL_RIGHT) { + gtk_adjustment_set_value(hadj, value + step * 3); + } else if (event->direction == GDK_SCROLL_SMOOTH) { + gtk_adjustment_set_value(hadj, value + event->delta_x * step * 3); + } + return TRUE; // Event handled + } + return FALSE; // Let default handler process vertical scroll +} + +static void setup_treeview_columns(GtkTreeView *treeview) { + const char *titles[] = {"Seeders", "Leechers", "Size", "Title", "Author"}; + + // Don't show URL column (COL_URL) - data is still in store for downloads + for (int i = 0; i < COL_URL; i++) { + GtkCellRenderer *renderer = gtk_cell_renderer_text_new(); + GtkTreeViewColumn *column; + + // Use markup for Author column to render italic anonymous users + if (i == COL_AUTHOR) { + column = gtk_tree_view_column_new_with_attributes( + titles[i], renderer, "markup", i, NULL); + } else { + column = gtk_tree_view_column_new_with_attributes( + titles[i], renderer, "text", i, NULL); + } + + gtk_tree_view_column_set_resizable(column, TRUE); + + // Make title column expand + if (i == COL_TITLE) { + gtk_tree_view_column_set_expand(column, TRUE); + } + + gtk_tree_view_append_column(treeview, column); + } +} + +GtkWidget *window_create(App *app) { + // Set up CSS for alternating row colors + GtkCssProvider *css_provider = gtk_css_provider_new(); + gtk_css_provider_load_from_data(css_provider, + "treeview.view row:nth-child(even) { background-color: rgba(0, 26, 45, 0.15); }" + "treeview.view row:nth-child(even) cell { background-color: rgba(0, 26, 45, 0.15); }", + -1, NULL); + gtk_style_context_add_provider_for_screen(gdk_screen_get_default(), + GTK_STYLE_PROVIDER(css_provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + g_object_unref(css_provider); + + // Main window + GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + gtk_window_set_title(GTK_WINDOW(window), "Scallywag"); + gtk_window_set_default_size(GTK_WINDOW(window), 600, 500); + g_signal_connect(window, "destroy", G_CALLBACK(on_window_destroy), app); + + // Main vertical box with spacing + GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8); + gtk_container_set_border_width(GTK_CONTAINER(vbox), 12); + gtk_container_add(GTK_CONTAINER(window), vbox); + + // Logo (clickable - toggles loading dialog) + GError *error = NULL; + GdkPixbuf *pixbuf = gdk_pixbuf_new_from_resource_at_scale( + LOGO_RESOURCE_PATH, 120, -1, TRUE, &error); + if (pixbuf) { + GtkWidget *logo = gtk_image_new_from_pixbuf(pixbuf); + GtkWidget *event_box = gtk_event_box_new(); + gtk_container_add(GTK_CONTAINER(event_box), logo); + gtk_widget_set_halign(event_box, GTK_ALIGN_CENTER); + gtk_box_pack_start(GTK_BOX(vbox), event_box, FALSE, FALSE, 0); + g_signal_connect(event_box, "button-press-event", G_CALLBACK(on_logo_clicked), app); + g_object_unref(pixbuf); + } + if (error) g_error_free(error); + + // Program name label + GtkWidget *title_label = gtk_label_new(NULL); + gtk_label_set_markup(GTK_LABEL(title_label), "Scallywag"); + gtk_widget_set_halign(title_label, GTK_ALIGN_CENTER); + gtk_widget_set_margin_bottom(title_label, 12); + gtk_box_pack_start(GTK_BOX(vbox), title_label, FALSE, FALSE, 0); + + // Search row + GtkWidget *search_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); + gtk_box_pack_start(GTK_BOX(vbox), search_box, FALSE, TRUE, 0); + + GtkWidget *entry_search = gtk_entry_new(); + gtk_entry_set_placeholder_text(GTK_ENTRY(entry_search), "Enter search terms..."); + gtk_box_pack_start(GTK_BOX(search_box), entry_search, TRUE, TRUE, 0); + g_signal_connect(entry_search, "activate", G_CALLBACK(on_search_activate), app); + + GtkWidget *btn_search = gtk_button_new_with_label("Search"); + gtk_box_pack_start(GTK_BOX(search_box), btn_search, FALSE, FALSE, 0); + g_signal_connect(btn_search, "clicked", G_CALLBACK(on_search_clicked), app); + + // Collapsible proxy section + GtkWidget *proxy_toggle = gtk_toggle_button_new_with_label("\342\226\266 Proxy"); + gtk_widget_set_halign(proxy_toggle, GTK_ALIGN_START); + gtk_button_set_relief(GTK_BUTTON(proxy_toggle), GTK_RELIEF_NONE); + gtk_box_pack_start(GTK_BOX(vbox), proxy_toggle, FALSE, FALSE, 0); + + GtkWidget *proxy_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); + gtk_widget_set_margin_start(proxy_box, 16); + gtk_box_pack_start(GTK_BOX(vbox), proxy_box, FALSE, FALSE, 0); + + GtkWidget *combo_proxy = gtk_combo_box_text_new(); + gtk_box_pack_start(GTK_BOX(proxy_box), combo_proxy, TRUE, TRUE, 0); + g_signal_connect(combo_proxy, "changed", G_CALLBACK(on_proxy_changed), app); + + GtkWidget *btn_refresh = gtk_button_new_with_label("Refresh"); + gtk_box_pack_start(GTK_BOX(proxy_box), btn_refresh, FALSE, FALSE, 0); + g_signal_connect(btn_refresh, "clicked", G_CALLBACK(on_refresh_clicked), app); + + // Connect toggle after children are added, then hide + g_signal_connect(proxy_toggle, "toggled", G_CALLBACK(on_proxy_toggle), proxy_box); + gtk_widget_set_no_show_all(proxy_box, TRUE); // Prevent show_all from main window + gtk_widget_hide(proxy_box); + + // Scrolled window for results + GtkWidget *scrolled = gtk_scrolled_window_new(NULL, NULL); + gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_IN); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled), + GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_box_pack_start(GTK_BOX(vbox), scrolled, TRUE, TRUE, 0); + + // Tree view for results + GtkWidget *treeview = gtk_tree_view_new_with_model(GTK_TREE_MODEL(app->results_store)); + setup_treeview_columns(GTK_TREE_VIEW(treeview)); + gtk_container_add(GTK_CONTAINER(scrolled), treeview); + g_signal_connect(treeview, "row-activated", G_CALLBACK(on_row_activated), app); + g_signal_connect(treeview, "scroll-event", G_CALLBACK(on_scroll_event), scrolled); + gtk_widget_add_events(treeview, GDK_SCROLL_MASK | GDK_SMOOTH_SCROLL_MASK); + + // Get More Results button (centered, initially disabled) + GtkWidget *btn_more = gtk_button_new_with_label("Get More Results"); + gtk_widget_set_halign(btn_more, GTK_ALIGN_CENTER); + gtk_widget_set_margin_top(btn_more, 4); + gtk_widget_set_margin_bottom(btn_more, 4); + gtk_widget_set_sensitive(btn_more, FALSE); + gtk_box_pack_start(GTK_BOX(vbox), btn_more, FALSE, FALSE, 0); + g_signal_connect(btn_more, "clicked", G_CALLBACK(on_more_clicked), app); + + // Download button (default action) + GtkWidget *btn_download = gtk_button_new_with_label("Download"); + gtk_widget_set_can_default(btn_download, TRUE); + gtk_box_pack_start(GTK_BOX(vbox), btn_download, FALSE, TRUE, 0); + g_signal_connect(btn_download, "clicked", G_CALLBACK(on_download_clicked), app); + + // Status bar row with page counter on right + GtkWidget *status_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); + gtk_box_pack_start(GTK_BOX(vbox), status_box, FALSE, TRUE, 0); + + GtkWidget *statusbar = gtk_statusbar_new(); + gtk_box_pack_start(GTK_BOX(status_box), statusbar, TRUE, TRUE, 0); + + GtkWidget *lbl_page_count = gtk_label_new("pages 0/0"); + gtk_widget_set_margin_end(lbl_page_count, 8); + gtk_box_pack_start(GTK_BOX(status_box), lbl_page_count, FALSE, FALSE, 0); + + // Store widget references in app + app->window = window; + app->combo_proxy = combo_proxy; + app->entry_search = entry_search; + app->treeview_results = treeview; + app->btn_search = btn_search; + app->btn_refresh = btn_refresh; + app->btn_download = btn_download; + app->btn_more = btn_more; + app->lbl_page_count = lbl_page_count; + app->statusbar = statusbar; + + // Set download as the default button + gtk_window_set_default(GTK_WINDOW(window), btn_download); + + return window; +} diff --git a/src/gui/window.h b/src/gui/window.h new file mode 100644 index 0000000..0749539 --- /dev/null +++ b/src/gui/window.h @@ -0,0 +1,31 @@ +/* + * Scallywag-NG: Half the scally, and twice the wag. + * Copyright (C) 2025 SILO GROUP LLC + * + * 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 . + */ + +#ifndef SCALLYWAG_WINDOW_H +#define SCALLYWAG_WINDOW_H + +#include + +// Forward declare App struct (defined in app.h) +struct App; + +// Create and return the main window +// Also sets up all widgets and stores references in App +GtkWidget *window_create(struct App *app); + +#endif diff --git a/src/http.c b/src/http.c new file mode 100644 index 0000000..3e66a86 --- /dev/null +++ b/src/http.c @@ -0,0 +1,128 @@ +/* + * Scallywag-NG: Half the scally, and twice the wag. + * Copyright (C) 2025 SILO GROUP LLC + * + * 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 . + */ + +#include "http.h" +#include "util/log.h" +#include +#include +#include + +#define USER_AGENT "Scallywag/1.0" + +// Global curl handle for URL encoding +static CURL *g_curl_encode = NULL; + +static size_t write_callback(void *contents, size_t size, size_t nmemb, void *userp) { + size_t realsize = size * nmemb; + HttpResponse *response = (HttpResponse *)userp; + + char *ptr = realloc(response->data, response->size + realsize + 1); + if (!ptr) { + LOG_ERROR("Failed to allocate memory for HTTP response"); + return 0; + } + + response->data = ptr; + memcpy(&response->data[response->size], contents, realsize); + response->size += realsize; + response->data[response->size] = '\0'; + + return realsize; +} + +int http_global_init(void) { + CURLcode res = curl_global_init(CURL_GLOBAL_DEFAULT); + if (res != CURLE_OK) { + LOG_ERROR("Failed to initialize curl: %s", curl_easy_strerror(res)); + return -1; + } + + g_curl_encode = curl_easy_init(); + if (!g_curl_encode) { + LOG_ERROR("Failed to create curl handle for encoding"); + curl_global_cleanup(); + return -1; + } + + return 0; +} + +void http_global_cleanup(void) { + if (g_curl_encode) { + curl_easy_cleanup(g_curl_encode); + g_curl_encode = NULL; + } + curl_global_cleanup(); +} + +HttpResponse *http_get(const char *url) { + if (!url) return NULL; + + HttpResponse *response = malloc(sizeof(HttpResponse)); + if (!response) { + LOG_ERROR("Failed to allocate HTTP response"); + return NULL; + } + response->data = NULL; + response->size = 0; + + CURL *curl = curl_easy_init(); + if (!curl) { + LOG_ERROR("Failed to create curl handle"); + free(response); + return NULL; + } + + curl_easy_setopt(curl, CURLOPT_URL, url); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, response); + curl_easy_setopt(curl, CURLOPT_USERAGENT, USER_AGENT); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L); + + CURLcode res = curl_easy_perform(curl); + if (res != CURLE_OK) { + LOG_ERROR("HTTP request failed for '%s': %s", url, curl_easy_strerror(res)); + http_response_free(response); + curl_easy_cleanup(curl); + return NULL; + } + + long http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + if (http_code < 200 || http_code >= 300) { + LOG_ERROR("HTTP request returned status %ld for '%s'", http_code, url); + http_response_free(response); + curl_easy_cleanup(curl); + return NULL; + } + + curl_easy_cleanup(curl); + return response; +} + +void http_response_free(HttpResponse *response) { + if (!response) return; + free(response->data); + free(response); +} + +char *http_url_encode(const char *str) { + if (!str || !g_curl_encode) return NULL; + return curl_easy_escape(g_curl_encode, str, 0); +} diff --git a/src/http.h b/src/http.h new file mode 100644 index 0000000..2caf2ee --- /dev/null +++ b/src/http.h @@ -0,0 +1,46 @@ +/* + * Scallywag-NG: Half the scally, and twice the wag. + * Copyright (C) 2025 SILO GROUP LLC + * + * 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 . + */ + +#ifndef SCALLYWAG_HTTP_H +#define SCALLYWAG_HTTP_H + +#include + +typedef struct { + char *data; + size_t size; +} HttpResponse; + +// Initialize curl globally (call once at startup) +int http_global_init(void); + +// Cleanup curl globally (call once at shutdown) +void http_global_cleanup(void); + +// Perform GET request, returns NULL on failure +// Caller must free with http_response_free() +HttpResponse *http_get(const char *url); + +// Free HTTP response +void http_response_free(HttpResponse *response); + +// URL encode a string (uses curl) +// Caller must free with curl_free() +char *http_url_encode(const char *str); + +#endif diff --git a/src/scraper/proxylister.c b/src/scraper/proxylister.c new file mode 100644 index 0000000..0b2eb3d --- /dev/null +++ b/src/scraper/proxylister.c @@ -0,0 +1,169 @@ +/* + * Scallywag-NG: Half the scally, and twice the wag. + * Copyright (C) 2025 SILO GROUP LLC + * + * 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 . + */ + +#include "proxylister.h" +#include "../app.h" +#include "../http.h" +#include "../util/log.h" +#include "../util/strutil.h" +#include +#include +#include +#include + +#define INITIAL_CAPACITY 16 +#define PROXY_XPATH "//body[@id='mainPage']/div[@class='container']/div[@id='content']/table[@id='searchResult']/tr/td[@class='site']/a/text()" + +ProxyList *proxy_list_new(void) { + ProxyList *list = malloc(sizeof(ProxyList)); + if (!list) return NULL; + + list->proxies = malloc(sizeof(char *) * INITIAL_CAPACITY); + if (!list->proxies) { + free(list); + return NULL; + } + + list->count = 0; + list->capacity = INITIAL_CAPACITY; + return list; +} + +void proxy_list_append(ProxyList *list, const char *proxy) { + if (!list || !proxy) return; + + if (list->count >= list->capacity) { + size_t new_capacity = list->capacity * 2; + char **new_proxies = realloc(list->proxies, sizeof(char *) * new_capacity); + if (!new_proxies) { + LOG_ERROR("Failed to grow proxy list"); + return; + } + list->proxies = new_proxies; + list->capacity = new_capacity; + } + + list->proxies[list->count] = str_dup(proxy); + if (list->proxies[list->count]) { + list->count++; + } +} + +void proxy_list_free(ProxyList *list) { + if (!list) return; + + for (size_t i = 0; i < list->count; i++) { + free(list->proxies[i]); + } + free(list->proxies); + free(list); +} + +ProxyList *proxylister_fetch(const char *proxylist_url) { + if (!proxylist_url) return NULL; + + LOG_INFO("Fetching proxies from '%s'", proxylist_url); + app_loading_log("[INFO] Connecting to proxy list server..."); + app_loading_pulse(); + + HttpResponse *response = http_get(proxylist_url); + if (!response) { + LOG_ERROR("Failed to fetch proxy list"); + app_loading_log("[ERROR] Failed to fetch proxy list - connection error"); + return NULL; + } + + app_loading_log("[OK] Received response, parsing HTML..."); + app_loading_pulse(); + + // Parse HTML + htmlDocPtr doc = htmlReadMemory(response->data, (int)response->size, NULL, NULL, + HTML_PARSE_RECOVER | HTML_PARSE_NOERROR | HTML_PARSE_NOWARNING); + + http_response_free(response); + + if (!doc) { + LOG_ERROR("Failed to parse proxy list HTML"); + app_loading_log("[ERROR] Failed to parse HTML response"); + return NULL; + } + + // Create XPath context + xmlXPathContextPtr ctx = xmlXPathNewContext(doc); + if (!ctx) { + LOG_ERROR("Failed to create XPath context"); + app_loading_log("[ERROR] XPath initialization failed"); + xmlFreeDoc(doc); + return NULL; + } + + app_loading_log("[INFO] Extracting proxy addresses..."); + app_loading_pulse(); + + // Evaluate XPath + xmlXPathObjectPtr result = xmlXPathEvalExpression((xmlChar *)PROXY_XPATH, ctx); + if (!result) { + LOG_ERROR("Failed to evaluate XPath"); + app_loading_log("[ERROR] XPath query failed"); + xmlXPathFreeContext(ctx); + xmlFreeDoc(doc); + return NULL; + } + + ProxyList *list = proxy_list_new(); + if (!list) { + xmlXPathFreeObject(result); + xmlXPathFreeContext(ctx); + xmlFreeDoc(doc); + return NULL; + } + + // Extract proxy URLs from result + if (result->nodesetval) { + for (int i = 0; i < result->nodesetval->nodeNr; i++) { + xmlNodePtr node = result->nodesetval->nodeTab[i]; + if (node->type == XML_TEXT_NODE && node->content) { + char *proxy = (char *)node->content; + // Trim whitespace + char *trimmed = str_trim(str_dup(proxy)); + if (trimmed && strlen(trimmed) > 0) { + LOG_INFO("Available proxy: %s", trimmed); + char msg[256]; + snprintf(msg, sizeof(msg), "[+] Found proxy: %s", trimmed); + app_loading_log(msg); + app_loading_pulse(); + proxy_list_append(list, trimmed); + } + free(trimmed); + } + } + } + + xmlXPathFreeObject(result); + xmlXPathFreeContext(ctx); + xmlFreeDoc(doc); + + if (list->count == 0) { + LOG_WARN("No proxies found"); + app_loading_log("[WARN] No proxies found in response"); + } else { + LOG_INFO("Found %zu proxies", list->count); + } + + return list; +} diff --git a/src/scraper/proxylister.h b/src/scraper/proxylister.h new file mode 100644 index 0000000..405a057 --- /dev/null +++ b/src/scraper/proxylister.h @@ -0,0 +1,43 @@ +/* + * Scallywag-NG: Half the scally, and twice the wag. + * Copyright (C) 2025 SILO GROUP LLC + * + * 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 . + */ + +#ifndef SCALLYWAG_PROXYLISTER_H +#define SCALLYWAG_PROXYLISTER_H + +#include + +typedef struct { + char **proxies; + size_t count; + size_t capacity; +} ProxyList; + +// Create empty proxy list +ProxyList *proxy_list_new(void); + +// Append proxy to list +void proxy_list_append(ProxyList *list, const char *proxy); + +// Free proxy list +void proxy_list_free(ProxyList *list); + +// Fetch proxy list from URL +// Returns NULL on failure +ProxyList *proxylister_fetch(const char *proxylist_url); + +#endif diff --git a/src/scraper/searcher.c b/src/scraper/searcher.c new file mode 100644 index 0000000..f63010a --- /dev/null +++ b/src/scraper/searcher.c @@ -0,0 +1,348 @@ +/* + * Scallywag-NG: Half the scally, and twice the wag. + * Copyright (C) 2025 SILO GROUP LLC + * + * 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 . + */ + +#include "searcher.h" +#include "../http.h" +#include "../util/log.h" +#include "../util/strutil.h" +#include +#include +#include +#include +#include +#include + +#define INITIAL_CAPACITY 32 +#define RESULTS_XPATH "//table[@id='searchResult']/tr" +#define MAGNET_XPATH "//div[@class='download']/a/@href" + +// Helper to evaluate XPath and get first text result +static char *xpath_get_text(xmlXPathContextPtr ctx, const char *xpath) { + xmlXPathObjectPtr result = xmlXPathEvalExpression((xmlChar *)xpath, ctx); + if (!result) return NULL; + + char *text = NULL; + if (result->nodesetval && result->nodesetval->nodeNr > 0) { + xmlNodePtr node = result->nodesetval->nodeTab[0]; + if (node->type == XML_TEXT_NODE && node->content) { + text = str_dup((char *)node->content); + } else if (node->type == XML_ATTRIBUTE_NODE && node->children && node->children->content) { + text = str_dup((char *)node->children->content); + } + } + + xmlXPathFreeObject(result); + return text; +} + +Result *result_new(const char *title, int seeders, int leechers, + const char *size, const char *author, const char *url) { + Result *r = malloc(sizeof(Result)); + if (!r) return NULL; + + r->title = str_dup(title ? title : ""); + r->seeders = seeders; + r->leechers = leechers; + r->size = str_dup(size ? size : ""); + r->author = str_dup(author ? author : ""); + r->url = str_dup(url ? url : ""); + + return r; +} + +void result_free(Result *result) { + if (!result) return; + free(result->title); + free(result->size); + free(result->author); + free(result->url); + free(result); +} + +ResultList *result_list_new(void) { + ResultList *list = malloc(sizeof(ResultList)); + if (!list) return NULL; + + list->items = malloc(sizeof(Result) * INITIAL_CAPACITY); + if (!list->items) { + free(list); + return NULL; + } + + list->count = 0; + list->capacity = INITIAL_CAPACITY; + list->total_pages = 1; + return list; +} + +void result_list_append(ResultList *list, Result *result) { + if (!list || !result) return; + + if (list->count >= list->capacity) { + size_t new_capacity = list->capacity * 2; + Result *new_items = realloc(list->items, sizeof(Result) * new_capacity); + if (!new_items) { + LOG_ERROR("Failed to grow result list"); + return; + } + list->items = new_items; + list->capacity = new_capacity; + } + + // Copy result data into list + list->items[list->count] = *result; + list->count++; + + // Free the original result container (but not its strings, now owned by list) + free(result); +} + +void result_list_clear(ResultList *list) { + if (!list) return; + + for (size_t i = 0; i < list->count; i++) { + free(list->items[i].title); + free(list->items[i].size); + free(list->items[i].author); + free(list->items[i].url); + } + list->count = 0; +} + +void result_list_free(ResultList *list) { + if (!list) return; + result_list_clear(list); + free(list->items); + free(list); +} + +ResultList *searcher_search(const char *proxy, const char *query, int page) { + if (!proxy || !query) return NULL; + if (page < 1) page = 1; + + // URL encode query + char *encoded_query = http_url_encode(query); + if (!encoded_query) { + LOG_ERROR("Failed to URL encode query"); + return NULL; + } + + // Build URL: https://{proxy}/search/{query}/{page}/99/0 + char url[1024]; + snprintf(url, sizeof(url), "https://%s/search/%s/%d/99/0", proxy, encoded_query, page); + curl_free(encoded_query); + + LOG_INFO("Searching: %s", url); + + HttpResponse *response = http_get(url); + if (!response) { + LOG_ERROR("Failed to fetch search results"); + return NULL; + } + + // Parse HTML + htmlDocPtr doc = htmlReadMemory(response->data, (int)response->size, NULL, NULL, + HTML_PARSE_RECOVER | HTML_PARSE_NOERROR | HTML_PARSE_NOWARNING); + + http_response_free(response); + + if (!doc) { + LOG_ERROR("Failed to parse search results HTML"); + return NULL; + } + + // Create XPath context + xmlXPathContextPtr ctx = xmlXPathNewContext(doc); + if (!ctx) { + LOG_ERROR("Failed to create XPath context"); + xmlFreeDoc(doc); + return NULL; + } + + // Get result rows + xmlXPathObjectPtr rows = xmlXPathEvalExpression((xmlChar *)RESULTS_XPATH, ctx); + if (!rows || !rows->nodesetval) { + LOG_WARN("No results found"); + if (rows) xmlXPathFreeObject(rows); + xmlXPathFreeContext(ctx); + xmlFreeDoc(doc); + return result_list_new(); // Return empty list + } + + ResultList *list = result_list_new(); + if (!list) { + xmlXPathFreeObject(rows); + xmlXPathFreeContext(ctx); + xmlFreeDoc(doc); + return NULL; + } + + int num_rows = rows->nodesetval->nodeNr; + // Skip last row (navigation hack from Python code) + if (num_rows > 1) num_rows--; + + for (int i = 0; i < num_rows; i++) { + xmlNodePtr row = rows->nodesetval->nodeTab[i]; + + // Set context node to current row + ctx->node = row; + + // Extract fields using relative XPath + char *title = xpath_get_text(ctx, "td[2]/div[1]/a[1]/text()"); + char *seeders_str = xpath_get_text(ctx, "td[3]/text()"); + char *leechers_str = xpath_get_text(ctx, "td[4]/text()"); + char *url_val = xpath_get_text(ctx, "td/div[@class='detName']/a[@class='detLink']/@href"); + char *size_raw = xpath_get_text(ctx, "td[2]/font/text()"); + // Try registered user (in tag) first, then anonymous (in tag) + char *author_raw = xpath_get_text(ctx, "td[2]/font[@class='detDesc']/a/text()"); + int is_anonymous = 0; + if (!author_raw) { + author_raw = xpath_get_text(ctx, "td[2]/font[@class='detDesc']/i/text()"); + is_anonymous = 1; + } + + // Parse integers + int seeders = seeders_str ? atoi(seeders_str) : 0; + int leechers = leechers_str ? atoi(leechers_str) : 0; + + // Extract size from "Size XXX," pattern + char *size = str_extract_size(size_raw); + + // Decode HTML entities in title and author + char *title_decoded = str_decode_html_entities(title); + char *author_decoded = str_decode_html_entities(author_raw); + + // Format author with Pango markup (italic for anonymous) + char *author = NULL; + if (author_decoded || author_raw) { + const char *author_text = author_decoded ? author_decoded : author_raw; + char *escaped = str_escape_markup(author_text); + if (escaped) { + if (is_anonymous) { + // Wrap in bold+italic tags for anonymous + size_t len = strlen(escaped) + 15; // + null + author = malloc(len); + if (author) { + snprintf(author, len, "%s", escaped); + } + free(escaped); + } else { + author = escaped; // Transfer ownership + } + } + } + + // Create result + if (title && url_val) { + Result *result = result_new( + title_decoded ? title_decoded : title, + seeders, leechers, size, + author, // Already formatted with Pango markup + url_val); + if (result) { + LOG_DEBUG("Result: %s (S:%d L:%d)", title_decoded ? title_decoded : title, seeders, leechers); + result_list_append(list, result); + } + } + + // Free temporary strings + free(title); + free(title_decoded); + free(seeders_str); + free(leechers_str); + free(url_val); + free(size_raw); + free(size); + free(author_raw); + free(author_decoded); + free(author); + } + + xmlXPathFreeObject(rows); + + // Parse total pages from pagination navbar + // Find the row with colspan=9, then get all text nodes that are numbers + ctx->node = xmlDocGetRootElement(doc); + xmlXPathObjectPtr pager = xmlXPathEvalExpression( + (xmlChar *)"//td[@colspan='9']/b/text() | //td[@colspan='9']/a/text()", ctx); + + if (pager && pager->nodesetval) { + int max_page = 1; + for (int i = 0; i < pager->nodesetval->nodeNr; i++) { + xmlNodePtr node = pager->nodesetval->nodeTab[i]; + if (node->content) { + int page_num = atoi((const char *)node->content); + if (page_num > max_page) { + max_page = page_num; + } + } + } + list->total_pages = max_page; + xmlXPathFreeObject(pager); + } + + xmlXPathFreeContext(ctx); + xmlFreeDoc(doc); + + LOG_INFO("Found %zu results, %d total pages", list->count, list->total_pages); + return list; +} + +char *searcher_get_magnet(const char *url) { + if (!url) return NULL; + + LOG_INFO("Fetching magnet from: %s", url); + + HttpResponse *response = http_get(url); + if (!response) { + LOG_ERROR("Failed to fetch torrent page"); + return NULL; + } + + // Parse HTML + htmlDocPtr doc = htmlReadMemory(response->data, (int)response->size, NULL, NULL, + HTML_PARSE_RECOVER | HTML_PARSE_NOERROR | HTML_PARSE_NOWARNING); + + http_response_free(response); + + if (!doc) { + LOG_ERROR("Failed to parse torrent page HTML"); + return NULL; + } + + // Create XPath context + xmlXPathContextPtr ctx = xmlXPathNewContext(doc); + if (!ctx) { + xmlFreeDoc(doc); + return NULL; + } + + // Get magnet link + char *magnet = xpath_get_text(ctx, MAGNET_XPATH); + + xmlXPathFreeContext(ctx); + xmlFreeDoc(doc); + + if (magnet) { + LOG_INFO("Found magnet link"); + } else { + LOG_WARN("No magnet link found"); + } + + return magnet; +} diff --git a/src/scraper/searcher.h b/src/scraper/searcher.h new file mode 100644 index 0000000..7cbe7a1 --- /dev/null +++ b/src/scraper/searcher.h @@ -0,0 +1,60 @@ +/* + * Scallywag-NG: Half the scally, and twice the wag. + * Copyright (C) 2025 SILO GROUP LLC + * + * 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 . + */ + +#ifndef SCALLYWAG_SEARCHER_H +#define SCALLYWAG_SEARCHER_H + +#include + +typedef struct { + char *title; + int seeders; + int leechers; + char *size; + char *author; + char *url; +} Result; + +typedef struct { + Result *items; + size_t count; + size_t capacity; + int total_pages; +} ResultList; + +// Result lifecycle +Result *result_new(const char *title, int seeders, int leechers, + const char *size, const char *author, const char *url); +void result_free(Result *result); + +// ResultList lifecycle +ResultList *result_list_new(void); +void result_list_append(ResultList *list, Result *result); +void result_list_clear(ResultList *list); +void result_list_free(ResultList *list); + +// Search for torrents on a proxy +// page starts at 1 +// Returns NULL on failure +ResultList *searcher_search(const char *proxy, const char *query, int page); + +// Get magnet link from torrent detail page +// Caller must free the returned string +char *searcher_get_magnet(const char *url); + +#endif diff --git a/src/util/log.h b/src/util/log.h new file mode 100644 index 0000000..4414ae4 --- /dev/null +++ b/src/util/log.h @@ -0,0 +1,34 @@ +/* + * Scallywag-NG: Half the scally, and twice the wag. + * Copyright (C) 2025 SILO GROUP LLC + * + * 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 . + */ + +#ifndef SCALLYWAG_LOG_H +#define SCALLYWAG_LOG_H + +#include + +#define LOG_ERROR(fmt, ...) fprintf(stderr, "[ERROR] " fmt "\n", ##__VA_ARGS__) +#define LOG_WARN(fmt, ...) fprintf(stderr, "[WARN] " fmt "\n", ##__VA_ARGS__) +#define LOG_INFO(fmt, ...) fprintf(stdout, "[INFO] " fmt "\n", ##__VA_ARGS__) + +#ifdef DEBUG +#define LOG_DEBUG(fmt, ...) fprintf(stdout, "[DEBUG] " fmt "\n", ##__VA_ARGS__) +#else +#define LOG_DEBUG(fmt, ...) ((void)0) +#endif + +#endif diff --git a/src/util/strutil.c b/src/util/strutil.c new file mode 100644 index 0000000..207271e --- /dev/null +++ b/src/util/strutil.c @@ -0,0 +1,190 @@ +/* + * Scallywag-NG: Half the scally, and twice the wag. + * Copyright (C) 2025 SILO GROUP LLC + * + * 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 . + */ + +#include "strutil.h" +#include +#include +#include + +char *str_dup(const char *s) { + if (!s) return NULL; + + size_t len = strlen(s) + 1; + char *copy = malloc(len); + if (!copy) return NULL; + + memcpy(copy, s, len); + return copy; +} + +char *str_extract_size(const char *text) { + if (!text) return NULL; + + const char *start = strstr(text, "Size "); + if (!start) return NULL; + start += 5; // skip "Size " + + const char *end = strchr(start, ','); + if (!end) return NULL; + + size_t len = end - start; + char *result = malloc(len + 1); + if (!result) return NULL; + + memcpy(result, start, len); + result[len] = '\0'; + return result; +} + +char *str_trim(char *s) { + if (!s) return NULL; + + // Trim leading whitespace + while (isspace((unsigned char)*s)) s++; + + if (*s == '\0') return s; + + // Trim trailing whitespace + char *end = s + strlen(s) - 1; + while (end > s && isspace((unsigned char)*end)) end--; + end[1] = '\0'; + + return s; +} + +// Named HTML entities +static const struct { + const char *name; + char replacement; +} html_entities[] = { + {"amp", '&'}, + {"lt", '<'}, + {"gt", '>'}, + {"quot", '"'}, + {"apos", '\''}, + {"nbsp", ' '}, + {NULL, 0} +}; + +char *str_decode_html_entities(const char *s) { + if (!s) return NULL; + + size_t len = strlen(s); + // Allocate enough space (decoded is always <= original) + char *result = malloc(len + 1); + if (!result) return NULL; + + const char *src = s; + char *dst = result; + + while (*src) { + if (*src == '&') { + const char *entity_start = src + 1; + const char *semicolon = strchr(entity_start, ';'); + + // Check for valid entity (semicolon within reasonable distance) + if (semicolon && (semicolon - entity_start) <= 10) { + size_t entity_len = semicolon - entity_start; + int decoded = 0; + + // Numeric entity: &#NNN; or &#xHHH; + if (*entity_start == '#') { + long codepoint = 0; + if (entity_start[1] == 'x' || entity_start[1] == 'X') { + // Hexadecimal + codepoint = strtol(entity_start + 2, NULL, 16); + } else { + // Decimal + codepoint = strtol(entity_start + 1, NULL, 10); + } + if (codepoint > 0 && codepoint < 128) { + *dst++ = (char)codepoint; + decoded = 1; + } else if (codepoint >= 128 && codepoint < 0x10000) { + // UTF-8 encode (simplified for BMP) + if (codepoint < 0x80) { + *dst++ = (char)codepoint; + } else if (codepoint < 0x800) { + *dst++ = (char)(0xC0 | (codepoint >> 6)); + *dst++ = (char)(0x80 | (codepoint & 0x3F)); + } else { + *dst++ = (char)(0xE0 | (codepoint >> 12)); + *dst++ = (char)(0x80 | ((codepoint >> 6) & 0x3F)); + *dst++ = (char)(0x80 | (codepoint & 0x3F)); + } + decoded = 1; + } + } else { + // Named entity + for (int i = 0; html_entities[i].name; i++) { + if (strlen(html_entities[i].name) == entity_len && + strncmp(entity_start, html_entities[i].name, entity_len) == 0) { + *dst++ = html_entities[i].replacement; + decoded = 1; + break; + } + } + } + + if (decoded) { + src = semicolon + 1; + continue; + } + } + } + *dst++ = *src++; + } + *dst = '\0'; + + return result; +} + +char *str_escape_markup(const char *s) { + if (!s) return NULL; + + // Count characters that need escaping + size_t extra = 0; + for (const char *p = s; *p; p++) { + if (*p == '&') extra += 4; // & -> & (4 extra) + else if (*p == '<') extra += 3; // < -> < (3 extra) + else if (*p == '>') extra += 3; // > -> > (3 extra) + } + + size_t len = strlen(s); + char *result = malloc(len + extra + 1); + if (!result) return NULL; + + char *dst = result; + for (const char *src = s; *src; src++) { + if (*src == '&') { + memcpy(dst, "&", 5); + dst += 5; + } else if (*src == '<') { + memcpy(dst, "<", 4); + dst += 4; + } else if (*src == '>') { + memcpy(dst, ">", 4); + dst += 4; + } else { + *dst++ = *src; + } + } + *dst = '\0'; + + return result; +} diff --git a/src/util/strutil.h b/src/util/strutil.h new file mode 100644 index 0000000..91a7b0a --- /dev/null +++ b/src/util/strutil.h @@ -0,0 +1,42 @@ +/* + * Scallywag-NG: Half the scally, and twice the wag. + * Copyright (C) 2025 SILO GROUP LLC + * + * 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 . + */ + +#ifndef SCALLYWAG_STRUTIL_H +#define SCALLYWAG_STRUTIL_H + +#include + +// Duplicate string (caller must free result) +char *str_dup(const char *s); + +// Extract substring matching "Size (.+?)," pattern +// Returns allocated string or NULL +char *str_extract_size(const char *text); + +// Trim leading/trailing whitespace (in-place) +char *str_trim(char *s); + +// Decode HTML entities in string (returns new allocated string) +// Handles & < > " ' &#NNN; &#xHHH; +char *str_decode_html_entities(const char *s); + +// Escape string for Pango markup (returns new allocated string) +// Escapes <, >, & characters +char *str_escape_markup(const char *s); + +#endif