commit f9347f37cc6626afdf8571c06c183c844b8b3271 Author: codestation Date: Sun Aug 11 17:12:00 2013 -0430 First commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d3fec56 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +qcma.pro.user* diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8fe9ee1 --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +QCMA +==== + +QCMA is an cross-platform application to provide a Open Source implementation +of the original Content Manager Assistant that comes with the PS Vita. QCMA is +meant to be compatible with Linux, Windows and MAC OS X. + +## Features + +The aim of this project is to provide a implementation that is on par with the +official CMA and also offer some features missing in the original one. + +#### Implemented features missing in OpenCMA (Yifan Lu CLI application) +* Metadata for PSP savedatas. +* Basic metadata for single songs (album, artist, title). +* Basic metadata for videos (duration, dimensions). +* Basic metadata for photos (dimensions). +* Easy wireless pairing (show PIN to the user when a Vita is detected). +* Ability to restart the connection if the Vita is reconnected (on test). +* Ability to change the connection mode (usb/wireless) on the fly (on test). + +#### TODO: +* Fix remaining bugs with thread synchronizations. +* Implement thumbnails for videos and album art for music. +* Folder categories for music/videos. +* SQLite backend for database. +* Fix wireless streaming for music/videos. + +## Planned features +* **Backup browser**: provide a human readable listing of the games saved on the +computer (name, icon, side on disk) and provide some actions (delete savedata, +patches or the whole game without need of the Vita). Also some sort of interface +to prepare VHBL homebrew in the folder of the exploited savedata. + +* **DLNA bridge**: connect an existing DLNA server to interface with the Vita +using the wireless streaming feature. + +#### Dependencies +* [Qt 4.x or 5.x](http://qt-project.org/) + +* [VitaMTP](https://github.com/yifanlu/VitaMTP) + +* [MediaInfo](http://mediaarea.net/en/MediaInfo) + + +#### Where do I get the source code? +Check the GitHub repo here: https://github.com/codestation/qcma + +#### I want to contribute +Contact me on [GitHub](https://github.com/codestation/) + +#### Thanks to +[Yifan Lu](https://github.com/yifanlu/vitamtp/) - for the vitamtp library and +the reference implementation of OpenCMA. + +#### License +GPL v3: since some parts of QCMA are based on the reference implementation of +OpenCMA. diff --git a/baseworker.cpp b/baseworker.cpp new file mode 100644 index 0000000..ae161f2 --- /dev/null +++ b/baseworker.cpp @@ -0,0 +1,57 @@ +/* + * QCMA: Cross-platform content manager assistant for the PS Vita + * + * Copyright (C) 2013 Codestation + * + * 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 "baseworker.h" + +BaseWorker::BaseWorker(QObject *parent) : + QObject(parent) +{ + thread = NULL; +} + +void BaseWorker::onFinished() +{ +} + +void BaseWorker::start() +{ + thread = new QThread(); + + // Move this service to a new thread + this->moveToThread(thread); + + // The main loop will be executed when the thread + // signals that it has started. + connect(thread, SIGNAL(started()), this, SLOT(process())); + + // Make sure that we notify ourselves when the thread + // is finished in order to correctly clean-up the thread. + connect(thread, SIGNAL(finished()), this, SLOT(onFinished())); + + // The thread will quit when the sercives + // signals that it's finished. + connect(this, SIGNAL(finished()), thread, SLOT(quit())); + + // The thread will be scheduled for deletion when the + // service signals that it's finished + connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater())); + + // Start the thread + thread->start(); +} diff --git a/baseworker.h b/baseworker.h new file mode 100644 index 0000000..ccbf9a0 --- /dev/null +++ b/baseworker.h @@ -0,0 +1,48 @@ +/* + * QCMA: Cross-platform content manager assistant for the PS Vita + * + * Copyright (C) 2013 Codestation + * + * 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 BASEWORKER_H +#define BASEWORKER_H + +#include +#include + +class BaseWorker : public QObject +{ + Q_OBJECT + +public: + explicit BaseWorker(QObject *parent = 0); + + void start(); + +private: + QThread *thread; + +signals: + void finished(); + +protected slots: + virtual void process() = 0; + +private slots: + virtual void onFinished(); +}; + +#endif // BASEWORKER_H diff --git a/capability.cpp b/capability.cpp new file mode 100644 index 0000000..3aebed3 --- /dev/null +++ b/capability.cpp @@ -0,0 +1,112 @@ +/* + * QCMA: Cross-platform content manager assistant for the PS Vita + * + * Copyright (C) 2013 Codestation + * + * 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 "capability.h" + +#include +#include + +bool DeviceCapability::exchangeInfo(vita_device_t *device) +{ + if(VitaMTP_GetVitaInfo(device, &vita_info) != PTP_RC_OK) { + qWarning("Cannot retreve device information."); + return false; + } + + if(vita_info.protocolVersion > VITAMTP_PROTOCOL_MAX_VERSION) { + qWarning("Vita wants protocol version %08d while we only support %08d. Attempting to continue.", + vita_info.protocolVersion, VITAMTP_PROTOCOL_MAX_VERSION); + } + + QString hostname = QHostInfo::localHostName(); + const initiator_info_t *pc_info = VitaMTP_Data_Initiator_New(hostname.toUtf8().data(), vita_info.protocolVersion); + + // Next, we send the client's (this program) info (discard the const here) + if(VitaMTP_SendInitiatorInfo(device, (initiator_info_t *)pc_info) != PTP_RC_OK) { + qWarning("Cannot send host information."); + return false; + } + + if(vita_info.protocolVersion >= VITAMTP_PROTOCOL_FW_2_10) { + // Get the device's capabilities + capability_info_t *vita_capabilities; + + if(VitaMTP_GetVitaCapabilityInfo(device, &vita_capabilities) != PTP_RC_OK) { + qWarning("Failed to get capability information from Vita."); + return false; + } + + VitaMTP_Data_Free_Capability(vita_capabilities); // TODO: Use this data + // Send the host's capabilities + capability_info_t *pc_capabilities = generate_pc_capability_info(); + + if(VitaMTP_SendPCCapabilityInfo(device, pc_capabilities) != PTP_RC_OK) { + qWarning("Failed to send capability information to Vita."); + free_pc_capability_info(pc_capabilities); + return false; + } + + free_pc_capability_info(pc_capabilities); + } + + // Finally, we tell the Vita we are connected + if(VitaMTP_SendHostStatus(device, VITA_HOST_STATUS_Connected) != PTP_RC_OK) { + qWarning("Cannot send host status."); + return false; + } + + VitaMTP_Data_Free_Initiator(pc_info); + return true; +} + +void DeviceCapability::free_pc_capability_info(capability_info_t *info) +{ + delete &info->functions.formats.next_item[-1]; + delete &info->functions.next_item[-1]; + delete info; +} + +capability_info_t *DeviceCapability::generate_pc_capability_info() +{ + typedef capability_info::capability_info_function tfunction; + typedef tfunction::capability_info_format tformat; + + // TODO: Actually do this based on QCMA capabilities + capability_info_t *pc_capabilities = new capability_info_t; + pc_capabilities->version = "1.0"; + tfunction *functions = new tfunction[3](); + tformat *game_formats = new tformat[5](); + game_formats[0].contentType = "vitaApp"; + game_formats[0].next_item = &game_formats[1]; + game_formats[1].contentType = "PSPGame"; + game_formats[1].next_item = &game_formats[2]; + game_formats[2].contentType = "PSPSaveData"; + game_formats[2].next_item = &game_formats[3]; + game_formats[3].contentType = "PSGame"; + game_formats[3].next_item = &game_formats[4]; + game_formats[4].contentType = "PSMApp"; + functions[0].type = "game"; + functions[0].formats = game_formats[0]; + functions[0].next_item = &functions[1]; + functions[1].type = "backup"; + functions[1].next_item = &functions[2]; + functions[2].type = "systemUpdate"; + pc_capabilities->functions = functions[0]; + return pc_capabilities; +} diff --git a/capability.h b/capability.h new file mode 100644 index 0000000..ada79a1 --- /dev/null +++ b/capability.h @@ -0,0 +1,46 @@ +/* + * QCMA: Cross-platform content manager assistant for the PS Vita + * + * Copyright (C) 2013 Codestation + * + * 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 CAPABILITY_H +#define CAPABILITY_H + +extern "C" { +#include +} + +class DeviceCapability +{ +public: + explicit DeviceCapability() {} + bool exchangeInfo(vita_device_t *device); + + //TODO: vita_info_t doesn't retrieve this info, update vitamtp to get it + const char *getVersion() { return ""; } + const char *getProtocol() { return ""; } + const char *getOnlineId() { return "PS Vita"; } + const char *getModelInfo() { return ""; } + +private: + capability_info_t *generate_pc_capability_info(); + void free_pc_capability_info(capability_info_t *info); + + vita_info_t vita_info; +}; + +#endif // CAPABILITY_H diff --git a/cmaobject.cpp b/cmaobject.cpp new file mode 100644 index 0000000..ed46505 --- /dev/null +++ b/cmaobject.cpp @@ -0,0 +1,300 @@ +/* + * QCMA: Cross-platform content manager assistant for the PS Vita + * + * Copyright (C) 2013 Codestation + * + * 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 "cmaobject.h" +#include "sforeader.h" +#include "utils.h" + +#include +#include +#include + +#include +#include + +int CMAObject::ohfi_count = OHFI_OFFSET; + +CMAObject::CMAObject(CMAObject *obj_parent) : + parent(obj_parent), metadata() +{ +} + +CMAObject::~CMAObject() +{ + free(metadata.name); + free(metadata.path); + + if(MASK_SET(metadata.dataType, SaveData | Folder)) { + free(metadata.data.saveData.title); + free(metadata.data.saveData.detail); + free(metadata.data.saveData.dirName); + free(metadata.data.saveData.savedataTitle); + } else if(MASK_SET(metadata.dataType, Photo | File)) { + free(metadata.data.photo.title); + free(metadata.data.photo.fileName); + delete metadata.data.photo.tracks; + } else if(MASK_SET(metadata.dataType, Music | File)) { + free(metadata.data.music.title); + free(metadata.data.music.fileName); + free(metadata.data.music.album); + free(metadata.data.music.artist); + delete metadata.data.music.tracks; + } else if(MASK_SET(metadata.dataType, Video | File)) { + free(metadata.data.video.title); + free(metadata.data.video.explanation); + free(metadata.data.video.fileName); + free(metadata.data.video.copyright); + delete metadata.data.video.tracks; + } +} + +void CMAObject::loadSfoMetadata(const QString &path) +{ + QString sfo = QDir(path).absoluteFilePath("PARAM.SFO"); + SfoReader reader; + + if(reader.load(sfo)) { + metadata.data.saveData.title = strdup(reader.value("TITLE", "")); + metadata.data.saveData.detail = strdup(reader.value("SAVEDATA_DETAIL", "")); + metadata.data.saveData.savedataTitle = strdup(reader.value("SAVEDATA_TITLE", "")); + metadata.data.saveData.dateTimeUpdated = QFileInfo(sfo).created().toTime_t(); + } else { + metadata.data.saveData.title = strdup(metadata.name); + metadata.data.saveData.detail = strdup(""); + metadata.data.saveData.savedataTitle = strdup(""); + metadata.data.saveData.dateTimeUpdated = 0; + } +} + +void CMAObject::loadMusicMetadata(const QString &path) +{ + MediaInfoLib::MediaInfo media; + if(media.Open(path.toStdWString())) { + QString data; + data = QString::fromStdWString(media.Get(MediaInfoLib::Stream_General, 0, MediaInfoLib::General_Album)); + metadata.data.music.album = strdup(!data.isEmpty() ? data.toUtf8().data() : (parent->metadata.name ? parent->metadata.name : "")); + data = QString::fromStdWString(media.Get(MediaInfoLib::Stream_General, 0, MediaInfoLib::General_Performer)); + metadata.data.music.artist = strdup(data.toUtf8().data()); + data = QString::fromStdWString(media.Get(MediaInfoLib::Stream_General, 0, MediaInfoLib::General_Title)); + metadata.data.music.title = strdup(!data.isEmpty() ? data.toUtf8().data() : metadata.name); + data = QString::fromStdWString(media.Get(MediaInfoLib::Stream_Audio, 0, MediaInfoLib::Audio_BitRate)); + bool ok; + int bitrate = data.toInt(&ok); + if(ok) { + metadata.data.music.tracks->data.track_audio.bitrate = bitrate; + } + } else { + metadata.data.music.album = strdup(parent->metadata.name ? parent->metadata.name : ""); + metadata.data.music.artist = strdup(""); + metadata.data.music.title = strdup(metadata.name); + } +} + +void CMAObject::loadVideoMetadata(const QString &path) +{ + MediaInfoLib::MediaInfo media; + if(media.Open(path.toStdWString())) { + bool ok; + QString data; + data = QString::fromStdWString(media.Get(MediaInfoLib::Stream_General, 0, MediaInfoLib::General_Title)); + metadata.data.video.title = strdup(!data.isEmpty() ? data.toUtf8().data() : metadata.name); + data = QString::fromStdWString(media.Get(MediaInfoLib::Stream_General, 0, MediaInfoLib::General_Comment)); + metadata.data.video.explanation = strdup(data.toUtf8().data()); + data = QString::fromStdWString(media.Get(MediaInfoLib::Stream_General, 0, MediaInfoLib::General_Copyright)); + metadata.data.video.copyright = strdup(data.toUtf8().data()); + data = QString::fromStdWString(media.Get(MediaInfoLib::Stream_Video, 0, MediaInfoLib::Video_BitRate)); + int bitrate = data.toInt(&ok); + if(ok) { + metadata.data.video.tracks->data.track_video.bitrate = bitrate; + } + data = QString::fromStdWString(media.Get(MediaInfoLib::Stream_Video, 0, MediaInfoLib::Video_Duration)); + int duration = data.toInt(&ok); + if(ok) { + metadata.data.video.tracks->data.track_video.duration = duration; + } + data = QString::fromStdWString(media.Get(MediaInfoLib::Stream_Video, 0, MediaInfoLib::Video_Width)); + int width = data.toInt(&ok); + if(ok) { + metadata.data.video.tracks->data.track_video.width = width; + } + data = QString::fromStdWString(media.Get(MediaInfoLib::Stream_Video, 0, MediaInfoLib::Video_Height)); + int height = data.toInt(&ok); + if(ok) { + metadata.data.video.tracks->data.track_video.height = height; + } + } else { + metadata.data.video.title = strdup(metadata.name); + metadata.data.video.explanation = strdup(""); + metadata.data.video.copyright = strdup(""); + } +} + +void CMAObject::loadPhotoMetadata(const QString &path) +{ + MediaInfoLib::MediaInfo media; + if(media.Open(path.toStdWString())) { + bool ok; + QString data; + data = QString::fromStdWString(media.Get(MediaInfoLib::Stream_General, 0, MediaInfoLib::General_Title)); + metadata.data.photo.title = strdup(!data.isEmpty() ? data.toUtf8().data() : metadata.name); + data = QString::fromStdWString(media.Get(MediaInfoLib::Stream_Image, 0, MediaInfoLib::Image_Height)); + int height = data.toInt(&ok); + if(ok) { + metadata.data.photo.tracks->data.track_photo.height = height; + } + int width = data.toInt(&ok); + if(ok) { + metadata.data.photo.tracks->data.track_photo.width = width; + } + } else { + metadata.data.photo.title = strdup(metadata.name); + } +} + +void CMAObject::initObject(const QFileInfo &file) +{ + metadata.name = strdup(file.fileName().toUtf8().data()); + metadata.ohfiParent = parent->metadata.ohfi; + metadata.ohfi = ohfi_count++; + if(metadata.ohfi == 1494) { + metadata.ohfi = metadata.ohfi; + } + metadata.type = VITA_DIR_TYPE_MASK_REGULAR; // ignored for files + metadata.dateTimeCreated = file.created().toTime_t(); + metadata.size = file.size(); + DataType type = file.isFile() ? File : Folder; + metadata.dataType = (DataType)(type | (parent->metadata.dataType & ~Folder)); + + // create additional metadata + if(MASK_SET(metadata.dataType, SaveData | Folder)) { + metadata.data.saveData.dirName = strdup(metadata.name); + metadata.data.saveData.statusType = 1; + loadSfoMetadata(file.absoluteFilePath()); + } else if(MASK_SET(metadata.dataType, Music | File)) { + metadata.data.music.fileName = strdup(metadata.name); + metadata.data.music.fileFormatType = 20; + metadata.data.music.statusType = 1; + metadata.data.music.numTracks = 1; + metadata.data.music.tracks = new media_track(); + metadata.data.music.tracks->type = VITA_TRACK_TYPE_AUDIO; + metadata.data.music.tracks->data.track_photo.codecType = 12; // MP3? + loadMusicMetadata(file.absoluteFilePath()); + } else if(MASK_SET(metadata.dataType, Video | File)) { + metadata.data.video.fileName = strdup(metadata.name); + metadata.data.video.dateTimeUpdated = file.created().toTime_t(); + metadata.data.video.statusType = 1; + metadata.data.video.fileFormatType = 1; + metadata.data.video.parentalLevel = 0; + metadata.data.video.numTracks = 1; + metadata.data.video.tracks = new media_track(); + metadata.data.video.tracks->type = VITA_TRACK_TYPE_VIDEO; + metadata.data.video.tracks->data.track_video.codecType = 3; // this codec is working + loadVideoMetadata(file.absoluteFilePath()); + } else if(MASK_SET(metadata.dataType, Photo | File)) { + metadata.data.photo.fileName = strdup(metadata.name); + metadata.data.photo.fileFormatType = 28; // working + metadata.data.photo.statusType = 1; + metadata.data.photo.dateTimeOriginal = file.created().toTime_t(); + metadata.data.photo.numTracks = 1; + metadata.data.photo.tracks = new media_track(); + metadata.data.photo.tracks->type = VITA_TRACK_TYPE_PHOTO; + metadata.data.photo.tracks->data.track_photo.codecType = 17; // JPEG? + loadPhotoMetadata(file.absoluteFilePath()); + } + + path = file.absoluteFilePath(); + + if(parent->metadata.path == NULL) { + metadata.path = strdup(metadata.name); + } else { + QString newpath = QString(parent->metadata.path) + QDir::separator() + metadata.name; + metadata.path = strdup(newpath.toUtf8().data()); + } + + updateParentSize(metadata.size); +} + +bool CMAObject::removeReferencedObject() +{ + if(metadata.dataType & Folder) { + return removeRecursively(path); + } else { + return QFile::remove(path); + } +} + +void CMAObject::updateParentSize(unsigned long size) +{ + if(parent) { + parent->metadata.size += size; + parent->updateParentSize(size); + } +} + +void CMAObject::rename(const QString &newname) +{ + free(metadata.name); + metadata.name = strdup(newname.toUtf8().data()); + + if(metadata.path) { + QStringList metadata_path(QString(metadata.path).split(QDir::separator())); + metadata_path.replace(metadata_path.count() - 1, newname); + free(metadata.path); + metadata.path = strdup(metadata_path.join(QDir::separator()).toUtf8().data()); + } + + path = QFileInfo(path).absoluteDir().path() + QDir::separator() + newname; +} + +void CMAObject::refreshPath() +{ + if(parent) { + free(metadata.path); + QString newpath(QString(parent->metadata.path) + QDir::separator() + metadata.name); + metadata.path = strdup(newpath.toUtf8().data()); + path = parent->path + QDir::separator() + metadata.name; + } +} + +bool CMAObject::hasParent(const CMAObject *obj) +{ + if(parent) { + if(metadata.ohfiParent == obj->metadata.ohfi) { + return true; + } else { + return parent->hasParent(obj); + } + } + return false; +} + +bool CMAObject::operator==(const CMAObject &obj) +{ + return metadata.ohfi == obj.metadata.ohfi; +} + +bool CMAObject::operator!=(const CMAObject &obj) +{ + return metadata.ohfi != obj.metadata.ohfi; +} + +bool CMAObject::operator<(const CMAObject &obj) +{ + return metadata.ohfi < obj.metadata.ohfi; +} diff --git a/cmaobject.h b/cmaobject.h new file mode 100644 index 0000000..7606ac7 --- /dev/null +++ b/cmaobject.h @@ -0,0 +1,71 @@ +/* + * QCMA: Cross-platform content manager assistant for the PS Vita + * + * Copyright (C) 2013 Codestation + * + * 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 CMAOBJECT_H +#define CMAOBJECT_H + +#include +#include + +extern "C" { +#include +} + +#define OHFI_OFFSET 1000 + +class CMAObject +{ +public: + explicit CMAObject(CMAObject *parent = 0); + ~CMAObject(); + + void refreshPath(); + void rename(const QString &name); + bool removeReferencedObject(); + void initObject(const QFileInfo &file); + void updateParentSize(unsigned long size); + bool hasParent(const CMAObject *obj); + + bool operator==(const CMAObject &obj); + bool operator!=(const CMAObject &obj); + bool operator<(const CMAObject &obj); + + void setOhfi(int ohfi) { + metadata.ohfi = ohfi; + } + + static void resetOhfiCounter() { + ohfi_count = OHFI_OFFSET; + } + + QString path; + CMAObject *parent; + metadata_t metadata; + +protected: + static int ohfi_count; + +private: + void loadSfoMetadata(const QString &path); + void loadMusicMetadata(const QString &path); + void loadVideoMetadata(const QString &path); + void loadPhotoMetadata(const QString &path); +}; + +#endif // CMAOBJECT_H diff --git a/cmarootobject.cpp b/cmarootobject.cpp new file mode 100644 index 0000000..d5f92b8 --- /dev/null +++ b/cmarootobject.cpp @@ -0,0 +1,139 @@ +/* + * QCMA: Cross-platform content manager assistant for the PS Vita + * + * Copyright (C) 2013 Codestation + * + * 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 "cmarootobject.h" + +#include + +QString CMARootObject::uuid = "ffffffffffffffff"; + +CMARootObject::CMARootObject(int ohfi) : + num_filters(0), filters(NULL), root_ohfi(ohfi) +{ +} + +void CMARootObject::initObject(const QString &path) +{ + metadata.ohfi = root_ohfi; + metadata.type = VITA_DIR_TYPE_MASK_ROOT | VITA_DIR_TYPE_MASK_REGULAR; + + switch(root_ohfi) { + case VITA_OHFI_PHOTO: + metadata.dataType = Photo; + this->path = path; + num_filters = 2; + filters = new metadata_t[2]; + createFilter(&filters[0], "Folders", VITA_DIR_TYPE_MASK_PHOTO | VITA_DIR_TYPE_MASK_ROOT | VITA_DIR_TYPE_MASK_REGULAR); + createFilter(&filters[1], "All", VITA_DIR_TYPE_MASK_PHOTO | VITA_DIR_TYPE_MASK_ROOT | VITA_DIR_TYPE_MASK_ALL); + break; + + case VITA_OHFI_VIDEO: + metadata.dataType = Video; + this->path = path; + num_filters = 2; + filters = new metadata_t[2]; + createFilter(&filters[0], "Folders", VITA_DIR_TYPE_MASK_VIDEO | VITA_DIR_TYPE_MASK_ROOT | VITA_DIR_TYPE_MASK_REGULAR); + createFilter(&filters[1], "All", VITA_DIR_TYPE_MASK_VIDEO | VITA_DIR_TYPE_MASK_ROOT | VITA_DIR_TYPE_MASK_ALL); + break; + + case VITA_OHFI_MUSIC: + metadata.dataType = Music; + this->path = path; + num_filters = 1; + filters = new metadata_t[1]; + //createFilter(&filters[0], "Folders", VITA_DIR_TYPE_MASK_MUSIC | VITA_DIR_TYPE_MASK_ROOT | VITA_DIR_TYPE_MASK_PLAYLISTS); + createFilter(&filters[0], "All", VITA_DIR_TYPE_MASK_MUSIC | VITA_DIR_TYPE_MASK_ROOT | VITA_DIR_TYPE_MASK_SONGS); + break; + + case VITA_OHFI_VITAAPP: + metadata.dataType = App; + this->path = QDir(QDir(path).absoluteFilePath("APP")).absoluteFilePath(uuid); + num_filters = 0; + break; + + case VITA_OHFI_PSPAPP: + metadata.dataType = App; + this->path = QDir(QDir(path).absoluteFilePath("PGAME")).absoluteFilePath(uuid); + num_filters = 0; + break; + + case VITA_OHFI_PSPSAVE: + metadata.dataType = SaveData; + this->path = QDir(QDir(path).absoluteFilePath("PSAVEDATA")).absoluteFilePath(uuid); + num_filters = 0; + break; + + case VITA_OHFI_PSXAPP: + metadata.dataType = App; + this->path = QDir(QDir(path).absoluteFilePath("PSGAME")).absoluteFilePath(uuid); + num_filters = 0; + break; + + case VITA_OHFI_PSMAPP: + metadata.dataType = App; + this->path = QDir(QDir(path).absoluteFilePath("PSM")).absoluteFilePath(uuid); + num_filters = 0; + break; + + case VITA_OHFI_BACKUP: + metadata.dataType = App; + this->path = QDir(QDir(path).absoluteFilePath("SYSTEM")).absoluteFilePath(uuid); + num_filters = 0; + } +} + +CMARootObject::~CMARootObject() +{ + for(int i = 0; i < num_filters; i++) { + free(filters[i].name); + free(filters[i].path); + } + + delete[] filters; +} + +void CMARootObject::createFilter(metadata_t *filter, const char *name, int type) +{ + filter->ohfiParent = metadata.ohfi; + filter->ohfi = ohfi_count++; + filter->name = strdup(name); + filter->path = strdup(metadata.path ? metadata.path : ""); + filter->type = type; + filter->dateTimeCreated = 0; + filter->size = 0; + filter->dataType = static_cast(Folder | Special); + filter->next_metadata = NULL; +} + +int CMARootObject::getFilters(metadata_t **p_head) +{ + int numObjects = num_filters; + + for(int i = 0; i < numObjects; i++) { + filters[i].next_metadata = &filters[i + 1]; + } + + filters[numObjects - 1].next_metadata = NULL; + + if(p_head != NULL) { + *p_head = filters; + } + + return numObjects; +} diff --git a/cmarootobject.h b/cmarootobject.h new file mode 100644 index 0000000..5b1d1a2 --- /dev/null +++ b/cmarootobject.h @@ -0,0 +1,51 @@ +/* + * QCMA: Cross-platform content manager assistant for the PS Vita + * + * Copyright (C) 2013 Codestation + * + * 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 CMAROOTOBJECT_H +#define CMAROOTOBJECT_H + +#include "cmaobject.h" + +#include + +extern "C" { +#include +} + +class CMARootObject : public CMAObject +{ +public: + explicit CMARootObject(int ohfi); + ~CMARootObject(); + + void initObject(const QString &path); + void remove(const CMAObject *obj); + int getFilters(metadata_t **p_head); + + int num_filters; + metadata_t *filters; + static QString uuid; + +private: + void createFilter(metadata_t *filter, const char *name, int type); + + int root_ohfi; +}; + +#endif // CMAROOTOBJECT_H diff --git a/configwidget.cpp b/configwidget.cpp new file mode 100644 index 0000000..2f225bd --- /dev/null +++ b/configwidget.cpp @@ -0,0 +1,155 @@ +/* + * QCMA: Cross-platform content manager assistant for the PS Vita + * + * Copyright (C) 2013 Codestation + * + * 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 "configwidget.h" +#include "ui_configwidget.h" + +extern "C" { +#include +} + +#include +#include + +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) +#include +#else +#include +#define QStandardPaths QDesktopServices +#define writableLocation storageLocation +#endif + +ConfigWidget::ConfigWidget(QWidget *parent) : + QDialog(parent), + ui(new Ui::ConfigWidget) +{ + ui->setupUi(this); + connectSignals(); + setDefaultDirs(); +} + +void ConfigWidget::connectSignals() +{ + QSignalMapper *mapper = new QSignalMapper(this); + mapper->setMapping(ui->photoBtn, BTN_PHOTO); + mapper->setMapping(ui->musicBtn, BTN_MUSIC); + mapper->setMapping(ui->videoBtn, BTN_VIDEO); + mapper->setMapping(ui->appBtn, BTN_APPS); + mapper->setMapping(ui->urlBtn, BTN_URL); + connect(ui->photoBtn, SIGNAL(clicked()), mapper, SLOT(map())); + connect(ui->musicBtn, SIGNAL(clicked()), mapper, SLOT(map())); + connect(ui->videoBtn, SIGNAL(clicked()), mapper, SLOT(map())); + connect(ui->appBtn, SIGNAL(clicked()), mapper, SLOT(map())); + connect(ui->urlBtn, SIGNAL(clicked()), mapper, SLOT(map())); + connect(mapper, SIGNAL(mapped(int)), this, SLOT(browseBtnPressed(int))); + connect(ui->buttonBox, SIGNAL(accepted()), this, SLOT(accept())); + connect(ui->buttonBox, SIGNAL(rejected()), this, SLOT(reject())); +} + +void ConfigWidget::setDefaultDirs() +{ + QString defaultdir; + QSettings settings; + defaultdir = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); + ui->photoPath->setText(settings.value("photoPath", defaultdir).toString()); + defaultdir = QStandardPaths::writableLocation(QStandardPaths::MusicLocation); + ui->musicPath->setText(settings.value("musicPath", defaultdir).toString()); + defaultdir = QStandardPaths::writableLocation(QStandardPaths::MoviesLocation); + ui->videoPath->setText(settings.value("videoPath", defaultdir).toString()); + defaultdir = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); + defaultdir.append(QDir::separator()).append("PS Vita"); + ui->appPath->setText(settings.value("appsPath", defaultdir).toString()); + defaultdir = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); + defaultdir.append(QDir::separator()).append("PSV Updates"); + ui->urlPath->setText(settings.value("urlPath", defaultdir).toString()); +} + +ConfigWidget::~ConfigWidget() +{ + delete ui; +} + +void ConfigWidget::browseBtnPressed(int btn) +{ + QString msg; + QLineEdit *lineedit; + + switch(btn) { + case BTN_PHOTO: + lineedit = ui->photoPath; + msg = tr("Select the folder to be used as a photo source"); + break; + + case BTN_MUSIC: + lineedit = ui->musicPath; + msg = tr("Select the folder to be used as a music source"); + break; + + case BTN_VIDEO: + lineedit = ui->videoPath; + msg = tr("Select the folder to be used as a video source"); + break; + + case BTN_APPS: + lineedit = ui->appPath; + msg = tr("Select the folder to be used to save PS Vita games and backups"); + break; + + case BTN_URL: + lineedit = ui->urlPath; + msg = tr("Select the folder to be used to fetch software updates"); + break; + + default: + return; + } + + QFileDialog dialog; + dialog.setFileMode(QFileDialog::Directory); + dialog.setOption(QFileDialog::ShowDirsOnly); + dialog.setDirectory(lineedit->text()); + dialog.setWindowTitle(msg); + + if(dialog.exec()) { + QStringList list = dialog.selectedFiles(); + lineedit->setText(list.first()); + } +} + +void ConfigWidget::savePath(QSettings &settings, const QLineEdit *edit, const QString &key) +{ + QString path = edit->text(); + if(path.endsWith(QDir::separator())) { + path.chop(1); + } + settings.setValue(key, path); + QDir(QDir::root()).mkpath(path); +} + +void ConfigWidget::accept() +{ + QSettings settings; + savePath(settings, ui->photoPath, "photoPath"); + savePath(settings, ui->musicPath, "musicPath"); + savePath(settings, ui->videoPath, "videoPath"); + savePath(settings, ui->appPath, "appsPath"); + savePath(settings, ui->urlPath, "urlPath"); + settings.sync(); + done(Accepted); +} diff --git a/configwidget.h b/configwidget.h new file mode 100644 index 0000000..593972b --- /dev/null +++ b/configwidget.h @@ -0,0 +1,55 @@ +/* + * QCMA: Cross-platform content manager assistant for the PS Vita + * + * Copyright (C) 2013 Codestation + * + * 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 CONFIGWIDGET_H +#define CONFIGWIDGET_H + +#include +#include +#include +#include + +namespace Ui +{ +class ConfigWidget; +} + +class ConfigWidget : public QDialog +{ + Q_OBJECT + +public: + explicit ConfigWidget(QWidget *parent = 0); + ~ConfigWidget(); + +private: + enum browse_buttons {BTN_PHOTO, BTN_MUSIC, BTN_VIDEO, BTN_APPS, BTN_URL}; + + void connectSignals(); + void setDefaultDirs(); + void savePath(QSettings &settings, const QLineEdit *edit, const QString &key); + + Ui::ConfigWidget *ui; + +private slots: + void browseBtnPressed(int from); + void accept(); +}; + +#endif // CONFIGWIDGET_H diff --git a/configwidget.ui b/configwidget.ui new file mode 100644 index 0000000..c47a894 --- /dev/null +++ b/configwidget.ui @@ -0,0 +1,210 @@ + + + ConfigWidget + + + + 0 + 0 + 520 + 405 + + + + QCMA Settings + + + + + + + + 0 + + + + Folders + + + + + + + + Specify the folders that the PS Vita will access for each content type + + + true + + + + + + + + + Photo Folder + + + + + + + + + true + + + + + + + Browse... + + + + + + + + + + + + + Video Folder + + + + + + + + + true + + + + + + + Browse... + + + + + + + + + + + + + Music Folder + + + + + + + + + true + + + + + + + Browse... + + + + + + + + + + + + + Applications / Backups + + + + + + + + + true + + + + + + + Browse... + + + + + + + + + + + + + Updates / Web content + + + + + + + + + true + + + + + + + Browse... + + + + + + + + + + + + + + Other + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + diff --git a/database.cpp b/database.cpp new file mode 100644 index 0000000..598c75d --- /dev/null +++ b/database.cpp @@ -0,0 +1,334 @@ +/* + * QCMA: Cross-platform content manager assistant for the PS Vita + * + * Copyright (C) 2013 Codestation + * + * 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 "database.h" + +#include +#include +#include +#include +#include + +#define OHFI_OFFSET 1000 + +Database::Database(QObject *parent) : + QObject(parent), mutex(QMutex::Recursive) +{ +} + +Database::~Database() +{ + destroy(); +} + +void Database::setUUID(const QString uuid) +{ + CMARootObject::uuid = uuid; +} + +int Database::create() +{ + int total_objects = 0; + QMutexLocker locker(&mutex); + const int ohfi_array[] = { VITA_OHFI_MUSIC, VITA_OHFI_PHOTO, VITA_OHFI_VIDEO, + VITA_OHFI_BACKUP, VITA_OHFI_VITAAPP, VITA_OHFI_PSPAPP, + VITA_OHFI_PSPSAVE, VITA_OHFI_PSXAPP, VITA_OHFI_PSMAPP + }; + CMAObject::resetOhfiCounter(); + QSettings settings; + + for(int i = 0, max = sizeof(ohfi_array) / sizeof(int); i < max; i++) { + CMARootObject *obj = new CMARootObject(ohfi_array[i]); + + switch(ohfi_array[i]) { + case VITA_OHFI_MUSIC: + obj->initObject(settings.value("musicPath").toString()); + break; + + case VITA_OHFI_PHOTO: + obj->initObject(settings.value("photoPath").toString()); + break; + + case VITA_OHFI_VIDEO: + obj->initObject(settings.value("videoPath").toString()); + break; + + case VITA_OHFI_BACKUP: + case VITA_OHFI_VITAAPP: + case VITA_OHFI_PSPAPP: + case VITA_OHFI_PSPSAVE: + case VITA_OHFI_PSXAPP: + case VITA_OHFI_PSMAPP: + obj->initObject(settings.value("appsPath").toString()); + } + + root_list list; + list << obj; + total_objects += scanRootDirectory(list); + object_list[ohfi_array[i]] = list; + } + return total_objects; +} + +CMAObject *Database::getParent(CMAObject *last_dir, const QString ¤t_path) +{ + while(last_dir && current_path != last_dir->path) { + last_dir = last_dir->parent; + } + + return last_dir; +} + +int Database::scanRootDirectory(root_list &list) +{ + int total_objects = 0; + CMAObject *last_dir = list.first(); + QDir dir(last_dir->path); + dir.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot); + QDirIterator it(dir, QDirIterator::Subdirectories); + + while(it.hasNext()) { + it.next(); + CMAObject *obj = new CMAObject(getParent(last_dir, it.fileInfo().path())); + obj->initObject(it.fileInfo()); + list << obj; + + if(obj->metadata.dataType & Folder) { + last_dir = obj; + } else { + total_objects++; + } + } + return total_objects; +} + +void Database::destroy() +{ + QMutexLocker locker(&mutex); + + for(map_list::iterator root = object_list.begin(); root != object_list.end(); ++root) { + qDeleteAll(*root); + } + + object_list.clear(); +} + +bool Database::removeInternal(root_list &list, const CMAObject *obj) +{ + bool found = false; + QList::iterator it = list.begin(); + + while(it != list.end()) { + if(!found && (*it) == obj) { + it = list.erase(it); + found = true; + } else if(found && (*it)->metadata.ohfiParent == obj->metadata.ohfi) { + it = list.erase(it); + } else { + ++it; + } + } + + return found; +} + +bool Database::remove(const CMAObject *obj, int ohfi_root) +{ + QMutexLocker locker(&mutex); + + if(ohfi_root) { + return removeInternal(object_list[ohfi_root], obj); + } else { + for(map_list::iterator root = object_list.begin(); root != object_list.end(); ++root) { + if(removeInternal(*root, obj)) { + return true; + } + } + } + return false; +} + +bool Database::lessThanComparator(const CMAObject *a, const CMAObject *b) +{ + return a->metadata.ohfi < b->metadata.ohfi; +} + +bool Database::hasFilter(const CMARootObject *object,int ohfi) +{ + for(int i = 0; i < object->num_filters; i++) { + if(object->filters[i].ohfi == ohfi) { + return true; + } + } + return false; +} + +bool Database::findInternal(const root_list &list, int ohfi, find_data &data) +{ + if(hasFilter(static_cast(list.first()), ohfi)) { + data.it = list.begin(); + } else { + CMAObject obj; + obj.setOhfi(ohfi); + data.it = qBinaryFind(list.begin(), list.end(), &obj, Database::lessThanComparator); + } + data.end = list.end(); + return data.it != data.end; +} + +bool Database::find(int ohfi, Database::find_data &data) +{ + QMutexLocker locker(&mutex); + + for(map_list::iterator root = object_list.begin(); root != object_list.end(); ++root) { + if(findInternal(*root, ohfi, data)) { + return true; + } + } + return false; +} + +void Database::append(int parent_ohfi, CMAObject *object) +{ + QMutexLocker locker(&mutex); + CMAObject parent; + parent.setOhfi(parent_ohfi); + + for(map_list::iterator root = object_list.begin(); root != object_list.end(); ++root) { + root_list *cat_list = &(*root); + root_list::const_iterator it = qBinaryFind(cat_list->begin(), cat_list->end(), &parent, Database::lessThanComparator); + + if(it != cat_list->end()) { + cat_list->append(object); + break; + } + } +} + +CMAObject *Database::ohfiToObject(int ohfi) +{ + QMutexLocker locker(&mutex); + find_data data; + return find(ohfi, data) ? *data.it : NULL; +} + +CMAObject *Database::pathToObjectInternal(const root_list &list, const char *path) +{ + // skip the first element since is the root element + root_list::const_iterator skipped_first = ++list.begin(); + + for(root_list::const_iterator obj = skipped_first; obj != list.end(); ++obj) { + if(strcmp(path, (*obj)->metadata.path) == 0) { + return (*obj); + } + } + return NULL; +} + +CMAObject *Database::pathToObject(const char *path, int ohfiRoot) +{ + QMutexLocker locker(&mutex); + + for(map_list::iterator root = object_list.begin(); root != object_list.end(); ++root) { + + if(ohfiRoot && (*root).first()->metadata.ohfi != ohfiRoot) { + continue; + } + CMAObject *obj = pathToObjectInternal(*root, path); + + if(obj) { + return obj; + } + } + return NULL; +} + +int Database::acceptFilteredObject(const CMAObject *parent, const CMAObject *current, int type) +{ + QMutexLocker locker(&mutex); + int result = 0; + + if(MASK_SET(type, VITA_DIR_TYPE_MASK_PHOTO)) { + result = (current->metadata.dataType & Photo); + } else if(MASK_SET(type, VITA_DIR_TYPE_MASK_VIDEO)) { + result = (current->metadata.dataType & Video); + } else if(MASK_SET(type, VITA_DIR_TYPE_MASK_MUSIC)) { + result = (current->metadata.dataType & Music); + } + + if(type & (VITA_DIR_TYPE_MASK_ALL | VITA_DIR_TYPE_MASK_SONGS)) { + result = result && (current->metadata.dataType & File); + } else if(type & (VITA_DIR_TYPE_MASK_REGULAR)) { + result = (parent->metadata.ohfi == current->metadata.ohfiParent); + } + + // TODO: Support other filter types + return result; +} + +int Database::filterObjects(int ohfiParent, metadata_t **p_head) +{ + QMutexLocker locker(&mutex); + CMARootObject *parent = static_cast(ohfiToObject(ohfiParent)); + + if(parent == NULL) { + return 0; + } + + int type = parent->metadata.type; + + if(parent->metadata.ohfi < OHFI_OFFSET && parent->filters) { // if we have filters + if(ohfiParent == parent->metadata.ohfi) { // if we are looking at root + return parent->getFilters(p_head); + } else { // we are looking at a filter + for(int j = 0; j < parent->num_filters; j++) { + if(parent->filters[j].ohfi == ohfiParent) { + type = parent->filters[j].type; + break; + } + } + } + } + + int numObjects = 0; + metadata_t temp = metadata_t(); + metadata_t *tail = &temp; + + for(map_list::iterator root = object_list.begin(); root != object_list.end(); ++root) { + for(root_list::iterator object = (*root).begin(); object != (*root).end(); ++object) { + if(acceptFilteredObject(parent, *object, type)) { + tail->next_metadata = &(*object)->metadata; + tail = tail->next_metadata; + numObjects++; + } + } + + if(numObjects > 0) { + break; + } + } + + tail->next_metadata = NULL; + + if(p_head != NULL) { + *p_head = temp.next_metadata; + } + + return numObjects; +} diff --git a/database.h b/database.h new file mode 100644 index 0000000..40c6744 --- /dev/null +++ b/database.h @@ -0,0 +1,79 @@ +/* + * QCMA: Cross-platform content manager assistant for the PS Vita + * + * Copyright (C) 2013 Codestation + * + * 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 DATABASE_H +#define DATABASE_H + +#include "cmarootobject.h" + +#include +#include +#include +#include + +extern "C" { +#include +} + +class Database : public QObject +{ + Q_OBJECT +public: + typedef struct { + QList::const_iterator it; + QList::const_iterator end; + } find_data; + + explicit Database(QObject *parent = 0); + ~Database(); + + + void destroy(); + void setUUID(const QString uuid); + int create(); + void addEntries(CMAObject *root); + CMAObject *ohfiToObject(int ohfi); + bool find(int ohfi, find_data &data); + void append(int parent_ohfi, CMAObject *object); + bool remove(const CMAObject *obj, int ohfi_root = 0); + int filterObjects(int ohfiParent, metadata_t **p_head); + CMAObject *pathToObject(const char *path, int ohfiRoot); + int acceptFilteredObject(const CMAObject *parent, const CMAObject *current, int type); + + QMutex mutex; + +private: + typedef QList root_list; + typedef QMap map_list; + + int scanRootDirectory(root_list &list); + bool hasFilter(const CMARootObject *object,int ohfi); + bool removeInternal(root_list &list, const CMAObject *obj); + bool findInternal(const root_list &list, int ohfi, find_data &data); + CMAObject *getParent(CMAObject *last_dir, const QString ¤t_path); + CMAObject *pathToObjectInternal(const root_list &list, const char *path); + static bool lessThanComparator(const CMAObject *a, const CMAObject *b); + + map_list object_list; + +signals: + void finished(); +}; + +#endif // DATABASE_H diff --git a/listenerworker.cpp b/listenerworker.cpp new file mode 100644 index 0000000..3691e25 --- /dev/null +++ b/listenerworker.cpp @@ -0,0 +1,906 @@ +/* + * QCMA: Cross-platform content manager assistant for the PS Vita + * + * Copyright (C) 2013 Codestation + * + * 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 "listenerworker.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "utils.h" + +#define CALL_MEMBER_FUNC(obj,memberptr) ((obj).*(memberptr)) + +const metadata_t ListenerWorker::g_thumbmeta = {0, 0, 0, NULL, NULL, 0, 0, 0, Thumbnail, {{18, 144, 80, 0, 1, 1.0f, 2}}, NULL}; + +ListenerWorker::ListenerWorker(QObject *parent) : + BaseWorker(parent) +{ + connected = true; +} + +int ListenerWorker::rebuildDatabase() +{ + db.destroy(); + return db.create(); +} + +void ListenerWorker::setDevice(vita_device_t *device) +{ + this->device = device; +} + +bool ListenerWorker::isConnected() +{ + return connected; +} + +void ListenerWorker::disconnect() +{ + qDebug("Stopping event listener"); + connected = false; +} + +void ListenerWorker::process() +{ + vita_event_t event; + + qDebug() << "From listener: "<< QThread::currentThreadId(); + + while(connected) { + if(VitaMTP_Read_Event(device, &event) < 0) { + qWarning("Error reading event from Vita."); + connected = false; + break; + } + + qDebug("Event 0x%04X recieved", event.Code); + + switch(event.Code) { + case PTP_EC_VITA_RequestSendNumOfObject: + vitaEventSendNumOfObject(&event, event.Param1); + break; + case PTP_EC_VITA_RequestSendObjectMetadata: + vitaEventSendObjectMetadata(&event, event.Param1); + break; + case PTP_EC_VITA_RequestSendObject: + vitaEventSendObject(&event, event.Param1); + break; + case PTP_EC_VITA_RequestCancelTask: // unimplemented + vitaEventCancelTask(&event, event.Param1); + break; + case PTP_EC_VITA_RequestSendHttpObjectFromURL: + vitaEventSendHttpObjectFromURL(&event, event.Param1); + break; + case PTP_EC_VITA_Unknown1: // unimplemented + vitaEventUnimplementated(&event, event.Param1); + break; + case PTP_EC_VITA_RequestSendObjectStatus: + vitaEventSendObjectStatus(&event, event.Param1); + break; + case PTP_EC_VITA_RequestSendObjectThumb: + vitaEventSendObjectThumb(&event, event.Param1); + break; + case PTP_EC_VITA_RequestDeleteObject: + vitaEventDeleteObject(&event, event.Param1); + break; + case PTP_EC_VITA_RequestGetSettingInfo: + vitaEventGetSettingInfo(&event, event.Param1); + break; + case PTP_EC_VITA_RequestSendHttpObjectPropFromURL: + vitaEventSendHttpObjectPropFromURL(&event, event.Param1); + break; + case PTP_EC_VITA_RequestSendPartOfObject: + vitaEventSendPartOfObject(&event, event.Param1); + break; + case PTP_EC_VITA_RequestOperateObject: + vitaEventOperateObject(&event, event.Param1); + break; + case PTP_EC_VITA_RequestGetPartOfObject: + vitaEventGetPartOfObject(&event, event.Param1); + break; + case PTP_EC_VITA_RequestSendStorageSize: + vitaEventSendStorageSize(&event, event.Param1); + break; + case PTP_EC_VITA_RequestCheckExistance: + vitaEventCheckExistance(&event, event.Param1); + break; + case PTP_EC_VITA_RequestGetTreatObject: + vitaEventGetTreatObject(&event, event.Param1); + break; + case PTP_EC_VITA_RequestSendCopyConfirmationInfo: + vitaEventSendCopyConfirmationInfo(&event, event.Param1); + break; + case PTP_EC_VITA_RequestSendObjectMetadataItems: + vitaEventSendObjectMetadataItems(&event, event.Param1); + break; + case PTP_EC_VITA_RequestSendNPAccountInfo: + vitaEventSendNPAccountInfo(&event, event.Param1); + break; + case PTP_EC_VITA_RequestTerminate: + vitaEventRequestTerminate(&event, event.Param1); + break; + default: + vitaEventUnimplementated(&event, event.Param1); + } + } + emit finished(); +} + +quint16 ListenerWorker::processAllObjects(CMAObject *parent, quint32 handle) +{ + union { + unsigned char *fileData; + uint32_t *handles; + } data; + + metadata_t remote_meta; + unsigned int length; + + if(VitaMTP_GetObject(device, handle, &remote_meta, (void **)&data, &length) != PTP_RC_OK) { + qWarning("Cannot get object for handle %d", handle); + return PTP_RC_VITA_Invalid_Data; + } + + + CMAObject *object = db.pathToObject(remote_meta.name, parent->metadata.ohfi); + + if(object) { + qDebug("Deleting %s", object->path.toStdString().c_str()); + removeRecursively(object->path); + db.remove(object); + } + + QDir dir(parent->path); + + if(remote_meta.dataType & Folder) { + if(!dir.mkpath(remote_meta.name)) { + qWarning("Cannot create directory: %s", remote_meta.name); + free(data.fileData); + free(remote_meta.name); + return PTP_RC_VITA_Failed_Operate_Object; + } + } else { + QFile file(dir.absoluteFilePath(remote_meta.name)); + + if(!file.open(QIODevice::WriteOnly)) { + qWarning("Cannot write to %s", remote_meta.name); + free(data.fileData); + free(remote_meta.name); + return PTP_RC_VITA_Invalid_Permission; + } else { + file.write((const char *)data.fileData, remote_meta.size); + } + } + + QFileInfo info(dir, remote_meta.name); + object = new CMAObject(parent); + object->initObject(info); + object->metadata.handle = remote_meta.handle; + db.append(parent->metadata.ohfi, object); + free(remote_meta.name); + + qDebug("Added object %s with OHFI %i to database", object->metadata.path, object->metadata.ohfi); + + if(remote_meta.dataType & Folder) { + for(unsigned int i = 0; i < length; i++) { + quint16 ret = processAllObjects(object, data.handles[i]); + + if(ret != PTP_RC_OK) { + qDebug("Deleteting object with OHFI %d", object->metadata.ohfi); + db.remove(object); + free(data.fileData); + return ret; + } + } + } + + free(data.fileData); + return PTP_RC_OK; +} + +void ListenerWorker::vitaEventGetTreatObject(vita_event_t *event, int eventId) +{ + qDebug("Event recieved in %s, code: 0x%x, id: %d", Q_FUNC_INFO, event->Code, eventId); + + treat_object_t treatObject; + + if(VitaMTP_GetTreatObject(device, eventId, &treatObject) != PTP_RC_OK) { + qWarning("Cannot get information on object to get"); + return; + } + + QMutexLocker locker(&db.mutex); + + CMAObject *parent = db.ohfiToObject(treatObject.ohfiParent); + + if(parent == NULL) { + qWarning("Cannot find parent OHFI %d", treatObject.ohfiParent); + VitaMTP_ReportResult(device, eventId, PTP_RC_VITA_Invalid_OHFI); + return; + } + + VitaMTP_ReportResult(device, eventId, processAllObjects(parent, treatObject.handle)); +} + +void ListenerWorker::vitaEventSendCopyConfirmationInfo(vita_event_t *event, int eventId) +{ + qDebug("Event recieved in %s, code: 0x%x, id: %d", Q_FUNC_INFO, event->Code, eventId); + + copy_confirmation_info_t *info; + if(VitaMTP_SendCopyConfirmationInfoInit(device, eventId, &info) != PTP_RC_OK) { + qWarning("Error recieving initial information."); + return; + } + + QMutexLocker locker(&db.mutex); + + quint64 size = 0; + + for(quint32 i = 0; i < info->count; i++) { + CMAObject *object; + + if((object = db.ohfiToObject(info->ohfi[i])) == NULL) { + qWarning("Cannot find OHFI %d", info->ohfi[i]); + free(info); + return; + } + + size += object->metadata.size; + } + + if(VitaMTP_SendCopyConfirmationInfo(device, eventId, info, size) != PTP_RC_OK) { + qWarning("Error sending copy confirmation"); + } else { + VitaMTP_ReportResult(device, eventId, PTP_RC_OK); + } + + free(info); +} + +void ListenerWorker::vitaEventSendObjectMetadataItems(vita_event_t *event, int eventId) +{ + qDebug("Event recieved in %s, code: 0x%x, id: %d", Q_FUNC_INFO, event->Code, eventId); + + quint32 ohfi; + if(VitaMTP_SendObjectMetadataItems(device, eventId, &ohfi) != PTP_RC_OK) { + qWarning("Cannot get OHFI for retreving metadata"); + return; + } + + QMutexLocker locker(&db.mutex); + + CMAObject *object = db.ohfiToObject(ohfi); + + if(object == NULL) { + qWarning("Cannot find OHFI %d in database", ohfi); + VitaMTP_ReportResult(device, eventId, PTP_RC_VITA_Invalid_OHFI); + return; + } + + metadata_t *metadata = &object->metadata; + metadata->next_metadata = NULL; + qDebug("Sending metadata for OHFI %d (%s)", ohfi, metadata->path); + + if(VitaMTP_SendObjectMetadata(device, eventId, metadata) != PTP_RC_OK) { + qWarning("Error sending metadata"); + } else { + VitaMTP_ReportResult(device, eventId, PTP_RC_OK); + } +} + +void ListenerWorker::vitaEventSendNPAccountInfo(vita_event_t *event, int eventId) +{ + qDebug("Event recieved in %s, code: 0x%x, id: %d", Q_FUNC_INFO, event->Code, eventId); + // AFAIK, Sony hasn't even implemented this in their CMA + qWarning("Event 0x%x unimplemented!", event->Code); +} + +void ListenerWorker::vitaEventRequestTerminate(vita_event_t *event, int eventId) +{ + qDebug("Event recieved in %s, code: 0x%x, id: %d", Q_FUNC_INFO, event->Code, eventId); + qWarning("Event 0x%x unimplemented!", event->Code); +} + +void ListenerWorker::vitaEventUnimplementated(vita_event_t *event, int eventId) +{ + qWarning("Unknown event not handled, code: 0x%x, id: %d", event->Code, eventId); + qWarning("Param1: 0x%08X, Param2: 0x%08X, Param3: 0x%08X", event->Param1, event->Param2, event->Param3); +} + +void ListenerWorker::vitaEventSendNumOfObject(vita_event_t *event, int eventId) +{ + qDebug("Event recieved in %s, code: 0x%x, id: %d", Q_FUNC_INFO, event->Code, eventId); + + QMutexLocker locker(&db.mutex); + + uint ohfi = event->Param2; + int items = db.filterObjects(ohfi, NULL); + + if(VitaMTP_SendNumOfObject(device, eventId, items) != PTP_RC_OK) { + qWarning("Error occured receiving object count for OHFI parent %d", ohfi); + } else { + qDebug("Returned count of %d objects for OHFI parent %d", items, ohfi); + VitaMTP_ReportResult(device, eventId, PTP_RC_OK); + } +} + +void ListenerWorker::vitaEventSendObjectMetadata(vita_event_t *event, int eventId) +{ + qDebug("Event recieved in %s, code: 0x%x, id: %d", Q_FUNC_INFO, event->Code, eventId); + + browse_info_t browse; + + if(VitaMTP_GetBrowseInfo(device, eventId, &browse) != PTP_RC_OK) { + qWarning("GetBrowseInfo failed"); + return; + } + QMutexLocker locker(&db.mutex); + + metadata_t *meta; + int count = db.filterObjects(browse.ohfiParent, &meta); // if meta is null, will return empty XML + qDebug("Sending %i metadata filtered objects for OHFI %id", count, browse.ohfiParent); + + if(VitaMTP_SendObjectMetadata(device, eventId, meta) != PTP_RC_OK) { // send all objects with OHFI parent + qWarning("Sending metadata for OHFI parent %d failed", browse.ohfiParent); + } else { + VitaMTP_ReportResult(device, eventId, PTP_RC_OK); + } +} + +void ListenerWorker::vitaEventSendObject(vita_event_t *event, int eventId) +{ + qDebug("Event recieved in %s, code: 0x%x, id: %d", Q_FUNC_INFO, event->Code, eventId); + + int ohfi = event->Param2; + + QMutexLocker locker(&db.mutex); + + Database::find_data iters; + if(!db.find(ohfi, iters)) { + qWarning("Failed to find OHFI %d", ohfi); + VitaMTP_ReportResult(device, eventId, PTP_RC_VITA_Invalid_OHFI); + return; + } + + unsigned long len = 0; + CMAObject *object = *iters.it; + CMAObject *start = object; + uint parentHandle = event->Param3; + uint handle; + + do { + uchar *data = NULL; + len = object->metadata.size; + QFile file(object->path); + + // read the file to send if it's not a directory + // if it is a directory, data and len are not used by VitaMTP + if(object->metadata.dataType & File) { + if(!file.open(QIODevice::ReadOnly) || (data = file.map(0, file.size())) == NULL) { + qWarning("Failed to read %s", object->path.toStdString().c_str()); + VitaMTP_ReportResult(device, eventId, PTP_RC_VITA_Not_Exist_Object); + return; + } + } + + // get the PTP object ID for the parent to put the object + // we know the parent has to be before the current node + // the first time this is called, parentHandle is left untouched + + if(start != object) { + parentHandle = object->parent->metadata.handle; + } + + // send the data over + qDebug("Sending %s of %lu bytes to device", object->metadata.name, len); + qDebug("OHFI %d with handle 0x%08X", ohfi, parentHandle); + + if(VitaMTP_SendObject(device, &parentHandle, &handle, &object->metadata, data) != PTP_RC_OK) { + qWarning("Sending of %s failed.", object->metadata.name); + file.unmap(data); + return; + } + + object->metadata.handle = handle; + file.unmap(data); + object = *iters.it++; + + } while(iters.it != iters.end && object->metadata.ohfiParent >= OHFI_OFFSET); // get everything under this "folder" + + VitaMTP_ReportResultWithParam(device, eventId, PTP_RC_OK, handle); + VitaMTP_ReportResult(device, eventId, PTP_RC_VITA_Invalid_Data); // TODO: Send thumbnail +} + +void ListenerWorker::vitaEventCancelTask(vita_event_t *event, int eventId) +{ + qDebug("Event recieved in %s, code: 0x%x, id: %d", Q_FUNC_INFO, event->Code, eventId); + + int eventIdToCancel = event->Param2; + VitaMTP_CancelTask(device, eventIdToCancel); + qWarning("Event CancelTask (0x%x) unimplemented!", event->Code); +} + +void ListenerWorker::vitaEventSendHttpObjectFromURL(vita_event_t *event, int eventId) +{ + qDebug("Event recieved in %s, code: 0x%x, id: %d", Q_FUNC_INFO, event->Code, eventId); + + char *url; + if(VitaMTP_GetUrl(device, eventId, &url) != PTP_RC_OK) { + qWarning("Failed to recieve URL"); + return; + } + + QString urlpath = QSettings().value("urlPath").toString(); + QString basename = QFileInfo(QUrl(url).path()).fileName(); + QFile file(QDir(urlpath).absoluteFilePath(basename)); + + QByteArray data; + + if(!file.open(QIODevice::ReadOnly)) { + if(basename == "psp2-updatelist.xml") { + qDebug("Found request for update list. Sending cached data"); + QFile res(":/main/psp2-updatelist.xml"); + res.open(QIODevice::ReadOnly); + data = res.readAll(); + } else { + qWarning("Failed to download %s", url); + VitaMTP_ReportResult(device, eventId, PTP_RC_VITA_Failed_Download); + free(url); + return; + } + } else { + data = file.readAll(); + } + + qDebug("Sending %i bytes of data for HTTP request %s", data.size(), url); + + if(VitaMTP_SendHttpObjectFromURL(device, eventId, data.data(), data.size()) != PTP_RC_OK) { + qWarning("Failed to send HTTP object"); + } else { + VitaMTP_ReportResult(device, eventId, PTP_RC_OK); + } + + free(url); +} + +void ListenerWorker::vitaEventSendObjectStatus(vita_event_t *event, int eventId) +{ + qDebug("Event recieved in %s, code: 0x%x, id: %d", Q_FUNC_INFO, event->Code, eventId); + + object_status_t objectstatus; + + if(VitaMTP_SendObjectStatus(device, eventId, &objectstatus) != PTP_RC_OK) { + qWarning("Failed to get information for object status."); + return; + } + + QMutexLocker locker(&db.mutex); + + CMAObject *object = db.pathToObject(objectstatus.title, objectstatus.ohfiRoot); + + if(object == NULL) { // not in database, don't return metadata + qDebug("Object %s not in database (OHFI: %i). Sending OK response for non-existence", objectstatus.title, objectstatus.ohfiRoot); + VitaMTP_ReportResult(device, eventId, PTP_RC_OK); + } else { + metadata_t *metadata = &object->metadata; + metadata->next_metadata = NULL; + qDebug("Sending metadata for OHFI %d", object->metadata.ohfi); + + if(VitaMTP_SendObjectMetadata(device, eventId, metadata) != PTP_RC_OK) { + qWarning("Error sending metadata for %d", object->metadata.ohfi); + } else { + VitaMTP_ReportResult(device, eventId, PTP_RC_OK); + } + } + + free(objectstatus.title); +} + +void ListenerWorker::vitaEventSendObjectThumb(vita_event_t *event, int eventId) +{ + qDebug("Event recieved in %s, code: 0x%x, id: %d", Q_FUNC_INFO, event->Code, eventId); + + QMutexLocker locker(&db.mutex); + + int ohfi = event->Param2; + CMAObject *object = db.ohfiToObject(ohfi); + + if(object == NULL) { + qWarning("Cannot find OHFI %d in database.", ohfi); + VitaMTP_ReportResult(device, eventId, PTP_RC_VITA_Invalid_OHFI); + return; + } + + QByteArray data; + + if(MASK_SET(object->metadata.dataType, SaveData)) { + QString thumbpath = QDir(object->path).absoluteFilePath("ICON0.PNG"); + qDebug("Sending savedata thumbnail from %s", thumbpath.toStdString().c_str()); + + QFile file(thumbpath); + if(!file.open(QIODevice::ReadOnly)) { + qWarning("Cannot find thumbnail %s", thumbpath.toStdString().c_str()); + VitaMTP_ReportResult(device, eventId, PTP_RC_VITA_Invalid_Data); + return; + } + data = file.readAll(); + + } else { + QImage img; + if(!MASK_SET(object->metadata.dataType, Photo) || !img.load(object->path)) { + qWarning("Thumbnail sending for the file %s is not supported", object->metadata.path); + VitaMTP_ReportResult(device, eventId, PTP_RC_VITA_Invalid_Data); + return; + } + qDebug("Creating thumbnail of %s", object->metadata.name); + + QBuffer buffer(&data); + buffer.open(QIODevice::WriteOnly); + QImage result = img.scaled(256, 256, Qt::KeepAspectRatio, Qt::FastTransformation); + result.save(&buffer, "JPEG"); + } + + // workaround for the vitamtp locale bug + char *locale = strdup(setlocale(LC_ALL, NULL)); + setlocale(LC_ALL, "C"); + + if(VitaMTP_SendObjectThumb(device, eventId, (metadata_t *)&g_thumbmeta, (uchar *)data.data(), data.size()) != PTP_RC_OK) { + qWarning("Error sending thumbnail"); + } else { + VitaMTP_ReportResult(device, eventId, PTP_RC_OK); + } + + // restore locale + setlocale(LC_ALL, locale); + free(locale); +} + +void ListenerWorker::vitaEventDeleteObject(vita_event_t *event, int eventId) +{ + qDebug("Event recieved in %s, code: 0x%x, id: %d", Q_FUNC_INFO, event->Code, eventId); + + QMutexLocker locker(&db.mutex); + + int ohfi = event->Param2; + CMAObject *object = db.ohfiToObject(ohfi); + + if(object == NULL) { + qWarning("OHFI %d not found", ohfi); + VitaMTP_ReportResult(device, eventId, PTP_RC_VITA_Invalid_OHFI); + return; + } + + qDebug("Deleting %s, OHFI: %i", object->metadata.path, object->metadata.ohfi); + removeRecursively(object->path); + db.remove(object); + + VitaMTP_ReportResult(device, eventId, PTP_RC_OK); +} + +void ListenerWorker::vitaEventGetSettingInfo(vita_event_t *event, int eventId) +{ + qDebug("Event recieved in %s, code: 0x%x, id: %d", Q_FUNC_INFO, event->Code, eventId); + + settings_info_t *settingsinfo; + if(VitaMTP_GetSettingInfo(device, eventId, &settingsinfo) != PTP_RC_OK) { + qWarning("Failed to get setting info from Vita."); + return; + } + + qDebug("Current account id: %s", settingsinfo->current_account.accountId); + + db.setUUID(settingsinfo->current_account.accountId); + + // set the database to be updated ASAP + emit refreshDatabase(); + + // free all the information + VitaMTP_Data_Free_Settings(settingsinfo); + VitaMTP_ReportResult(device, eventId, PTP_RC_OK); +} + +void ListenerWorker::vitaEventSendHttpObjectPropFromURL(vita_event_t *event, int eventId) +{ + qDebug("Event recieved in %s, code: 0x%x, id: %d", Q_FUNC_INFO, event->Code, eventId); + + char *url; + if(VitaMTP_GetUrl(device, eventId, &url) != PTP_RC_OK) { + qWarning("Failed to get URL"); + return; + } + + QString urlpath = QSettings().value("urlPath").toString(); + QString basename = QFileInfo(url).fileName(); + QFileInfo file(QDir(urlpath).absoluteFilePath(basename)); + + if(!file.exists()) { + qWarning("The file %s is not accesible", url); + VitaMTP_ReportResult(device, eventId, PTP_RC_VITA_Failed_Download); + free(url); + return; + } + + QString timestamp = file.lastModified().toString(); + + http_object_prop_t httpobjectprop; + httpobjectprop.timestamp = timestamp.toUtf8().data(); + httpobjectprop.timestamp_len = timestamp.toUtf8().size(); + + if(VitaMTP_SendHttpObjectPropFromURL(device, eventId, &httpobjectprop) != PTP_RC_OK) { + qWarning("Failed to send object properties"); + } else { + VitaMTP_ReportResult(device, eventId, PTP_RC_OK); + } + + free(url); +} + +void ListenerWorker::vitaEventSendPartOfObject(vita_event_t *event, int eventId) +{ + qDebug("Event recieved in %s, code: 0x%x, id: %d", Q_FUNC_INFO, event->Code, eventId); + + send_part_init_t part_init; + + if(VitaMTP_SendPartOfObjectInit(device, eventId, &part_init) != PTP_RC_OK) { + qWarning("Cannot get information on object to send"); + return; + } + + QMutexLocker locker(&db.mutex); + + CMAObject *object = db.ohfiToObject(part_init.ohfi); + + if(object == NULL) { + qWarning("Cannot find object for OHFI %d", part_init.ohfi); + VitaMTP_ReportResult(device, eventId, PTP_RC_VITA_Invalid_Context); + return; + } + + QFile file(object->path); + + if(!file.open(QIODevice::ReadOnly)) { + qWarning("Cannot read %s", object->path.toStdString().c_str()); + VitaMTP_ReportResult(device, eventId, PTP_RC_VITA_Not_Exist_Object); + return; + } else { + file.seek(part_init.offset); + QByteArray data = file.read(part_init.size); + qDebug("Sending %s at file offset %zu for %zu bytes", object->metadata.path, part_init.offset, part_init.size); + + if(VitaMTP_SendPartOfObject(device, eventId, (unsigned char *)data.data(), data.size()) != PTP_RC_OK) { + qWarning("Failed to send part of object OHFI %d", part_init.ohfi); + } else { + VitaMTP_ReportResult(device, eventId, PTP_RC_OK); + } + } +} + +void ListenerWorker::vitaEventOperateObject(vita_event_t *event, int eventId) +{ + qDebug("Event recieved in %s, code: 0x%x, id: %d", Q_FUNC_INFO, event->Code, eventId); + + operate_object_t operateobject; + + if(VitaMTP_OperateObject(device, eventId, &operateobject) != PTP_RC_OK) { + qWarning("Cannot get information on object to operate"); + return; + } + + QMutexLocker locker(&db.mutex); + + CMAObject *root = db.ohfiToObject(operateobject.ohfi); + + // end for renaming only + if(root == NULL) { + VitaMTP_ReportResult(device, eventId, PTP_RC_VITA_Not_Exist_Object); + return; + } + + switch(operateobject.cmd) { + case VITA_OPERATE_CREATE_FOLDER: { + qDebug("Operate command %d: Create folder %s", operateobject.cmd, operateobject.title); + + QDir dir(root->path); + if(!dir.mkdir(operateobject.title)) { + qWarning("Unable to create temporary folder: %s", operateobject.title); + VitaMTP_ReportResult(device, eventId, PTP_RC_VITA_Failed_Operate_Object); + } else { + CMAObject *newobj = new CMAObject(root); + newobj->initObject(QFileInfo(dir, operateobject.title)); + db.append(operateobject.ohfi, newobj); + qDebug("Created folder %s with OHFI %d under parent %s", newobj->metadata.path, newobj->metadata.ohfi, root->metadata.path); + VitaMTP_ReportResultWithParam(device, eventId, PTP_RC_OK, newobj->metadata.ohfi); + } + break; + } + case VITA_OPERATE_CREATE_FILE: { + qDebug("Operate command %d: Create file %s", operateobject.cmd, operateobject.title); + + QFile file(root->path + QDir::separator() + operateobject.title); + if(!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + qWarning("Unable to create temporary file: %s", operateobject.title); + VitaMTP_ReportResult(device, eventId, PTP_RC_VITA_Failed_Operate_Object); + } else { + CMAObject *newobj = new CMAObject(root); + newobj->initObject(file); + db.append(root->metadata.ohfi, newobj); + qDebug("Created file %s with OHFI %d under parent %s", newobj->metadata.path, newobj->metadata.ohfi, root->metadata.path); + VitaMTP_ReportResultWithParam(device, eventId, PTP_RC_OK, newobj->metadata.ohfi); + } + break; + } + case VITA_OPERATE_RENAME: { + qDebug("Operate command %d: Rename %s to %s", operateobject.cmd, root->metadata.name, operateobject.title); + + QString oldpath = root->path; + QString oldname = root->metadata.name; + + //rename the current object + root->rename(operateobject.title); + Database::find_data iters; + db.find(root->metadata.ohfi, iters); + + // rename the rest of the list only if has the renamed parent in some part of the chain + while(iters.it != iters.end) { + CMAObject *obj = *iters.it++; + + if(obj->hasParent(root)) { + obj->refreshPath(); + } + } + + // rename in filesystem + if(!QFile(oldpath).rename(root->path)) { + qWarning("Unable to rename %s to %s", oldname.toStdString().c_str(), operateobject.title); + VitaMTP_ReportResult(device, eventId, PTP_RC_VITA_Failed_Operate_Object); + break; + } + + qDebug("Renamed OHFI %d from %s to %s", root->metadata.ohfi, oldname.toStdString().c_str(), root->metadata.name); + VitaMTP_ReportResultWithParam(device, eventId, PTP_RC_OK, root->metadata.ohfi); + break; + } + + default: + qWarning("Operate command %d: Not implemented", operateobject.cmd); + VitaMTP_ReportResult(device, eventId, PTP_RC_VITA_Failed_Operate_Object); + break; + } + + free(operateobject.title); +} + +void ListenerWorker::vitaEventGetPartOfObject(vita_event_t *event, int eventId) +{ + qDebug("Event recieved in %s, code: 0x%x, id: %d", Q_FUNC_INFO, event->Code, eventId); + + unsigned char *data; + send_part_init_t part_init; + + if(VitaMTP_GetPartOfObject(device, eventId, &part_init, &data) != PTP_RC_OK) { + qWarning("Cannot get object from device"); + return; + } + + QMutexLocker locker(&db.mutex); + CMAObject *object = db.ohfiToObject(part_init.ohfi); + + if(object == NULL) { + qWarning("Cannot find OHFI %d", part_init.ohfi); + VitaMTP_ReportResult(device, eventId, PTP_RC_VITA_Invalid_OHFI); + free(data); + return; + } + + qDebug("Receiving %s at offset %zu for %zu bytes", object->metadata.path, part_init.offset, part_init.size); + + QFile file(object->path); + if(!file.open(QIODevice::ReadWrite)) { + qWarning("Cannot write to file %s", object->path.toStdString().c_str()); + VitaMTP_ReportResult(device, eventId, PTP_RC_VITA_Invalid_Permission); + } else { + file.seek(part_init.offset); + file.write((const char *)data, part_init.size); + object->metadata.size += part_init.size; + object->updateParentSize(part_init.size); + qDebug("Written %zu bytes to %s at offset %zu.", part_init.size, object->path.toStdString().c_str(), part_init.offset); + VitaMTP_ReportResult(device, eventId, PTP_RC_OK); + } + + free(data); +} + +void ListenerWorker::vitaEventSendStorageSize(vita_event_t *event, int eventId) +{ + qDebug("Event recieved in %s, code: 0x%x, id: %d", Q_FUNC_INFO, event->Code, eventId); + + QMutexLocker locker(&db.mutex); + + int ohfi = event->Param2; + CMAObject *object = db.ohfiToObject(ohfi); + + if(object == NULL) { + qWarning("Error: Cannot find OHFI %d", ohfi); + VitaMTP_ReportResult(device, eventId, PTP_RC_VITA_Invalid_OHFI); + return; + } else { + QFile file(object->path); + + if(!file.exists()) { + // create the directory if doesn't exist so the query don't fail + qDebug("Creating %s", object->path.toStdString().c_str()); + + if(!QDir(QDir::root()).mkpath(object->path)) { + qWarning("Create directory failed"); + VitaMTP_ReportResult(device, eventId, PTP_RC_VITA_Invalid_Permission); + return; + } + } + } + + quint64 total; + quint64 free; + + if(!getDiskSpace(object->path, &free, &total)) { + qWarning("Cannot get disk space"); + VitaMTP_ReportResult(device, eventId, PTP_RC_VITA_Invalid_Permission); + return; + } + + qDebug("Storage stats for drive containing OHFI %d, free: %llu, total: %llu", ohfi, free, total); + + if(VitaMTP_SendStorageSize(device, eventId, total, free) != PTP_RC_OK) { + qWarning("Send storage size failed"); + } else { + VitaMTP_ReportResult(device, eventId, PTP_RC_OK); + } +} + +void ListenerWorker::vitaEventCheckExistance(vita_event_t *event, int eventId) +{ + qDebug("Event recieved in %s, code: 0x%x, id: %d", Q_FUNC_INFO, event->Code, eventId); + + int handle = event->Param2; + existance_object_t existance; + + if(VitaMTP_CheckExistance(device, handle, &existance) != PTP_RC_OK) { + qWarning("Cannot read information on object to be sent"); + return; + } + + QMutexLocker locker(&db.mutex); + + CMAObject *object = db.pathToObject(existance.name, 0); + + if(object == NULL) { + VitaMTP_ReportResult(device, eventId, PTP_RC_VITA_Different_Object); + } else { + VitaMTP_ReportResultWithParam(device, eventId, PTP_RC_VITA_Same_Object, object->metadata.ohfi); + } + + VitaMTP_ReportResult(device, eventId, PTP_RC_OK); +} diff --git a/listenerworker.h b/listenerworker.h new file mode 100644 index 0000000..27e1800 --- /dev/null +++ b/listenerworker.h @@ -0,0 +1,82 @@ +/* + * QCMA: Cross-platform content manager assistant for the PS Vita + * + * Copyright (C) 2013 Codestation + * + * 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 LISTENERWORKER_H +#define LISTENERWORKER_H + +#include "database.h" +#include "baseworker.h" + +#include + +#include + +extern "C" { +#include +} + +class ListenerWorker : public BaseWorker +{ + Q_OBJECT +public: + explicit ListenerWorker(QObject *parent = 0); + + void setDevice(vita_device_t *device); + bool isConnected(); + void disconnect(); + int rebuildDatabase(); + + Database db; + +private: + uint16_t processAllObjects(CMAObject *parent, uint32_t handle); + void vitaEventSendObject(vita_event_t *event, int eventId); + void vitaEventSendObjectMetadata(vita_event_t *event, int eventId); + void vitaEventSendNumOfObject(vita_event_t *event, int eventId); + void vitaEventCancelTask(vita_event_t *event, int eventId); + void vitaEventSendHttpObjectFromURL(vita_event_t *event, int eventId); + void vitaEventUnimplementated(vita_event_t *event, int eventId); + void vitaEventSendObjectStatus(vita_event_t *event, int eventId); + void vitaEventSendObjectThumb(vita_event_t *event, int eventId); + void vitaEventDeleteObject(vita_event_t *event, int eventId); + void vitaEventGetSettingInfo(vita_event_t *event, int eventId); + void vitaEventSendHttpObjectPropFromURL(vita_event_t *event, int eventId); + void vitaEventSendPartOfObject(vita_event_t *event, int eventId); + void vitaEventOperateObject(vita_event_t *event, int eventId); + void vitaEventGetPartOfObject(vita_event_t *event, int eventId); + void vitaEventSendStorageSize(vita_event_t *event, int eventId); + void vitaEventCheckExistance(vita_event_t *event, int eventId); + void vitaEventGetTreatObject(vita_event_t *event, int eventId); + void vitaEventSendCopyConfirmationInfo(vita_event_t *event, int eventId); + void vitaEventSendObjectMetadataItems(vita_event_t *event, int eventId); + void vitaEventSendNPAccountInfo(vita_event_t *event, int eventId); + void vitaEventRequestTerminate(vita_event_t *event, int eventId); + + vita_device_t *device; + volatile bool connected; + static const metadata_t g_thumbmeta; + +signals: + void refreshDatabase(); + +private slots: + void process(); +}; + +#endif // LISTENERWORKER_H diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..652352d --- /dev/null +++ b/main.cpp @@ -0,0 +1,69 @@ +/* + * QCMA: Cross-platform content manager assistant for the PS Vita + * + * Copyright (C) 2013 Codestation + * + * 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 + +#include "singleapplication.h" +#include "mainwidget.h" + +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) +void noMessageOutput(QtMsgType type, const QMessageLogContext &, const QString & str) +{ + const char * msg = str.toStdString().c_str(); +#else +void noMessageOutput(QtMsgType type, const char *msg) +{ +#endif + Q_UNUSED(type); + Q_UNUSED(msg); +} + +int main(int argc, char *argv[]) +{ + if(SingleApplication::sendMessage(QObject::tr("A instance of QCMA is already running"))) { + return 0; + } + + SingleApplication app(argc, argv); + + if(!app.arguments().contains("--with-debug")) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) + qInstallMessageHandler(noMessageOutput); +#else + qInstallMsgHandler(noMessageOutput); +#endif + } + + qDebug()<<"From main thread: "<. + */ + +#include "mainwidget.h" +#include "qcma.h" + +#include +#include +#include +#include +#include +#include + +const QStringList MainWidget::path_list = QStringList() << "photoPath" << "musicPath" << "videoPath" << "appsPath" << "urlPath"; + +MainWidget::MainWidget(QWidget *parent) : + QWidget(parent) +{ +} + +void MainWidget::checkSettings() +{ + QSettings settings; + // make sure that all the paths are set, else show the config dialog + foreach(const QString &path, path_list) { + if(!settings.contains(path)) { + first_run = true; + dialog.show(); + return; + } + } + CmaWorker.start(); +} + +void MainWidget::dialogResult(int result) +{ + if(result == QDialog::Accepted) { + if(first_run) { + first_run = false; + CmaWorker.start(); + } + } else if(first_run) { + qApp->quit(); + } +} + +void MainWidget::deviceDisconnect() +{ + setTrayTooltip(tr("Disconnected")); + receiveMessage(tr("The device has been disconnected")); +} + +void MainWidget::connectSignals() +{ + connect(&dialog, SIGNAL(finished(int)), this, SLOT(dialogResult(int))); + connect(&CmaWorker, SIGNAL(createdPin(int)), this, SLOT(showPin(int))); + connect(&CmaWorker, SIGNAL(deviceConnected(QString)), this, SLOT(receiveMessage(QString))); + connect(&CmaWorker, SIGNAL(deviceConnected(QString)), this, SLOT(setTrayTooltip(QString))); + connect(&CmaWorker, SIGNAL(statusUpdated(QString)), this, SLOT(receiveMessage(QString))); + connect(&CmaWorker, SIGNAL(deviceDisconnected()), this, SLOT(deviceDisconnect())); + connect(&CmaWorker, SIGNAL(finished()), qApp, SLOT(quit())); +} + +void MainWidget::showPin(int pin) +{ + receiveMessage(QString(tr("Received PIN: %1").arg(QString::number(pin), 8, QChar('0')))); +} + +void MainWidget::prepareApplication() +{ + connectSignals(); + createTrayIcon(); + trayIcon->show(); + checkSettings(); +} + +void MainWidget::setTrayTooltip(QString message) +{ + trayIcon->setToolTip(message); +} + +void MainWidget::toggleWireless() +{ + QSettings settings; + if(wireless->isChecked()) { + wireless->setText(tr("&Wireless enabled")); + settings.setValue("wireless", true); + } else { + wireless->setText(tr("&Wireless disabled")); + settings.setValue("wireless", false); + } + CmaWorker.reload(); +} + +void MainWidget::createTrayIcon() +{ + QSettings settings; + wireless = new QAction(this); + wireless->setCheckable(true); + + if(settings.value("wireless", false).toBool()) { + wireless->setText(tr("&Wireless enabled")); + wireless->setChecked(true); + } else { + wireless->setText(tr("&Wireless disabled")); + } + + options = new QAction(tr("&Settings"), this); + reload = new QAction(tr("&Refresh database"), this); + quit = new QAction(tr("&Quit"), this); + + connect(options, SIGNAL(triggered()), &dialog, SLOT(open())); + connect(reload, SIGNAL(triggered()), &CmaWorker, SLOT(allowRefresh()), Qt::DirectConnection); + connect(quit, SIGNAL(triggered()), &CmaWorker, SLOT(stop()), Qt::DirectConnection); + connect(wireless, SIGNAL(triggered()), this, SLOT(toggleWireless())); + + QMenu *trayIconMenu = new QMenu(this); + trayIconMenu->addAction(options); + trayIconMenu->addAction(reload); + trayIconMenu->addAction(wireless); + trayIconMenu->addSeparator(); + trayIconMenu->addAction(quit); + + trayIcon = new QSystemTrayIcon(this); + trayIcon->setContextMenu(trayIconMenu); + trayIcon->setIcon(QIcon::fromTheme("image-loading")); +} + +void MainWidget::receiveMessage(QString message) +{ + if(trayIcon->isVisible()) { + trayIcon->showMessage(tr("Information"), message); + } +} + +MainWidget::~MainWidget() +{ + trayIcon->hide(); +} diff --git a/mainwidget.h b/mainwidget.h new file mode 100644 index 0000000..c48ac2d --- /dev/null +++ b/mainwidget.h @@ -0,0 +1,70 @@ +/* + * QCMA: Cross-platform content manager assistant for the PS Vita + * + * Copyright (C) 2013 Codestation + * + * 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 MAINWIDGET_H +#define MAINWIDGET_H + +#include "configwidget.h" +#include "qcma.h" + +#include +#include +#include + +class QMenu; + +namespace Ui +{ +class MainWidget; +} + +class MainWidget : public QWidget +{ + Q_OBJECT +public: + explicit MainWidget(QWidget *parent = 0); + ~MainWidget(); + + void prepareApplication(); + +private: + void createTrayIcon(); + void checkSettings(); + void connectSignals(); + + bool first_run; + QCMA CmaWorker; + ConfigWidget dialog; + QSystemTrayIcon *trayIcon; + QAction *quit; + QAction *reload; + QAction *options; + QAction *wireless; + const static QStringList path_list; + +private slots: + void deviceDisconnect(); + void dialogResult(int result); + void receiveMessage(QString message); + void setTrayTooltip(QString message); + void toggleWireless(); + void showPin(int pin); +}; + +#endif // MAINWIDGET_H diff --git a/psp2-updatelist.xml b/psp2-updatelist.xml new file mode 100644 index 0000000..9508ab6 --- /dev/null +++ b/psp2-updatelist.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/qcma.cpp b/qcma.cpp new file mode 100644 index 0000000..41371f8 --- /dev/null +++ b/qcma.cpp @@ -0,0 +1,174 @@ +/* + * QCMA: Cross-platform content manager assistant for the PS Vita + * + * Copyright (C) 2013 Codestation + * + * 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 "qcma.h" + +#include "capability.h" +#include "listenerworker.h" +#include "wirelessworker.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#define RETRY_TIMEOUT 2000 + +QCMA::QCMA(QObject *parent) : + BaseWorker(parent) +{ + exit_thread = false; +} + +vita_device_t *QCMA::connectWireless() +{ + int num_tries = 0; + vita_device_t *vita = NULL; + WirelessWorker *broadcast = new WirelessWorker(); + + // bubble up the pin received signal + connect(broadcast, SIGNAL(createdPin(int)), this, SIGNAL(createdPin(int))); + + qDebug("Starting broadcast"); + broadcast->start(); + + QMutex mutex; + QString hostname = QHostInfo::localHostName(); + WirelessWorker::info.name = hostname.toUtf8().data(); + + while(!break_loop && (vita = VitaMTP_Get_First_Wireless_Vita( + &WirelessWorker::info, 0, 10, + WirelessWorker::deviceRegistered, + WirelessWorker::generatePin)) == NULL) { + qDebug("Error connecting to device. Attempt %d", ++num_tries); + //FIXME: use a proper sleep function + mutex.lock(); + waitCondition.wait(&mutex, RETRY_TIMEOUT); + mutex.unlock(); + } + qDebug("Stopping broadcast"); + broadcast->stopBroadcast(); + return vita; +} + +vita_device_t *QCMA::connectUsb() +{ + int num_tries = 0; + vita_device_t *vita = NULL; + QMutex mutex; + while(!break_loop && (vita = VitaMTP_Get_First_USB_Vita()) == NULL) { + qDebug("No Vita found. Attempt %d", ++num_tries); + //FIXME: use a proper sleep function + mutex.lock(); + waitCondition.wait(&mutex, RETRY_TIMEOUT); + mutex.unlock(); + } + + return vita; +} + +void QCMA::process() +{ + QSettings settings; + + qDebug() << "From QCMA: "<< QThread::currentThreadId(); + + while(!exit_thread) { + break_loop = false; + VitaMTP_Set_Logging(VitaMTP_NONE); + bool wireless = settings.value("wireless", false).toBool(); + emit statusUpdated("Searching for device..."); + vita_device_t *device = wireless ? connectWireless() : connectUsb(); + if(!device) { + continue; + } + + qDebug("Vita connected: id %s", VitaMTP_Get_Identification(device)); + + // create a listener thread to receive the vita events + ListenerWorker *listener = new ListenerWorker(); + listener->setDevice(device); + connect(listener, SIGNAL(refreshDatabase()), this, SLOT(allowRefresh()), Qt::DirectConnection); + connect(listener, SIGNAL(finished()), this, SIGNAL(deviceDisconnected()), Qt::DirectConnection); + connect(listener, SIGNAL(finished()), this, SLOT(reload()), Qt::DirectConnection); + listener->start(); + qDebug("Exchanging info with vita"); + DeviceCapability *vita_info = new DeviceCapability(); + + if(!vita_info->exchangeInfo(device)) { + qCritical("Error while exchanging info with the vita"); + VitaMTP_SendHostStatus(device, VITA_HOST_STATUS_EndConnection); + VitaMTP_Release_Device(device); + sleep(RETRY_TIMEOUT); + continue; + } + + // Conection successful, inform the user + emit deviceConnected(QString(tr("Connected to ")) + vita_info->getOnlineId()); + + delete vita_info; + QElapsedTimer timer; + qint64 sec; + + while(listener->isConnected()) { + sema.acquire(); + if(break_loop) { + break; + } + qDebug("Updating database"); + timer.start(); + int scanned_objects = listener->rebuildDatabase(); + sec = timer.elapsed(); + qDebug("Scanned %i objects in %lli milliseconds", scanned_objects, sec); + emit statusUpdated(QString("Scanned %1 objects in %2 milliseconds").arg(scanned_objects, sec)); + } + + qDebug("Ending device connection..."); + VitaMTP_SendHostStatus(device, VITA_HOST_STATUS_EndConnection); + VitaMTP_Release_Device(device); + } + qDebug("QCMA thread end"); + emit finished(); +} + +void QCMA::allowRefresh() +{ + sema.release(); +} + +void QCMA::reload() +{ + qDebug("Stopping QCMA workers..."); + break_loop = true; + waitCondition.wakeAll(); + sema.release(); +} + +void QCMA::stop() +{ + exit_thread = true; + reload(); + qDebug("Stopping QCMA thread..."); + emit stopCMA(); +} diff --git a/qcma.h b/qcma.h new file mode 100644 index 0000000..9ebdea6 --- /dev/null +++ b/qcma.h @@ -0,0 +1,70 @@ +/* + * QCMA: Cross-platform content manager assistant for the PS Vita + * + * Copyright (C) 2013 Codestation + * + * 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 QCMA_H +#define QCMA_H + +#include "baseworker.h" + +#include +#include +#include +#include +#include +#include + +class QMenu; + +extern "C" { +#include +} + +#define QCMA_VERSION_STRING "QCMA 0.1" + +class QCMA : public BaseWorker +{ + Q_OBJECT +public: + explicit QCMA(QObject *parent = 0); + +private: + QSemaphore sema; + QWaitCondition waitCondition; + volatile bool break_loop; + volatile bool exit_thread; + vita_device_t *connectUsb(); + vita_device_t *connectWireless(); + +signals: + void createdPin(int); + void deviceConnected(QString); + void statusUpdated(QString); + void deviceDisconnected(); + void stopCMA(); + +public slots: + void stop(); + void reload(); + +protected slots: + void allowRefresh(); + virtual void process(); +}; + +#endif // QCMA_H diff --git a/qcma.pro b/qcma.pro new file mode 100644 index 0000000..c09f266 --- /dev/null +++ b/qcma.pro @@ -0,0 +1,58 @@ +#------------------------------------------------- +# +# Project created by QtCreator 2013-07-23T15:34:17 +# +#------------------------------------------------- + +QT += core \ + gui \ + widgets \ + network + +TARGET = qcma + +TEMPLATE = app + +SOURCES += main.cpp \ + qcma.cpp \ + wirelessworker.cpp \ + listenerworker.cpp \ + capability.cpp \ + database.cpp \ + cmaobject.cpp \ + cmarootobject.cpp \ + utils.cpp \ + mainwidget.cpp \ + configwidget.cpp \ + singleapplication.cpp \ + baseworker.cpp \ + sforeader.cpp + +HEADERS += \ + qcma.h \ + wirelessworker.h \ + listenerworker.h \ + capability.h \ + database.h \ + cmaobject.h \ + cmarootobject.h \ + utils.h \ + mainwidget.h \ + configwidget.h \ + singleapplication.h \ + baseworker.h \ + sforeader.h + +CONFIG += link_pkgconfig +PKGCONFIG += libvitamtp libmediainfo + +QMAKE_CXXFLAGS += -Wno-write-strings -Wall + +RESOURCES += \ + qcmares.qrc + +OTHER_FILES += \ + psp2-updatelist.xml + +FORMS += \ + configwidget.ui diff --git a/qcmares.qrc b/qcmares.qrc new file mode 100644 index 0000000..b296c1b --- /dev/null +++ b/qcmares.qrc @@ -0,0 +1,5 @@ + + + psp2-updatelist.xml + + diff --git a/sforeader.cpp b/sforeader.cpp new file mode 100644 index 0000000..170aa35 --- /dev/null +++ b/sforeader.cpp @@ -0,0 +1,49 @@ +/* + * QCMA: Cross-platform content manager assistant for the PS Vita + * + * Copyright (C) 2013 Codestation + * + * 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 "sforeader.h" + +#include + +SfoReader::SfoReader() +{ +} + +bool SfoReader::load(const QString &path) { + QFile file(path); + if(file.open(QIODevice::ReadOnly)) { + data = file.readAll(); + key_offset = data.constData(); + header = (sfo_header *)key_offset; + index = (sfo_index *)(key_offset + sizeof(sfo_header)); + return true; + } + return false; +} + +const char *SfoReader::value(const char *key, const char *defaultValue) { + const char *base_key = key_offset + header->key_offset; + for(uint i = 0; i < header->pair_count; i++) { + const char *curr_key = base_key + index[i].key_offset; + if(strcmp(key, curr_key) == 0) { + return key_offset + header->value_offset + index[i].data_offset; + } + } + return defaultValue; +} diff --git a/sforeader.h b/sforeader.h new file mode 100644 index 0000000..639f5a3 --- /dev/null +++ b/sforeader.h @@ -0,0 +1,56 @@ +/* + * QCMA: Cross-platform content manager assistant for the PS Vita + * + * Copyright (C) 2013 Codestation + * + * 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 SFOREADER_H +#define SFOREADER_H + +#include + +class SfoReader +{ +public: + SfoReader(); + bool load(const QString &path); + const char *value(const char *key, const char *defaultValue); + +private: + typedef struct { + quint16 key_offset; + uchar alignment; + uchar data_type; + quint32 value_size; + quint32 value_size_with_padding; + quint32 data_offset; + }__attribute__((packed)) sfo_index; + + typedef struct { + char id[4]; + quint32 version; + quint32 key_offset; + quint32 value_offset; + quint32 pair_count; + }__attribute__((packed)) sfo_header; + + QByteArray data; + const char *key_offset; + const sfo_header *header; + const sfo_index *index; +}; + +#endif // SFOREADER_H diff --git a/singleapplication.cpp b/singleapplication.cpp new file mode 100644 index 0000000..0b5532f --- /dev/null +++ b/singleapplication.cpp @@ -0,0 +1,78 @@ +/* + * QCMA: Cross-platform content manager assistant for the PS Vita + * + * Copyright (C) 2013 Codestation + * + * 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 "singleapplication.h" + +#include +#include + +const int SingleApplication::timeout = 500; +const QString SingleApplication::SHARED_KEY = "QCMA_KEY"; + +SingleApplication::SingleApplication(int &argc, char **argv) : + QApplication(argc, argv) +{ + server = new QLocalServer(this); + connect(server, SIGNAL(newConnection()), this, SLOT(receiveMessage())); +#ifdef Q_OS_UNIX + // Make sure that the socket file is deleted + QFile address(QString("/tmp/" + SHARED_KEY)); + + if(address.exists()) { + address.remove(); + } + +#endif + server->listen(SHARED_KEY); +} + +void SingleApplication::receiveMessage() +{ + QLocalSocket *socket = server->nextPendingConnection(); + + if(!socket->waitForReadyRead(timeout)) { + qDebug() << socket->errorString(); + return; + } + + QByteArray byteArray = socket->readAll(); + QString message = QString::fromUtf8(byteArray.constData()); + emit messageAvailable(message); + socket->disconnectFromServer(); +} + +bool SingleApplication::sendMessage(const QString &message) +{ + QLocalSocket socket; + socket.connectToServer(SHARED_KEY, QIODevice::WriteOnly); + + if(!socket.waitForConnected(timeout)) { + return false; + } + + socket.write(message.toUtf8()); + + if(!socket.waitForBytesWritten(timeout)) { + qDebug() << socket.errorString(); + return false; + } + + socket.disconnectFromServer(); + return true; +} diff --git a/singleapplication.h b/singleapplication.h new file mode 100644 index 0000000..1fee13a --- /dev/null +++ b/singleapplication.h @@ -0,0 +1,50 @@ +/* + * QCMA: Cross-platform content manager assistant for the PS Vita + * + * Copyright (C) 2013 Codestation + * + * 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 SINGLEAPPLICATION_H +#define SINGLEAPPLICATION_H + +#include +#include +#include +#include + +class SingleApplication : public QApplication +{ + Q_OBJECT +public: + explicit SingleApplication(int &argc, char **argv); + + static bool sendMessage(const QString &message); + +private: + QLocalServer *server; + + static const int timeout; + static const QString SHARED_KEY; + +signals: + void messageAvailable(QString message); + + +public slots: + void receiveMessage(); +}; + +#endif // SINGLEAPPLICATION_H diff --git a/utils.cpp b/utils.cpp new file mode 100644 index 0000000..1cd9559 --- /dev/null +++ b/utils.cpp @@ -0,0 +1,81 @@ +/* + * QCMA: Cross-platform content manager assistant for the PS Vita + * + * Copyright (C) 2013 Codestation + * + * 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 "utils.h" + +#include +#include + +extern "C" { +#include +} + +#ifdef Q_OS_WIN32 +#include +#else +#include +#endif + +bool getDiskSpace(const QString &dir, quint64 *free, quint64 *total) +{ +#ifdef Q_OS_WIN32 + + if(GetDiskFreeSpaceExW(dir.toStdWString().c_str(), free, total, NULL) != 0) { + return true; + } + +#else + struct statvfs64 stat; + + if(statvfs64(dir.toUtf8().data(), &stat) == 0) { + *total = stat.f_frsize * stat.f_blocks; + *free = stat.f_frsize * stat.f_bfree; + return true; + } + +#endif + return false; +} + +bool removeRecursively(const QString &dirName) +{ +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) + return QDir(dirName).removeRecursively(); +#else + bool result = false; + QDir dir(dirName); + + if(dir.exists(dirName)) { + Q_FOREACH(QFileInfo info, dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden | QDir::AllDirs | QDir::Files, QDir::DirsFirst)) { + if(info.isDir()) { + result = removeRecursively(info.absoluteFilePath()); + } else { + result = QFile::remove(info.absoluteFilePath()); + } + + if(!result) { + return result; + } + } + result = dir.rmdir(dirName); + } + + return result; +#endif +} diff --git a/utils.h b/utils.h new file mode 100644 index 0000000..2073677 --- /dev/null +++ b/utils.h @@ -0,0 +1,28 @@ +/* + * QCMA: Cross-platform content manager assistant for the PS Vita + * + * Copyright (C) 2013 Codestation + * + * 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 UTILS_H +#define UTILS_H + +#include + +bool removeRecursively(const QString &dirName); +bool getDiskSpace(const QString &dir, quint64 *free, quint64 *total); + +#endif // UTILS_H diff --git a/wirelessworker.cpp b/wirelessworker.cpp new file mode 100644 index 0000000..30a94c6 --- /dev/null +++ b/wirelessworker.cpp @@ -0,0 +1,76 @@ +/* + * QCMA: Cross-platform content manager assistant for the PS Vita + * + * Copyright (C) 2013 Codestation + * + * 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 "qcma.h" +#include "wirelessworker.h" + +#include +#include + +#define QCMA_REQUEST_PORT 9309 + +wireless_host_info_t WirelessWorker::info = {"00000000-0000-0000-0000-000000000000", "win", NULL, QCMA_REQUEST_PORT}; + +WirelessWorker *WirelessWorker::this_object = NULL; + +WirelessWorker::WirelessWorker(QObject *parent) : + BaseWorker(parent) +{ + qsrand(QTime::currentTime().msec()); + this_object = this; +} + +int WirelessWorker::deviceRegistered(const char *deviceid) +{ + qDebug("Got connection request from %s", deviceid); + return 1; +} + +int WirelessWorker::generatePin(wireless_vita_info_t *info, int *p_err) +{ + qDebug("Registration request from %s (MAC: %s)", info->name, info->mac_addr); + int pin = qrand() % 100000000; + qDebug("Your registration PIN for %s is: %08d", info->name, pin); + *p_err = 0; + emit this_object->createdPin(pin); + return pin; +} + +void WirelessWorker::process() +{ + qDebug() <<" Wireless thread ID: "<< QThread::currentThreadId(); + + wireless_host_info_t broadcast_info = info; + QString hostname = QHostInfo::localHostName(); + broadcast_info.name = hostname.toUtf8().data(); + + qDebug("Starting CMA wireless broadcast..."); + if(VitaMTP_Broadcast_Host(&broadcast_info, 0) < 0) { + qCritical( "An error occured during broadcast. Exiting."); + } else { + qDebug("Broadcast ended."); + } + qDebug("WirelessrWorker thread end."); + emit finished(); +} + +void WirelessWorker::stopBroadcast() +{ + VitaMTP_Stop_Broadcast(); +} diff --git a/wirelessworker.h b/wirelessworker.h new file mode 100644 index 0000000..4305e06 --- /dev/null +++ b/wirelessworker.h @@ -0,0 +1,58 @@ +/* + * QCMA: Cross-platform content manager assistant for the PS Vita + * + * Copyright (C) 2013 Codestation + * + * 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 WIRELESSWORKER_H +#define WIRELESSWORKER_H + +#include "baseworker.h" + +#include +#include + +extern "C" { +#include +} + +class WirelessWorker : public BaseWorker +{ + Q_OBJECT +public: + explicit WirelessWorker(QObject *parent = 0); + + static int deviceRegistered(const char *deviceid); + static int generatePin(wireless_vita_info_t *info, int *p_err); + + static wireless_host_info_t info; + + +private: + //used to emit a signal from a static method + static WirelessWorker *this_object; + +signals: + void createdPin(int); + +public slots: + void stopBroadcast(); + +protected slots: + void process(); +}; + +#endif // WIRELESSWORKER_H