diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..e62ec04c
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,674 @@
+GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/menu/alias/alias.yml b/menu/alias/alias.yml
new file mode 100644
index 00000000..48ffa1a3
--- /dev/null
+++ b/menu/alias/alias.yml
@@ -0,0 +1,140 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+---
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: Install PGVPN
+ template:
+ src: pgvpn
+ dest: /bin/pgvpn
+ force: yes
+ mode: 0775
+
+ - name: Install Status UFSMonitor
+ template:
+ src: sufs
+ dest: /bin/sufs
+ force: yes
+ mode: 0775
+
+ - name: Nano UFSMonitor
+ template:
+ src: nufs
+ dest: /bin/nufs
+ force: yes
+ mode: 0775
+
+ ###### Install PTSupdate
+
+ - name: PTSupdate
+ template:
+ src: ptsupdate
+ dest: /bin/ptsupdate
+ force: yes
+ mode: 0775
+
+ ###### Install PGBlitz
+
+ - name: PlexGuide
+ template:
+ src: plexguide
+ dest: /bin/plexguide
+ force: yes
+ mode: 0775
+ owner: root
+
+ - name: PG
+ template:
+ src: pts
+ dest: /bin/pts
+ force: yes
+ mode: 0775
+ owner: root
+
+ - name: PGBlitz
+ template:
+ src: pgblitz
+ dest: /bin/pgblitz
+ force: yes
+ mode: 0775
+ owner: root
+
+ ###### Server reboot
+
+ - name: server reboot
+ template:
+ src: reboot
+ dest: /bin/reboot
+ force: yes
+ mode: 0775
+
+ ###### Check list of services
+
+ - name: list systemd services
+ template:
+ src: slist
+ dest: /bin/slist
+ force: yes
+ mode: 0775
+ owner: root
+
+ ###### Server update
+
+ - name: update server
+ template:
+ src: update
+ dest: /bin/update
+ force: yes
+ mode: 0775
+
+ ###### Server upgrade
+
+ - name: upgrade server
+ template:
+ src: upgrade
+ dest: /bin/upgrade
+ force: yes
+ mode: 0775
+ owner: root
+
+ ###### Install app
+
+ - name: install appname
+ template:
+ src: install
+ dest: /bin/install
+ force: yes
+ mode: 0775
+ owner: root
+
+ ###### Autoremove installed app packages
+
+ - name: autoremove unused packages after app install
+ template:
+ src: autoremove
+ dest: /bin/autoremove
+ force: yes
+ mode: 0775
+ owner: root
+
+ - name: Prune docker containers appname
+ template:
+ src: prune
+ dest: /bin/prune
+ force: yes
+ mode: 0775
+ owner: root
+
+ - name: Install PGFork
+ template:
+ src: pgfork
+ dest: /bin/pgfork
+ force: yes
+ mode: 0775
+
diff --git a/menu/alias/templates/autoremove b/menu/alias/templates/autoremove
new file mode 100644
index 00000000..316e25eb
--- /dev/null
+++ b/menu/alias/templates/autoremove
@@ -0,0 +1,9 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+
+sudo apt autoremove
diff --git a/menu/alias/templates/install b/menu/alias/templates/install
new file mode 100644
index 00000000..36b1d1ba
--- /dev/null
+++ b/menu/alias/templates/install
@@ -0,0 +1,8 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq - Sub7Seven
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+sudo apt install
diff --git a/menu/alias/templates/nufs b/menu/alias/templates/nufs
new file mode 100644
index 00000000..97264517
--- /dev/null
+++ b/menu/alias/templates/nufs
@@ -0,0 +1,9 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq - Sub7Seven
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+
+sudo nano /etc/systemd/system/ufsmonitor.service && systemctl daemon-reload
diff --git a/menu/alias/templates/pg b/menu/alias/templates/pg
new file mode 100644
index 00000000..76ba9ba8
--- /dev/null
+++ b/menu/alias/templates/pg
@@ -0,0 +1,45 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+file="/var/pgblitz/pg.number"
+if [ -e "$file" ]; then
+ check="$(cat /var/pgblitz/pg.number | head -c 1)"
+ if [[ "$check" == "5" || "$check" == "6" || "$check" == "7" ]]; then
+
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+🌎 INSTALLER BLOCK: Notice
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+We detected PG Version $check is running! Per the instructions, PG 8
+must be installed on a FRESH BOX! Exiting!
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+EOF
+ exit
+ fi
+fi
+
+source /opt/plexguide/menu/functions/functions.sh
+source /opt/plexguide/menu/functions/start.sh
+source /opt/plexguide/menu/functions/install.sh
+
+mkdir -p /opt/plexguide/roles/log
+mkdir -p /var/plexguide/logs
+mkdir -p /opt/appdata/plexguide
+
+sudocheck
+missingpull
+
+if [[ ! -e "/bin/pgblitz" ]]; then
+ cp /opt/plexguide/menu/alias/templates/pgblitz /bin
+fi
+
+chown 1000:1000 /bin/pgblitz &>/dev/null &
+chmod 0755 /bin/pgblitz &>/dev/null &
+# pg deploy contains pgdeploy at end
+pginstall
diff --git a/menu/alias/templates/pgblitz b/menu/alias/templates/pgblitz
new file mode 100644
index 00000000..36c228d4
--- /dev/null
+++ b/menu/alias/templates/pgblitz
@@ -0,0 +1,39 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+file="/var/plexguide/pg.number"
+if [ -e "$file" ]; then
+ check="$(cat /var/plexguide/pg.number | head -c 1)"
+ if [[ "$check" == "5" || "$check" == "6" || "$check" == "7" ]]; then
+
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+🌎 INSTALLER BLOCK: Notice
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+We detected PG Version $check is running! Per the instructions, PG 8
+must be installed on a FRESH BOX! Exiting!
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+EOF
+ exit
+ fi
+fi
+
+source /opt/plexguide/menu/functions/functions.sh
+source /opt/plexguide/menu/functions/start.sh
+source /opt/plexguide/menu/functions/install.sh
+
+mkdir -p /opt/plexguide/roles/log
+mkdir -p /var/plexguide/logs
+mkdir -p /opt/appdata/plexguide
+
+sudocheck
+missingpull
+
+# pg deploy contains pgdeploy at end
+pginstall
diff --git a/menu/alias/templates/pgfork b/menu/alias/templates/pgfork
new file mode 100644
index 00000000..25449d7f
--- /dev/null
+++ b/menu/alias/templates/pgfork
@@ -0,0 +1,31 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq - Sub7Seven
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+if [[ $EUID -ne 0 ]]; then
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+⛔️ You Must Execute as a SUDO USER (with sudo) or as ROOT!
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+EOF
+ exit 1
+fi
+
+### Execut YML
+ansible-playbook /opt/plexguide/menu/pgfork/main.yml
+
+tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+✅️ SYSTEM MESSAGE: Installed User's Forked Version of PG! Standby!
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+sleep 1.5
+file="/var/plexguide/community.app"
+if [ -e "$file" ]; then rm -rf /var/plexguide/community.app; fi
+bash /opt/plexguide/menu/interface/ending.sh
diff --git a/menu/alias/templates/pgvpn b/menu/alias/templates/pgvpn
new file mode 100644
index 00000000..91488cc0
--- /dev/null
+++ b/menu/alias/templates/pgvpn
@@ -0,0 +1,15 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq - Sub7Seven
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+echo ""
+echo "-----------------------------------------------------"
+echo "SYSTEM MESSAGE: Please Copy Your Information"
+echo "-----------------------------------------------------"
+echo ""
+cat /opt/appdata/plexguide/vpn.info
+echo ""
+echo "Config Info: Visit http://pgvpn.pgblitz.com or WIKI"
diff --git a/menu/alias/templates/plexguide b/menu/alias/templates/plexguide
new file mode 100644
index 00000000..8206d134
--- /dev/null
+++ b/menu/alias/templates/plexguide
@@ -0,0 +1,44 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+file="/var/plexguide/pg.number"
+if [ -e "$file" ]; then
+ check="$(cat /var/plexguide/pg.number | head -c 1)"
+ if [[ "$check" == "5" || "$check" == "6" || "$check" == "7" ]]; then
+
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+🌎 INSTALLER BLOCK: Notice
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+We detected PG Version $check is running! Per the instructions, PG 8
+must be installed on a FRESH BOX! Exiting!
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+EOF
+ exit
+ fi
+fi
+
+source /opt/plexguide/menu/functions/functions.sh
+source /opt/plexguide/menu/functions/start.sh
+source /opt/plexguide/menu/functions/install.sh
+
+mkdir -p /opt/plexguide/roles/log
+mkdir -p /var/plexguide/logs
+mkdir -p /opt/appdata/plexguide
+
+sudocheck
+missingpull
+
+if [[ ! -e "/bin/pgblitz" ]]; then
+ cp /opt/plexguide/menu/alias/templates/pgblitz /bin/ &>/dev/null &
+ chown 1000:1000 /bin/pgblitz
+ chmod 0755 /bin/pgblitz
+fi
+# pg deploy contains pgdeploy at end
+pginstall
diff --git a/menu/alias/templates/prune b/menu/alias/templates/prune
new file mode 100644
index 00000000..d0337851
--- /dev/null
+++ b/menu/alias/templates/prune
@@ -0,0 +1,8 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq - Sub7Seven
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+sudo docker system prune --volumes
diff --git a/menu/alias/templates/pts b/menu/alias/templates/pts
new file mode 100644
index 00000000..36c228d4
--- /dev/null
+++ b/menu/alias/templates/pts
@@ -0,0 +1,39 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+file="/var/plexguide/pg.number"
+if [ -e "$file" ]; then
+ check="$(cat /var/plexguide/pg.number | head -c 1)"
+ if [[ "$check" == "5" || "$check" == "6" || "$check" == "7" ]]; then
+
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+🌎 INSTALLER BLOCK: Notice
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+We detected PG Version $check is running! Per the instructions, PG 8
+must be installed on a FRESH BOX! Exiting!
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+EOF
+ exit
+ fi
+fi
+
+source /opt/plexguide/menu/functions/functions.sh
+source /opt/plexguide/menu/functions/start.sh
+source /opt/plexguide/menu/functions/install.sh
+
+mkdir -p /opt/plexguide/roles/log
+mkdir -p /var/plexguide/logs
+mkdir -p /opt/appdata/plexguide
+
+sudocheck
+missingpull
+
+# pg deploy contains pgdeploy at end
+pginstall
diff --git a/menu/alias/templates/ptsupdate b/menu/alias/templates/ptsupdate
new file mode 100644
index 00000000..f25980b5
--- /dev/null
+++ b/menu/alias/templates/ptsupdate
@@ -0,0 +1,13 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+source /opt/plexguide/menu/functions/functions.sh
+source /opt/plexguide/menu/functions/start.sh
+
+sudocheck
+missingpull
+exitcheck
diff --git a/menu/alias/templates/reboot b/menu/alias/templates/reboot
new file mode 100644
index 00000000..b78e4606
--- /dev/null
+++ b/menu/alias/templates/reboot
@@ -0,0 +1,8 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq - Sub7Seven
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+sudo reboot
diff --git a/menu/alias/templates/slist b/menu/alias/templates/slist
new file mode 100644
index 00000000..2af3b3b9
--- /dev/null
+++ b/menu/alias/templates/slist
@@ -0,0 +1,8 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq - Sub7Seven
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+sudo ls /etc/systemd/system
diff --git a/menu/alias/templates/sst2 b/menu/alias/templates/sst2
new file mode 100644
index 00000000..954c8924
--- /dev/null
+++ b/menu/alias/templates/sst2
@@ -0,0 +1,8 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq - Sub7Seven
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+sudo systemctl status supertransfer2
diff --git a/menu/alias/templates/sufs b/menu/alias/templates/sufs
new file mode 100644
index 00000000..48370360
--- /dev/null
+++ b/menu/alias/templates/sufs
@@ -0,0 +1,8 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq - Sub7Seven
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+sudo systemctl status ufsmonitor
diff --git a/menu/alias/templates/update b/menu/alias/templates/update
new file mode 100644
index 00000000..fcf27cf1
--- /dev/null
+++ b/menu/alias/templates/update
@@ -0,0 +1,8 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq - Sub7Seven
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+sudo apt update -y
diff --git a/menu/alias/templates/upgrade b/menu/alias/templates/upgrade
new file mode 100644
index 00000000..582c6740
--- /dev/null
+++ b/menu/alias/templates/upgrade
@@ -0,0 +1,8 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq - Sub7Seven
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+sudo apt upgrade -y
diff --git a/menu/amazonaws/amazonaws.sh b/menu/amazonaws/amazonaws.sh
new file mode 100644
index 00000000..dbfc1b69
--- /dev/null
+++ b/menu/amazonaws/amazonaws.sh
@@ -0,0 +1,248 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+test=$(hcloud server list)
+if [ "$test" == "" ]; then
+
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+⛔️ WARNING! - You Must Input an API from Hetzner First!
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+⚡ Reference: http://hcloud.pgblitz.com
+
+* Activate a Hetzner Cloud Account and Create a Project
+* Click Access (left hand side) and then click API Tokens
+* Create a Token and Save It (and paste below here)
+* Not Ready? Just Something & Press [ENTER]
+
+EOF
+ hcloud context create plexguide
+
+ test=$(hcloud server list)
+ if [ "$test" == "" ]; then
+ hcloud context delete plexguide
+ exit
+ fi
+
+fi
+
+# Start Process
+tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+🌎 PG - Hetzner's Cloud Generator
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+⚡ Reference: http://hcloud.pgblitz.com
+
+[1] Deploy a New Server
+[2] Destory a Server
+A - List Server Info
+B - Display Inital Server Passwords
+Z - Exit
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+read -p 'Type a Number | Press [ENTER]: ' typed /opt/appdata/plexguide/server.info
+
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+↘️ PG - New Server Information - [$name]
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+EOF
+ cat /opt/appdata/plexguide/server.info
+
+ # Creates Log
+ touch /opt/appdata/plexguide/server.store
+ cat /opt/appdata/plexguide/server.info >>/opt/appdata/plexguide/server.store
+ echo "Server Name: $name" >>/opt/appdata/plexguide/server.store
+ echo "" >>/opt/appdata/plexguide/server.store
+
+ # Variable Info
+ serverip=$(cat /opt/appdata/plexguide/server.info | tail -n +3 | head -n 1 | cut -d " " -f2-)
+ initialpw=$(cat /opt/appdata/plexguide/server.info | tail -n +4 | cut -d " " -f3-)
+
+ tee <<-EOF
+
+⚠️ To Reach Your Server >>> EXIT PG >>> TYPE: pg-$name ⚠️
+
+✅️ [IMPORTANT NOTE]
+
+Wait for one minute for the server to boot! Typing pg-$name will
+display your initial password! Also can manually by typing:
+
+Command: ssh root@$serverip
+FIRST TIME LOGIN - Initial Password: $initialpw
+
+EOF
+ read -p 'Press [ENTER] to Exit ' fill >/bin/pg-$name
+ echo "echo ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" >/bin/pg-$name
+ echo "echo '↘️ Server - $name | Initial Password $initialpw'" >>/bin/pg-$name
+ echo "echo ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" >>/bin/pg-$name
+ echo "echo '✅️ Donate @ donate.pgblitz.com - Helps Costs & Mrs. Admin - #1 Enemy!'" >>/bin/pg-$name
+ echo "echo ''" >>/bin/pg-$name
+ echo "ssh root@$serverip" >>/bin/pg-$name
+ chmod 777 /bin/pg-$name
+ chown 1000:1000 /bin/pg-$name
+
+ bash /opt/plexguide/menu/hetzner/hetzner.sh
+ exit
+
+elif [ "$typed" == "A" ] || [ "$typed" == "a" ]; then
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+↘️ PG - Hetzner Server Cloud List
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+Server Name
+━━━━━━━━━━━
+EOF
+ hcloud server list | tail -n +2 | cut -d " " -f2- | cut -d " " -f2- | cut -d " " -f2-
+ echo
+ read -p 'Press [ENTER] to Continue! ' typed ")
+ if [ "$next" == "0" ]; then
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+⛔️ PG - Server: $destroy - Does Not Exist!
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+EOF
+ read -p 'Press [ENTER] to Continue! ' typed old)
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+
+ touch /opt/appdata/plexguide/server.store
+ tac -r /opt/appdata/plexguide/server.store
+ echo "" &
+ echo ""
+ read -p 'Press [ENTER] to Continue! ' corn " | cut -d " " -f2- | cut -d " " -f2- | cut -d " " -f2-)
+#ipcheck=$(echo $check | awk '{ print $3 }')
+#⛔️ WARNING! - Must Configure RClone First /w >>> gdrive
+# read -n 1 -s -r -p "Press [ANY] Key to Continue "
diff --git a/menu/appguard/appguard.sh b/menu/appguard/appguard.sh
new file mode 100644
index 00000000..0c843498
--- /dev/null
+++ b/menu/appguard/appguard.sh
@@ -0,0 +1,60 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+
+# KEY VARIABLE RECALL & EXECUTION
+program=$(cat /tmp/program_var)
+mkdir -p /var/plexguide/cron/
+mkdir -p /opt/appdata/plexguide/cron
+# FUNCTIONS START ##############################################################
+source /opt/plexguide/menu/functions/functions.sh
+
+# FIRST QUESTION
+question1() {
+ appguard=$(cat /var/plexguide/server.ht)
+ if [ "$appguard" == "" ]; then
+ guard="DISABLED" && opp="Enable"
+ else guard="ENABLED" && opp="Disable"; fi
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+🌎 Welcome to AppGuard!
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+⚡ Reference: http://appguard.pgblitz.com
+
+Currently: [$guard]
+
+1. $opp AppGuard
+Z. Exit
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+
+ read -p 'Type a Number | Press [ENTER]: ' typed /var/plexguide/server.ht; fi
+ bash /opt/plexguide/menu/appguard/rebuild.sh
+ elif [[ "$typed" == "z" || "$typed" == "Z" ]]; then
+ exit
+ else badinput1; fi
+}
+
+# FUNCTIONS END ##############################################################
+
+break=off && while [ "$break" == "off" ]; do question1; done
diff --git a/menu/appguard/rebuild.sh b/menu/appguard/rebuild.sh
new file mode 100644
index 00000000..487b1b1d
--- /dev/null
+++ b/menu/appguard/rebuild.sh
@@ -0,0 +1,56 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+docker ps -a --format "{{.Names}}" >/var/plexguide/container.running
+
+sed -i -e "/traefik/d" /var/plexguide/container.running
+sed -i -e "/watchtower/d" /var/plexguide/container.running
+sed -i -e "/wp-*/d" /var/plexguide/container.running
+sed -i -e "/plex/d" /var/plexguide/container.running
+sed -i -e "/emby/d" /var/plexguide/container.running
+sed -i -e "/jellyfin/d" /var/plexguide/container.running
+sed -i -e "/ombi/d" /var/plexguide/container.running
+sed -i -e "/oauth/d" /var/plexguide/container.running
+sed -i -e "/portainer/d" /var/plexguide/container.running
+sed -i -e "/dockergc/d" /var/plexguide/container.running
+
+count=$(wc -l /tmp/program_var
+
+ if [ -e "/opt/coreapps/apps/$app.yml" ]; then ansible-playbook /opt/coreapps/apps/$app.yml; fi
+ if [ -e "/opt/communityapps/$app.yml" ]; then ansible-playbook /opt/communityapps/apps/$app.yml; fi
+done
+
+echo ""
+tee <<-EOF
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+ ✅️ AppGuard - Completed! All Containers were Rebuilt!
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+EOF
+read -p 'Continue? | Press [ENTER] ' name /var/plexguide/final.choice
+
+#### Note How to Make It Select a Type - echo "removal" > /var/plexguide/type.choice
+program=$(cat /var/plexguide/type.choice)
+
+menu=$(echo "on")
+
+while [ "$menu" != "break" ]; do
+ menu=$(cat /var/plexguide/final.choice)
+
+ ### Loads Key Variables
+ bash /opt/plexguide/menu/interface/$program/var.sh
+ ### Loads Key Execution
+ ansible-playbook /opt/plexguide/menu/core/selection.yml
+ ### Executes Actions
+ bash /opt/plexguide/menu/interface/$program/file.sh
+
+ ### Calls Variable Again - Incase of Break
+ menu=$(cat /var/plexguide/final.choice)
+
+ if [ "$menu" == "break" ]; then
+ echo ""
+ echo "---------------------------------------------------"
+ echo "SYSTEM MESSAGE: User Selected to Exit the Interface"
+ echo "---------------------------------------------------"
+ echo ""
+ sleep .5
+ fi
+
+done
diff --git a/menu/core/selection.yml b/menu/core/selection.yml
new file mode 100644
index 00000000..5f0520a6
--- /dev/null
+++ b/menu/core/selection.yml
@@ -0,0 +1,182 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq - Sub7Seven
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+---
+- hosts: localhost
+ gather_facts: false
+ tasks:
+
+ - name: Recall Menu Type
+ shell: "cat /var/plexguide/type.choice"
+ register: menutemp
+
+ - name: "Set Facts for Menu"
+ set_fact:
+ menu: "{{menutemp.stdout}}"
+
+################ Key Variable to Set Menu Length
+ - name: "Default Blank Variables"
+ set_fact:
+ info1: "\n\n1. Exit Interface"
+ info2: ""
+ info3: ""
+ info4: ""
+ info5: ""
+ info6: ""
+ info7: ""
+ info8: ""
+ info9: ""
+
+ - name: Launch Container Primary Information
+ include_tasks: "../interface/{{menu}}/choice.yml"
+
+################ Blank Out Variable Recall
+ - name: Recall Menu Type
+ shell: "cat /var/plexguide/menu.number"
+ register: tempnumber
+
+ - name: "Blank Out Variable Recall"
+ set_fact:
+ numberselect: "{{tempnumber.stdout}}"
+
+################ Sets Choice Limitation
+ - name: "Choice Number {{numberselect}}"
+ set_fact:
+ choices: choice.user_input == "1" or
+ choice.user_input == "2"
+ when: numberselect == "2"
+
+ - name: "Choice Number {{numberselect}}"
+ set_fact:
+ choices: choice.user_input == "1" or
+ choice.user_input == "2" or
+ choice.user_input == "3"
+ when: numberselect == "3"
+
+ - name: "Choice Number {{numberselect}}"
+ set_fact:
+ choices: choice.user_input == "1" or
+ choice.user_input == "2" or
+ choice.user_input == "3" or
+ choice.user_input == "4"
+ when: numberselect == "4"
+
+ - name: "Choice Number {{numberselect}}"
+ set_fact:
+ choices: choice.user_input == "1" or
+ choice.user_input == "2" or
+ choice.user_input == "3" or
+ choice.user_input == "4" or
+ choice.user_input == "5"
+ when: numberselect == "5"
+
+ - name: "Choice Number {{numberselect}}"
+ set_fact:
+ choices: choice.user_input == "1" or
+ choice.user_input == "2" or
+ choice.user_input == "3" or
+ choice.user_input == "4" or
+ choice.user_input == "5" or
+ choice.user_input == "6"
+ when: numberselect == "6"
+
+ - name: "Choice Number {{numberselect}}"
+ set_fact:
+ choices: choice.user_input == "1" or
+ choice.user_input == "2" or
+ choice.user_input == "3" or
+ choice.user_input == "4" or
+ choice.user_input == "5" or
+ choice.user_input == "6" or
+ choice.user_input == "7"
+ when: numberselect == "7"
+
+ - name: "Choice Number {{numberselect}}"
+ set_fact:
+ choices: choice.user_input == "1" or
+ choice.user_input == "2" or
+ choice.user_input == "3" or
+ choice.user_input == "4" or
+ choice.user_input == "5" or
+ choice.user_input == "6" or
+ choice.user_input == "7" or
+ choice.user_input == "8"
+ when: numberselect == "8"
+
+ - name: "Choice Number {{numberselect}}"
+ set_fact:
+ choices: choice.user_input == "1" or
+ choice.user_input == "2" or
+ choice.user_input == "3" or
+ choice.user_input == "4" or
+ choice.user_input == "5" or
+ choice.user_input == "6" or
+ choice.user_input == "7" or
+ choice.user_input == "8" or
+ choice.user_input == "9"
+ when: numberselect == "9"
+################# Recalls Variables for Set Length
+ - name: PG Main Menu
+ pause:
+ prompt: "\n-------------------------------------------------------------
+ {{head1}}
+ {{head2}}
+ \n-------------------------------------------------------------
+ {{info1}}
+ {{info2}}
+ {{info3}}
+ {{info4}}
+ {{info5}}
+ {{info6}}
+ {{info7}}
+ {{info8}}
+ {{info9}}
+ \n\nType a [NUMBER] Choice & Press [ENTER]"
+ register: choice
+ until: "{{choices}}"
+ retries: 99
+ delay: 1
+
+ - name: Set Choice
+ set_fact:
+ fchoice: "{{choice.user_input}}"
+
+ - name: Exiting Interface
+ shell: "echo break > /var/plexguide/final.choice"
+ when: fchoice == "1"
+
+ - name: Choice 2 Selected
+ shell: "echo 2 > /var/plexguide/final.choice"
+ when: fchoice == "2"
+
+ - name: Choice 3 Selected
+ shell: "echo 3 > /var/plexguide/final.choice"
+ when: fchoice == "3"
+
+ - name: Choice 4 Selected
+ shell: "echo 4 > /var/plexguide/final.choice"
+ when: fchoice == "4"
+
+ - name: Choice 5 Selected
+ shell: "echo 5 > /var/plexguide/final.choice"
+ when: fchoice == "5"
+
+ - name: Choice 6 Selected
+ shell: "echo 6 > /var/plexguide/final.choice"
+ when: fchoice == "6"
+
+ - name: Choice 7 Selected
+ shell: "echo 7 > /var/plexguide/final.choice"
+ when: fchoice == "7"
+
+ - name: Choice 8 Selected
+ shell: "echo 8 > /var/plexguide/final.choice"
+ when: fchoice == "8"
+
+ - name: Choice 9 Selected
+ shell: "echo 9 > /var/plexguide/final.choice"
+ when: fchoice == "9"
diff --git a/menu/cron/bcron.sh b/menu/cron/bcron.sh
new file mode 100644
index 00000000..8854ee59
--- /dev/null
+++ b/menu/cron/bcron.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+pgrole=$(cat /tmp/program_var)
+path=$(cat /var/plexguide/server.hd.path)
+tarlocation=$(cat /var/plexguide/data.location)
+serverid=$(cat /var/plexguide/pg.serverid)
+useragent=$(cat /var/plexguide/uagent)
+
+doc=no
+rolecheck=$(docker ps | grep -c "\<$pgrole\>")
+if [ $rolecheck != 0 ]; then docker stop $pgrole && doc=yes; fi
+
+tar \
+ --ignore-failed-read \
+ --warning=no-file-changed \
+ --warning=no-file-removed \
+ -cvzf $tarlocation/$pgrole.tar /opt/appdata/$pgrole/
+
+if [ $doc == yes ]; then docker restart $pgrole; fi
+
+chown -R 1000:1000 $tarlocation
+rclone --config /opt/appdata/plexguide/rclone.conf copy $tarlocation/$pgrole.tar gdrive:/plexguide/backup/$serverid -v --checksum --drive-chunk-size=64M --user-agent="$useragent"
+
+du -sh --apparent-size /opt/appdata/$pgrole | awk '{print $1}'
+rm -rf '$tarlocation/$pgrole.tar'
diff --git a/menu/cron/cron.sh b/menu/cron/cron.sh
new file mode 100644
index 00000000..0bbb2d83
--- /dev/null
+++ b/menu/cron/cron.sh
@@ -0,0 +1,100 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+
+# KEY VARIABLE RECALL & EXECUTION
+program=$(cat /tmp/program_var)
+mkdir -p /var/plexguide/cron/
+mkdir -p /opt/appdata/plexguide/cron
+# FUNCTIONS START ##############################################################
+source /opt/plexguide/menu/functions/functions.sh
+
+# FIRST QUESTION
+question1() {
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+⌛ PG Cron - Schedule Cron Jobs (Backups) | $program?
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+⚡ Reference: http://cron.pgblitz.com
+
+[1] No
+[2] Yes
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+
+ read -p '↘️ Type Number | Press [ENTER]: ' typed /var/plexguide/cron/cron.day && break=1
+ elif [ "$typed" == "8" ]; then
+ echo "*/1" >/var/plexguide/cron/$program.cron.day && break=1
+ else badinput; fi
+}
+
+# THIRD QUESTION
+question3() {
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+⌛ PG Cron - Hour of the Day?
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+Type an HOUR from [0 to 23]
+
+0 = 00:00 | 12AM
+12 = 12:00 | 12PM
+18 = 18:00 | 6 PM
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+
+ read -p '↘️ Type a Number | Press [ENTER]: ' typed /var/plexguide/cron/cron.hour && break=1
+ else badinput; fi
+}
+
+# FUNCTIONS END ##############################################################
+
+break=off && while [ "$break" == "off" ]; do question1; done
+break=off && while [ "$break" == "off" ]; do question2; done
+break=off && while [ "$break" == "off" ]; do question3; done
+
+echo $(($RANDOM % 59)) >/var/plexguide/cron/cron.minute
+ansible-playbook /opt/plexguide/menu/cron/cron.yml
diff --git a/menu/cron/cron.yml b/menu/cron/cron.yml
new file mode 100644
index 00000000..a5407ff0
--- /dev/null
+++ b/menu/cron/cron.yml
@@ -0,0 +1,42 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+---
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ # KEY VARIABLES ################################################################
+ - name: Set PGRole
+ shell: 'cat /tmp/program_var'
+ register: pgrole
+
+ - name: Set PGRole
+ shell: 'cat /var/plexguide/cron/cron.hour'
+ register: cronhour
+
+ - name: Set PGRole
+ shell: 'cat /var/plexguide/cron/cron.minute'
+ register: cronminute
+
+ - name: Set PGRole
+ shell: 'cat /var/plexguide/cron/cron.day'
+ register: cronday
+
+ # CRON START ###################################################################
+ # - name: Build Cron Job File
+ # shell: echo "ansible-playbook /opt/plexguide/menu/cron/bcron.yml --extra-vars 'program_var={{pgrole.stdout}}'" > /opt/appdata/plexguide/cron/{{pgrole.stdout}}
+
+ - name: Build Cron Job Schedule
+ cron:
+ name: '{{pgrole.stdout}}'
+ weekday: '{{cronday.stdout}}'
+ minute: '{{cronminute.stdout}}'
+ hour: '{{cronhour.stdout}}'
+ user: root
+ job: 'echo {{pgrole.stdout}} > /tmp/program_var && bash /opt/pgvault/pgcron'
+ state: present
+ become_user: root
diff --git a/menu/cron/mass.sh b/menu/cron/mass.sh
new file mode 100644
index 00000000..c57ebd13
--- /dev/null
+++ b/menu/cron/mass.sh
@@ -0,0 +1,75 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+#################################################################################
+
+# KEY VARIABLE RECALL & EXECUTION
+mkdir -p /var/plexguide/cron/
+mkdir -p /opt/appdata/plexguide/cron
+# FUNCTIONS START ##############################################################
+source /opt/plexguide/menu/functions/functions.sh
+
+weekrandom() {
+ while read p; do
+ echo "$p" >/tmp/program_var
+ echo $(($RANDOM % 23)) >/var/plexguide/cron/cron.hour
+ echo $(($RANDOM % 59)) >/var/plexguide/cron/cron.minute
+ echo $(($RANDOM % 6)) >/var/plexguide/cron/cron.day
+ ansible-playbook /opt/plexguide/menu/cron/cron.yml
+ done /tmp/program_var
+ echo $(($RANDOM % 23)) >/var/plexguide/cron/cron.hour
+ echo $(($RANDOM % 59)) >/var/plexguide/cron/cron.minute
+ echo "*/1" >/var/plexguide/cron/cron.day
+ ansible-playbook /opt/plexguide/menu/cron/cron.yml
+ done /tmp/program_var
+ bash /opt/plexguide/menu/cron/cron.sh
+ done $1; fi
+}
+
+# For ZipLocations
+
+variable /var/plexguide/server.hd.path "/mnt"
+pgpath=$(cat /var/plexguide/server.hd.path)
+
+used=$(df -h $pgpath | tail -n +2 | awk '{print $3}')
+capacity=$(df -h $pgpath | tail -n +2 | awk '{print $2}')
+percentage=$(df -h $pgpath | tail -n +2 | awk '{print $5}')
+###################### FOR VARIABLS ROLE SO DOESNT CREATE RED - START
+
+# Menu Interface
+tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+🌎 Processing Disk Interface
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+🌵 Processing Disk : $pgpath
+ Processing Space: $used of $capacity | $percentage Used Capacity
+
+☑️ PG does not format your second disk, nor mount it! We can
+only assist by changing the location path!
+
+☑️ Enables System to process items on a SECONDARY Drive rather
+than tax the PRIMARY DRIVE. Like Windows, you can have your items
+process on a (D): Drive instead of on a (C): Drive.
+
+Do You Want To Change the Processing Disk?
+
+[1] No
+[2] Yes
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+# Standby
+read -p '↘️ Type a Number | Press [ENTER]: ' typed >> exit
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+
+ # Standby
+ read -p '↘️ Type the NEW PATH (Follow Above Examples): ' typed /dev/null 2>&1
+
+ file="$typed/test"
+ if [ -e "$file" ]; then
+
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+✅️ WOOT WOOT: Location Is Valid - $typed
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+ sleep 2
+
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+⌛ STANDBY: Setting Up Your Permissions
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+ sleep 2
+
+ chown 1000:1000 "$typed"
+ chmod 0775 "$typed"
+ rm -rf "$typed/test"
+ echo $typed >/var/plexguide/server.hd.path
+ break=off
+
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+⌛ STANDBY: Making Folders & Rebuilding the Systems Docker Containers!
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+ sleep 2
+
+ ansible-playbook /opt/plexguide/menu/installer/main.yml
+ bash /opt/plexguide/menu/dlpath/rebuild.sh
+
+ tee <<-EOF
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+✅️ WOOT WOOT: Process Complete!
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+EOF
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ else
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+⛔️ WARNING! - Mount Error!
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+$typed does not exist!
+
+You may have forgotten to create it, but PG is unable to see it!
+Try >>> cd $path and see what happens!
+
+Exiting! Nothing has changed!
+
+EOF
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ fi
+else
+
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+🍖 NOM NOM: Failed to Make a Valid Selection! Restarting the Process!
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+ sleep 3
+ bash /opt/plexguide/menu/dlpath/dlpath.sh
+ exit
+fi
+
+exit
diff --git a/menu/dlpath/rebuild.sh b/menu/dlpath/rebuild.sh
new file mode 100644
index 00000000..567eea62
--- /dev/null
+++ b/menu/dlpath/rebuild.sh
@@ -0,0 +1,35 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+docker ps -a --format "{{.Names}}" >/var/plexguide/container.running
+
+sed -i -e "/traefik/d" /var/plexguide/container.running
+sed -i -e "/watchtower/d" /var/plexguide/container.running
+sed -i -e "/wp-*/d" /var/plexguide/container.running
+sed -i -e "/plex/d" /var/plexguide/container.running
+sed -i -e "/emby/d" /var/plexguide/container.running
+sed -i -e "/jellyfin/d" /var/plexguide/container.running
+sed -i -e "/pgblitz/d" /var/plexguide/container.running
+sed -i -e "/oauth/d" /var/plexguide/container.running
+sed -i -e "/dockergc/d" /var/plexguide/container.running
+sed -i -e "/pgui/d" /var/plexguide/container.running
+
+### Your Wondering Why No While Loop, using a While Loops Screws Up Ansible Prompts
+### BackDoor WorkAround to Stop This Behavior
+count=$(wc -l /tmp/program_var
+ if [ -e "/opt/coreapps/apps/$app.yml" ]; then ansible-playbook /opt/coreapps/apps/$app.yml; fi
+ if [ -e "/opt/communityapps/$app.yml" ]; then ansible-playbook /opt/communityapps/apps/$app.yml; fi
+done
+
+echo ""
+echo 'INFO - Rebuilding Complete!' >/var/plexguide/logs/pg.log && bash /opt/plexguide/menu/log/log.sh
diff --git a/menu/functions/functions.sh b/menu/functions/functions.sh
new file mode 100644
index 00000000..4f0c5578
--- /dev/null
+++ b/menu/functions/functions.sh
@@ -0,0 +1,54 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+
+# BAD INPUT
+badinput() {
+ echo
+ read -p '⛔️ ERROR - Bad Input! | Press [ENTER] ' typed $1; fi
+}
+
+readrcloneconfig() {
+ touch /opt/appdata/plexguide/rclone.conf
+ mkdir -p /var/plexguide/rclone/
+
+ gdcheck=$(cat /opt/appdata/plexguide/rclone.conf | grep gdrive)
+ if [ "$gdcheck" != "" ]; then
+ echo "good" >/var/plexguide/rclone/gdrive.status && gdstatus="good"
+ else echo "bad" >/var/plexguide/rclone/gdrive.status && gdstatus="bad"; fi
+
+ gccheck=$(cat /opt/appdata/plexguide/rclone.conf | grep "remote = gdrive:/encrypt")
+ if [ "$gccheck" != "" ]; then
+ echo "good" >/var/plexguide/rclone/gcrypt.status && gcstatus="good"
+ else echo "bad" >/var/plexguide/rclone/gcrypt.status && gcstatus="bad"; fi
+
+ tdcheck=$(cat /opt/appdata/plexguide/rclone.conf | grep tdrive)
+ if [ "$tdcheck" != "" ]; then
+ echo "good" >/var/plexguide/rclone/tdrive.status && tdstatus="good"
+ else echo "bad" >/var/plexguide/rclone/tdrive.status && tdstatus="bad"; fi
+
+}
+
+rcloneconfig() {
+ rclone config --config /opt/appdata/plexguide/rclone.conf
+}
+
+keysprocessed() {
+ mkdir -p /opt/appdata/plexguide/keys/processed
+ ls -1 /opt/appdata/plexguide/keys/processed | wc -l >/var/plexguide/project.keycount
+}
diff --git a/menu/functions/install.sh b/menu/functions/install.sh
new file mode 100644
index 00000000..4abfcd15
--- /dev/null
+++ b/menu/functions/install.sh
@@ -0,0 +1,562 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+source /opt/plexguide/menu/functions/functions.sh
+
+updateprime() {
+ abc="/var/plexguide"
+ mkdir -p ${abc}
+ chmod 0775 ${abc}
+ chown 1000:1000 ${abc}
+
+ mkdir -p /opt/appdata/plexguide
+ chmod 0775 /opt/appdata/plexguide
+ chown 1000:1000 /opt/appdata/plexguide
+
+ variable /var/plexguide/pgfork.project "UPDATE ME"
+ variable /var/plexguide/pgfork.version "changeme"
+ variable /var/plexguide/tld.program "portainer"
+ variable /opt/appdata/plexguide/plextoken ""
+ variable /var/plexguide/server.ht ""
+ variable /var/plexguide/server.email "NOT-SET"
+ variable /var/plexguide/server.domain "NOT-SET"
+ variable /var/plexguide/pg.number "New-Install"
+ variable /var/plexguide/emergency.log ""
+ variable /var/plexguide/pgbox.running ""
+ pgnumber=$(cat /var/plexguide/pg.number)
+
+ hostname -I | awk '{print $1}' >/var/plexguide/server.ip
+ file="${abc}/server.hd.path"
+ if [ ! -e "$file" ]; then echo "/mnt" >${abc}/server.hd.path; fi
+
+ file="${abc}/new.install"
+ if [ ! -e "$file" ]; then newinstall; fi
+
+ ospgdistro=$(lsb_release -is)
+ ospgrelease=$(lsb_release -sr | cut -d. -f1)
+ if [[ "$ospgdistro" = "Debian" ]] && [[ "$ospgrelease" = "9" ]] || [[ "$ospgrelease" = "10" ]]; then
+ echo "Debian" >${abc}/os.version
+ else echo "Ubuntu" >${abc}/os.version; fi
+
+ echo "3" >${abc}/pg.mergerinstall
+ echo "52" >${abc}/pg.pythonstart
+ echo "12" >${abc}/pg.aptupdate
+ echo "150" >${abc}/pg.preinstall
+ echo "24" >${abc}/pg.folders
+ echo "16" >${abc}/pg.dockerinstall
+ echo "15" >${abc}/pg.server
+ echo "1" >${abc}/pg.serverid
+ echo "33" >${abc}/pg.dependency
+ echo "11" >${abc}/pg.docstart
+ echo "2" >${abc}/pg.motd
+ echo "115" >${abc}/pg.alias
+ echo "3" >${abc}/pg.dep
+ echo "3" >${abc}/pg.cleaner
+ echo "3" >${abc}/pg.gcloud
+ echo "12" >${abc}/pg.hetzner
+ echo "1" >${abc}/pg.amazonaws
+ echo "8.4" >${abc}/pg.verionid
+ echo "11" >${abc}/pg.watchtower
+ echo "1" >${abc}/pg.installer
+ echo "7" >${abc}/pg.prune
+ echo "21" >${abc}/pg.mountcheck
+
+}
+
+pginstall() {
+ updateprime
+ bash /opt/plexguide/menu/pggce/gcechecker.sh
+ core pythonstart
+ core aptupdate
+ core alias &>/dev/null &
+ core folders
+ core dependency
+ core mergerinstall
+ core dockerinstall
+ core docstart
+
+ touch /var/plexguide/install.roles
+ rolenumber=3
+ # Roles Ensure that PG Replicates and has once if missing; important for startup, cron and etc
+ if [[ $(cat /var/plexguide/install.roles) != "$rolenumber" ]]; then
+ rm -rf /opt/communityapps
+ rm -rf /opt/coreapps
+ rm -rf /opt/pgshield
+
+ pgcore
+ pgcommunity
+ pgshield
+ echo "$rolenumber" >/var/plexguide/install.roles
+ fi
+
+ portainer
+ pgui
+ core motd &>/dev/null &
+ core hetzner &>/dev/null &
+ core gcloud
+ core cleaner &>/dev/null &
+ core serverid
+ core watchtower
+ core prune
+ customcontainers &>/dev/null &
+ pgedition
+ core mountcheck
+ emergency
+ pgdeploy
+}
+
+core() {
+ touch /var/plexguide/pg."${1}".stored
+ start=$(cat /var/plexguide/pg."${1}")
+ stored=$(cat /var/plexguide/pg."${1}".stored)
+ if [ "$start" != "$stored" ]; then
+ $1
+ cat /var/plexguide/pg."${1}" >/var/plexguide/pg."${1}".stored
+ fi
+}
+
+############################################################ INSTALLER FUNCTIONS
+alias() {
+ ansible-playbook /opt/plexguide/menu/alias/alias.yml
+}
+
+aptupdate() {
+ yes | apt-get update
+ yes | apt-get install software-properties-common
+ yes | apt-get install sysstat nmon
+ sed -i 's/false/true/g' /etc/default/sysstat
+}
+
+customcontainers() {
+ mkdir -p /opt/mycontainers
+ touch /opt/appdata/plexguide/rclone.conf
+ mkdir -p /opt/communityapps/apps
+ rclone --config /opt/appdata/plexguide/rclone.conf copy /opt/mycontainers/ /opt/communityapps/apps
+}
+
+cleaner() {
+ ansible-playbook /opt/plexguide/menu/pg.yml --tags autodelete &>/dev/null &
+ ansible-playbook /opt/plexguide/menu/pg.yml --tags clean &>/dev/null &
+ ansible-playbook /opt/plexguide/menu/pg.yml --tags clean-encrypt &>/dev/null &
+}
+
+dependency() {
+ ospgversion=$(cat /var/plexguide/os.version)
+ if [ "$ospgversion" == "debian" ]; then
+ ansible-playbook /opt/plexguide/menu/dependency/dependencydeb.yml
+ else
+ ansible-playbook /opt/plexguide/menu/dependency/dependency.yml
+ fi
+}
+
+docstart() {
+ ansible-playbook /opt/plexguide/menu/pg.yml --tags docstart
+}
+
+emergency() {
+ variable /var/plexguide/emergency.display "On"
+ if [[ $(ls /opt/appdata/plexguide/emergency) != "" ]]; then
+
+ # If not on, do not display emergency logs
+ if [[ $(cat /var/plexguide/emergency.display) == "On" ]]; then
+
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+⛔️ Emergency & Warning Log Generator
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+NOTE: This can be turned [On] or Off in Settings!
+
+EOF
+
+ countmessage=0
+ while read p; do
+ let countmessage++
+ echo -n "${countmessage}. " && cat /opt/appdata/plexguide/emergency/$p
+ done <<<"$(ls /opt/appdata/plexguide/emergency)"
+
+ echo
+ read -n 1 -s -r -p "Acknowledge Info | Press [ENTER]"
+ echo
+ else
+ touch /var/plexguide/emergency.log
+ fi
+ fi
+
+}
+
+folders() {
+ ansible-playbook /opt/plexguide/menu/installer/folders.yml
+}
+
+prune() {
+ ansible-playbook /opt/plexguide/menu/prune/main.yml
+}
+
+hetzner() {
+ if [ -e "$file" ]; then rm -rf /bin/hcloud; fi
+ version="v1.10.0"
+ wget -P /opt/appdata/plexguide "https://github.com/hetznercloud/cli/releases/download/$version/hcloud-linux-amd64-$version.tar.gz"
+ tar -xvf "/opt/appdata/plexguide/hcloud-linux-amd64-$version.tar.gz" -C /opt/appdata/plexguide
+ mv "/opt/appdata/plexguide/hcloud-linux-amd64-$version/bin/hcloud" /bin/
+ rm -rf /opt/appdata/plexguide/hcloud-linux-amd64-$version.tar.gz
+ rm -rf /opt/appdata/plexguide/hcloud-linux-amd64-$version
+}
+
+gcloud() {
+ export CLOUD_SDK_REPO="cloud-sdk-$(lsb_release -c -s)"
+ echo "deb http://packages.cloud.google.com/apt $CLOUD_SDK_REPO main" | sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list
+ curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add
+ sudo apt-get update && sudo apt-get install google-cloud-sdk -y
+}
+
+mergerinstall() {
+
+ ub16check=$(cat /etc/*-release | grep xenial)
+ ub18check=$(cat /etc/*-release | grep bionic)
+ deb9check=$(cat /etc/*-release | grep stretch)
+ deb10check=$(cat /etc/*-release | grep buster)
+ activated=false
+
+ apt --fix-broken install -y
+ apt-get remove mergerfs -y
+ mkdir -p /var/plexguide
+
+ if [ "$ub16check" != "" ]; then
+ activated=true
+ echo "ub16" >/var/plexguide/mergerfs.version
+ wget "https://github.com/trapexit/mergerfs/releases/download/2.28.1/mergerfs_2.28.1.ubuntu-xenial_amd64.deb"
+
+ elif [ "$ub18check" != "" ]; then
+ activated=true
+ echo "ub18" >/var/plexguide/mergerfs.version
+ wget "https://github.com/trapexit/mergerfs/releases/download/2.28.1/mergerfs_2.28.1.ubuntu-bionic_amd64.deb"
+
+ elif [ "$deb9check" != "" ]; then
+ activated=true
+ echo "deb9" >/var/plexguide/mergerfs.version
+ wget "https://github.com/trapexit/mergerfs/releases/download/2.28.1/mergerfs_2.28.1.debian-stretch_amd64.deb"
+
+ elif [ "$deb10check" != "" ]; then
+ activated=true
+ echo "deb10" >/var/plexguide/mergerfs.version
+ wget "https://github.com/trapexit/mergerfs/releases/download/2.28.1/mergerfs_2.28.1.debian-buster_amd64.deb"
+
+ elif [ "$activated" != "true" ]; then
+ activated=true && echo "ub18 - but didn't detect correctly" >/var/plexguide/mergerfs.version
+ wget "https://github.com/trapexit/mergerfs/releases/download/2.28.1/mergerfs_2.28.1.ubuntu-bionic_amd64.deb"
+ else
+ apt-get install g++ pkg-config git git-buildpackage pandoc debhelper libfuse-dev libattr1-dev -y
+ git clone https://github.com/trapexit/mergerfs.git
+ cd mergerfs
+ make clean
+ make deb
+ cd ..
+ fi
+
+ apt install -y ./mergerfs*_amd64.deb
+ rm mergerfs*_amd64.deb
+
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+↘️ MergerFS has been updated! Requires PG Clone redeployment.
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+INFORMATION: MergerFS was updated on your system and brings performance improvements!
+Users have reported faster plex scanning and playback with the new mergerfs and pgclone configuration.
+
+ATTENTION:
+You are required to re-deploy your mounts in the PG Clone menu (option 4, option A).
+It is advised to check the VFS mount settings in the options menu (C,2), as options have been updated.
+
+WARNING: This is not optional, you must redeploy your mounts in the PG Clone menu.
+Your mounts are currently down until you re-deploy pg clone as it requires configuration updates!
+This is not done for you, you must go to the PG Clone Menu (option 4) and deploy (option A).
+
+We apologize for this one-time inconvenience.
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+ read -p 'Acknowledge Info | Press [ENTER] ' typed /dev/null 2>&1
+ file="${abc}/new.install"
+ if [ ! -e "$file" ]; then
+ touch ${abc}/pg.number && echo off >/tmp/program_source
+ bash /opt/plexguide/menu/version/file.sh
+ file="${abc}/new.install"
+ if [ ! -e "$file" ]; then exit; fi
+ fi
+}
+
+pgdeploy() {
+ touch /var/plexguide/pg.edition
+ bash /opt/plexguide/menu/start/start.sh
+}
+
+pgedition() {
+ file="${abc}/path.check"
+ if [ ! -e "$file" ]; then touch ${abc}/path.check && bash /opt/plexguide/menu/dlpath/dlpath.sh; fi
+ # FOR PG-BLITZ
+ file="${abc}/project.deployed"
+ if [ ! -e "$file" ]; then echo "no" >${abc}/project.deployed; fi
+ file="${abc}/project.keycount"
+ if [ ! -e "$file" ]; then echo "0" >${abc}/project.keycount; fi
+ file="${abc}/server.id"
+ if [ ! -e "$file" ]; then echo "[NOT-SET]" -rf >${abc}/rm; fi
+}
+
+portainer() {
+ dstatus=$(docker ps --format '{{.Names}}' | grep "portainer")
+ if [ "$dstatus" != "portainer" ]; then
+ ansible-playbook /opt/coreapps/apps/portainer.yml &>/dev/null &
+ fi
+}
+
+# Roles Ensure that PG Replicates and has once if missing; important for startup, cron and etc
+pgcore() { if [ ! -e "/opt/coreapps/place.holder" ]; then ansible-playbook /opt/plexguide/menu/pgbox/pgboxcore.yml; fi; }
+pgcommunity() { if [ ! -e "/opt/communityapps/place.holder" ]; then ansible-playbook /opt/plexguide/menu/pgbox/pgboxcommunity.yml; fi; }
+pgshield() { if [ ! -e "/opt/pgshield/place.holder" ]; then
+ echo 'pgshield' >/var/plexguide/pgcloner.rolename
+ echo 'PGShield' >/var/plexguide/pgcloner.roleproper
+ echo 'PGShield' >/var/plexguide/pgcloner.projectname
+ echo 'v8.6' >/var/plexguide/pgcloner.projectversion
+ echo 'pgshield.sh' >/var/plexguide/pgcloner.startlink
+ ansible-playbook "/opt/plexguide/menu/pgcloner/corev2/primary.yml"
+fi; }
+
+pgui() {
+ file="/var/plexguide/pgui.switch"
+ if [ ! -e "$file" ]; then echo "On" >/var/plexguide/pgui.switch; fi
+
+ pguicheck=$(cat /var/plexguide/pgui.switch)
+ if [[ "$pguicheck" == "On" ]]; then
+
+ dstatus=$(docker ps --format '{{.Names}}' | grep "pgui")
+ if [ "$dstatus" != "pgui" ]; then
+ bash /opt/plexguide/menu/pgcloner/solo/pgui.sh
+ ansible-playbook /opt/pgui/pgui.yml
+ fi
+ fi
+}
+
+pythonstart() {
+
+ ansible="2.8.2"
+ pip="19.1.1"
+
+ apt-get install -y --reinstall \
+ nano \
+ git \
+ build-essential \
+ libssl-dev \
+ libffi-dev \
+ python3-dev \
+ python3-pip \
+ python-dev \
+ python-pip
+ python3 -m pip install --disable-pip-version-check --upgrade --force-reinstall pip==${pip}
+ python3 -m pip install --disable-pip-version-check --upgrade --force-reinstall setuptools
+ python3 -m pip install --disable-pip-version-check --upgrade --force-reinstall \
+ pyOpenSSL \
+ requests \
+ netaddr
+ python -m pip install --disable-pip-version-check --upgrade --force-reinstall pip==${pip}
+ python -m pip install --disable-pip-version-check --upgrade --force-reinstall setuptools
+ python -m pip install --disable-pip-version-check --upgrade --force-reinstall ansible==${1-$ansible}
+
+ ## Copy pip to /usr/bin
+ cp /usr/local/bin/pip /usr/bin/pip
+ cp /usr/local/bin/pip3 /usr/bin/pip3
+
+ mkdir -p /etc/ansible/inventories/ 1>/dev/null 2>&1
+ echo "[local]" >/etc/ansible/inventories/local
+ echo "127.0.0.1 ansible_connection=local" >>/etc/ansible/inventories/local
+
+ ### Reference: https://docs.ansible.com/ansible/2.4/intro_configuration.html
+ echo "[defaults]" >/etc/ansible/ansible.cfg
+ echo "deprecation_warnings=False" >>/etc/ansible/ansible.cfg
+ echo "command_warnings = False" >>/etc/ansible/ansible.cfg
+ echo "callback_whitelist = profile_tasks" >>/etc/ansible/ansible.cfg
+ echo "inventory = /etc/ansible/inventories/local" >>/etc/ansible/ansible.cfg
+
+ # Variables Need to Line Up with pg.sh (start)
+ touch /var/plexguide/background.1
+}
+
+dockerinstall() {
+ ospgversion=$(cat /var/plexguide/os.version)
+ if [ "$ospgversion" == "debian" ]; then
+ ansible-playbook /opt/plexguide/menu/pg.yml --tags dockerdeb
+ else
+ ansible-playbook /opt/plexguide/menu/pg.yml --tags docker
+ # If Docker FAILED, Emergency Install
+ file="/usr/bin/docker"
+ if [ ! -e "$file" ]; then
+ clear
+ echo "Installing Docker the Old School Way - (Please Be Patient)"
+ sleep 2
+ clear
+ curl -fsSL get.docker.com -o get-docker.sh
+ sh get-docker.sh
+ echo ""
+ echo "Starting Docker (Please Be Patient)"
+ sleep 2
+ systemctl start docker
+ sleep 2
+ fi
+
+ ##### Checking Again, if fails again; warns user
+ file="/usr/bin/docker"
+ if [ -e "$file" ]; then
+ sleep 5
+ else
+ echo "INFO - FAILED: Docker Failed to Install! Exiting PGBlitz!"
+ exit
+ fi
+ fi
+}
+
+serverid() {
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+↘️ Establishing Server ID 💬 Use One Word & Keep it Simple
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+ read -p '🌏 TYPE Server ID | Press [ENTER]: ' typed /var/plexguide/server.id
+ sleep 1
+ fi
+}
+
+watchtower() {
+
+ file="/var/plexguide/watchtower.wcheck"
+ if [ ! -e "$file" ]; then
+ echo "4" >/var/plexguide/watchtower.wcheck
+ fi
+
+ wcheck=$(cat "/var/plexguide/watchtower.wcheck")
+ if [[ "$wcheck" -ge "1" && "$wcheck" -le "3" ]]; then
+ wexit="1"
+ else wexit=0; fi
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+📂 PG WatchTower Edition
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+💬 WatchTower updates your containers soon as possible!
+
+[1] Containers: Auto-Update All
+[2] Containers: Auto-Update All Except | Plex & Emby
+[3] Containers: Never Update
+Z - Exit
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+
+ # Standby
+ read -p 'Type a Number | Press [ENTER]: ' typed /var/plexguide/watchtower.wcheck
+ elif [ "$typed" == "2" ]; then
+ watchtowergen
+ sed -i -e "/plex/d" /tmp/watchtower.set 1>/dev/null 2>&1
+ sed -i -e "/emby/d" /tmp/watchtower.set 1>/dev/null 2>&1
+ sed -i -e "/jellyfin/d" /tmp/watchtower.set 1>/dev/null 2>&1
+ ansible-playbook /opt/coreapps/apps/watchtower.yml
+ echo "2" >/var/plexguide/watchtower.wcheck
+ elif [ "$typed" == "3" ]; then
+ echo null >/tmp/watchtower.set
+ ansible-playbook /opt/coreapps/apps/watchtower.yml
+ echo "3" >/var/plexguide/watchtower.wcheck
+ elif [[ "$typed" == "Z" || "$typed" == "z" ]]; then
+ if [ "$wexit" == "0" ]; then
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+⚠️ WatchTower Preference Must be Set Once!
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+ sleep 3
+ watchtower
+ fi
+ exit
+ else
+ badinput
+ watchtower
+ fi
+}
+
+watchtowergen() {
+ bash /opt/coreapps/apps/_appsgen.sh
+ bash /opt/communityapps/apps/_appsgen.sh
+ while read p; do
+ echo -n $p >>/tmp/watchtower.set
+ echo -n " " >>/tmp/watchtower.set
+ done /var/plexguide/project.account
+
+ file="/var/plexguide/project.final"
+ if [ ! -e "$file" ]; then echo "[NOT SET]" >/var/plexguide/project.final; fi
+
+ file="/var/plexguide/project.processor"
+ if [ ! -e "$file" ]; then echo "NOT-SET" >/var/plexguide/project.processor; fi
+
+ file="/var/plexguide/project.location"
+ if [ ! -e "$file" ]; then echo "NOT-SET" >/var/plexguide/project.location; fi
+
+ file="/var/plexguide/project.ipregion"
+ if [ ! -e "$file" ]; then echo "NOT-SET" >/var/plexguide/project.ipregion; fi
+
+ file="/var/plexguide/project.ipaddress"
+ if [ ! -e "$file" ]; then echo "IP NOT-SET" >/var/plexguide/project.ipaddress; fi
+
+ file="/var/plexguide/gce.deployed"
+ if [ -e "$file" ]; then
+ echo "Server Deployed" >/var/plexguide/gce.deployed.status
+ else echo "Not Deployed" >/var/plexguide/gce.deployed.status; fi
+
+ project=$(cat /var/plexguide/project.final)
+ account=$(cat /var/plexguide/project.account)
+ processor=$(cat /var/plexguide/project.processor)
+ ipregion=$(cat /var/plexguide/project.ipregion)
+ ipaddress=$(cat /var/plexguide/project.ipaddress)
+ serverstatus=$(cat /var/plexguide/gce.deployed.status)
+}
diff --git a/menu/functions/pgvault.func b/menu/functions/pgvault.func
new file mode 100644
index 00000000..ead32afa
--- /dev/null
+++ b/menu/functions/pgvault.func
@@ -0,0 +1,559 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+runningcheck() {
+ initial2
+ runcheck5=$(docker ps | grep ${program_var})
+ if [ "$runcheck5" != "" ]; then running=1; else running=0; fi
+}
+
+initial() {
+ rm -rf /var/plexguide/pgvault.output 1>/dev/null 2>&1
+ rm -rf /var/plexguide/pgvault.buildup 1>/dev/null 2>&1
+ rm -rf /var/plexguide/program.temp 1>/dev/null 2>&1
+ rm -rf /var/plexguide/app.list 1>/dev/null 2>&1
+ rm -rf /var/plexguide/pgvault.output 1>/dev/null 2>&1
+ touch /var/plexguide/pgvault.output
+ touch /var/plexguide/program.temp
+ touch /var/plexguide/app.list
+ touch /var/plexguide/pgvault.buildup
+ touch /var/plexguide/pgvault.output
+ touch /var/plexguide/rclone.size
+ space=$(cat /var/plexguide/data.location)
+ # To Get Used Space
+ used=$(df -h /opt/appdata/plexguide | tail -n +2 | awk '{print $3}')
+ # To Get All Space
+ capacity=$(df -h /opt/appdata/plexguide | tail -n +2 | awk '{print $2}')
+ # Percentage
+ percentage=$(df -h /opt/appdata/plexguide | tail -n +2 | awk '{print $5}')
+}
+
+initial2() {
+ path=$(cat /var/plexguide/server.hd.path)
+ tarlocation=$(cat /var/plexguide/data.location)
+ program_size=$(cat /var/plexguide/rclone.size)
+ program_var=$(cat /tmp/program_var)
+ server_id=$(cat /var/plexguide/server.id)
+}
+
+final() {
+ echo
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ read -p '✅ Process Complete! | PRESS [ENTER] ' typed pgvault.serverlist
+}
+
+pgboxrecall() {
+ ls -p /opt/coreapps/apps | grep -v / >/var/plexguide/pgvault.apprecall
+ while read p; do
+ sed -i "/^$p\b/Id" /var/plexguide/pgvault.apprecall
+ done >/var/plexguide/pgvault.apprecall
+ done /dev/null 2>&1
+ while read p; do
+ echo -n $p >>/var/plexguide/program.temp
+ echo -n " " >>/var/plexguide/program.temp
+ num=$((num + 1))
+ if [ "$num" == 7 ]; then
+ num=0
+ echo " " >>/var/plexguide/program.temp
+ fi
+ done /var/plexguide/pgvault.apprecall
+ while read p; do
+ sed -i "/^$p\b/Id" /var/plexguide/pgvault.apprecall
+ done >/var/plexguide/pgvault.buildup
+ sed -i "/^$typed\b/Id" /var/plexguide/pgvault.apprecall
+
+ num=0
+ rm -rf /var/plexguide/pgvault.output 1>/dev/null 2>&1
+ while read p; do
+ echo -n $p >>/var/plexguide/pgvault.output
+ echo -n " " >>/var/plexguide/pgvault.output
+ if [ "$num" == 7 ]; then
+ num=0
+ echo " " >>/var/plexguide/pgvault.output
+ fi
+ done /tmp/server.list
+
+ ### List Out Apps In Readable Order (One's Not Installed)
+ num=0
+ rm -rf /var/plexguide/program.temp 1>/dev/null 2>&1
+ while read p; do
+ echo -n $p >>/var/plexguide/program.temp
+ echo -n " " >>/var/plexguide/program.temp
+ num=$((num + 1))
+ if [ "$num" == 7 ]; then
+ num=0
+ echo " " >>/var/plexguide/program.temp
+ fi
+ done /tmp/server.select
+
+ if [[ "$server" == "exit" || "$server" == "Exit" || "$server" == "EXIT" || "$server" == "z" || "$server" == "Z" ]]; then exit; fi
+
+ current2=$(cat /tmp/server.list | grep "\<$server\>")
+ if [ "$current2" == "" ]; then
+ badserver
+ serverprime
+ fi
+
+ tempserver=$server
+ ls -l /mnt/gdrive/plexguide/backup/$tempserver | awk '{print $9}' | tail -n +2 >/var/plexguide/pgvault.restoreapps
+
+ ### Blank Out Temp List
+ rm -rf /var/plexguide/pgvault.apprecall 1>/dev/null 2>&1
+ touch /var/plexguide/pgvault.apprecall
+
+ while read p; do
+ basename "$p" .tar >>/var/plexguide/pgvault.apprecall
+ done /dev/null 2>&1
+ touch /var/plexguide/program.temp
+ mathprime
+}
+
+buildup2() {
+ echo "$typed" >>/var/plexguide/pgvault.buildup
+ sed -i "/^$typed\b/Id" /var/plexguide/pgvault.apprecall
+
+ num=0
+ rm -rf /var/plexguide/pgvault.output 1>/dev/null 2>&1
+ while read p; do
+ echo -n $p >>/var/plexguide/pgvault.output
+ echo -n " " >>/var/plexguide/pgvault.output
+ if [ "$num" == 7 ]; then
+ num=0
+ echo " " >>/var/plexguide/pgvault.output
+ fi
+ done /tmp/program_var
+ # Execute Main Program
+ backup_process
+
+ sleep 2
+ done /dev/null 2>&1
+ fi
+
+ ###### Start the Backup Process - Backup Locally First
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+↘️ Zipping Data Locally - $program_var
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+
+ path=$(cat /var/plexguide/server.hd.path)
+ tarlocation=$(cat /var/plexguide/data.location)
+ server_id=$(cat /var/plexguide/server.id)
+
+ tar \
+ --warning=no-file-changed --ignore-failed-read --absolute-names --warning=no-file-removed \
+ --exclude-from=/opt/pgvault/exclude.list \
+ -C /opt/appdata/${program_var} -cvf /opt/appdata/plexguide/${program_var}.tar ./
+
+ #tar \
+ #--warning=no-file-changed --ignore-failed-read --absolute-names --warning=no-file-removed \
+ #--exclude-from=/opt/pgvault/exclude.list \
+ #-cfv ${program_var}.tar /opt/appdata/${program_var}
+
+ ##### Restart Docker Application if was Running Prior
+ if [ "$running" == "1" ]; then
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+↘️ Restarting Docker Application - $program_var
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+ sleep 2
+ docker restart $program_var 1>/dev/null 2>&1
+ fi
+
+ ###### Backing Up Files to GDrive
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+↘️ Sending Zipped Data to Google Drive - $program_var
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+
+ rclone --config /opt/appdata/plexguide/rclone.conf mkdir gdrive:/plexguide/backup/${server_id} 1>/dev/null 2>&1
+
+ rclone --config /opt/appdata/plexguide/rclone.conf \
+ --stats-one-line --stats 1s --progress \
+ moveto ${tarlocation}/${program_var}.tar \
+ gdrive:/plexguide/backup/${server_id}/${program_var}.tar \
+ -v --checksum --drive-chunk-size=64M --transfers=8 --user-agent="backup"
+
+ ##### Remove File Incase
+ rm -rf ${tarlocation}/${program_var}.tar 1>/dev/null 2>&1
+}
+######################################################## END - PG Vault Backup
+#
+##################################################### START - PG Vault Restore
+restore_start() {
+
+ while read p; do
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+PG Vault - Restoring: $p
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+
+ sleep 2.5
+
+ # Store Used Program
+ echo $p >/tmp/program_var
+ # Execute Main Program
+ restore_process
+
+ sleep 2
+ done /dev/null 2>&1
+ fi
+
+ ###### Start the Backup Process - Backup Locally First
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+↘️ UnZipping & Restoring Data - $program_var
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+
+ mkdir -p "/opt/appdata/${program_var}"
+ rm -rf "/opt/appdata/${program_var}/*"
+ chown -R 1000:1000 "/opt/appdata/${program_var}"
+ chmod -R 775 "/opt/appdata/${program_var}"
+ tar -C /opt/appdata/${program_var} -xvf ${tarlocation}/${program_var}.tar
+ chown -R 1000:1000 "/opt/appdata/${program_var}"
+ chmod -R 775 "/opt/appdata/${program_var}"
+
+ ##### Restart Docker Application if was Running Prior
+ if [ "$running" == "1" ]; then
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+↘️ Restarting Docker Application - $program_var
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+ sleep 2
+ docker restart $program_var 1>/dev/null 2>&1
+ fi
+
+ ##### Remove File Incase
+ rm -rf ${tarlocation}/${program_var}.tar 1>/dev/null 2>&1
+}
+##################################################### END - PG Vault Restore
+#
+##################################################### START - Backup Interface
+vaultbackup() {
+ ### List Out Apps In Readable Order (One's Not Installed)
+ notrun=$(cat /var/plexguide/program.temp)
+ buildup=$(cat /var/plexguide/pgvault.output)
+
+ if [ "$buildup" == "" ]; then buildup="NONE"; fi
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+🚀 PG Vault ~ Data Storage 📓 Reference: pgvault.pgblitz.com
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+📂 Potential Data to Backup
+
+$notrun
+
+💾 Apps Queued for Backup
+
+$buildup
+
+[A] Backup
+[Z] Exit
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+EOF
+ read -p '🌍 Type APP for QUEUE | Press [ENTER]: ' typed ")
+ if [ "$current2" != "" ]; then
+ queued
+ vaultbackup
+ fi
+
+ cat /var/plexguide/pgvault.buildup >/tmp/appcheck.5
+ cat /var/plexguide/pgvault.apprecall >>/tmp/appcheck.5
+ current1=$(cat /tmp/appcheck.5 | grep "\<$typed\>")
+ if [ "$current1" == "" ]; then badinput && vaultbackup; fi
+
+ buildup
+}
+##################################################### END - Backup Interface
+#
+##################################################### START - Restore Interface
+vaultrestore() {
+ notrun=$(cat /var/plexguide/program.temp)
+ buildup=$(cat /var/plexguide/pgvault.output)
+
+ if [ "$buildup" == "" ]; then buildup="NONE"; fi
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+🚀 PG Vault ~ Data Recall 📓 Reference: pgvault.pgblitz.com
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+📂 Potential Data to Restore
+
+$notrun
+
+💾 Apps Queued for Restore
+
+$buildup
+
+[A] Restore
+[Z] Exit
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+EOF
+ read -p '🌍 Type APP for QUEUE | Press [ENTER]: ' typed ")
+ if [ "$current2" != "" ]; then
+ queued
+ vaultrestore
+ fi
+
+ cat /var/plexguide/pgvault.buildup >/tmp/appcheck.5
+ cat /var/plexguide/pgvault.apprecall >>/tmp/appcheck.5
+ current1=$(cat /tmp/appcheck.5 | grep "\<$typed\>")
+ if [ "$current1" == "" ]; then badinput && vaultrestore; fi
+
+ buildup2
+}
+##################################################### START Primary Interface
+primaryinterface() {
+ initial2
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+📁 PG Vault - Main Interface 📓 Reference: pgvault.pgblitz.com
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+🌵 PG Disk Used Space: $used of $capacity | $percentage Used Capacity
+
+[1] Data Backup
+[2] Data Restore
+[3] Current Server ID : $server_id
+[4] Processing Location: $tarlocation
+
+[Z] Exit
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+ read -p 'Type a Number | Press [ENTER]: ' typed /var/plexguide/server.id.stored
+ bash /opt/plexguide/menu/interface/serverid.sh
+ primaryinterface
+ elif [ "$typed" == "4" ]; then
+ bash /opt/plexguide/menu/data/location.sh
+ primaryinterface
+ elif [[ "$typed" == "Z" || "$typed" == "z" ]]; then
+ exit
+ else
+ badinput
+ primaryinterface
+ fi
+}
+##################################################### END Primary Interface
+
+restorecheck() {
+ if [ "$restoreid" == "[NOT-SET]" ]; then
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+⛔️ WARNING! - You Must Set Your Recovery ID First! Restarting Process!
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+EOF
+ read -n 1 -s -r -p "Press [ANY] Key to Continue "
+ echo
+ primaryinterface
+ exit
+ fi
+}
diff --git a/menu/functions/start.sh b/menu/functions/start.sh
new file mode 100644
index 00000000..2e6d9ba9
--- /dev/null
+++ b/menu/functions/start.sh
@@ -0,0 +1,70 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+source /opt/plexguide/menu/functions/functions.sh
+source /opt/plexguide/menu/functions/install.sh
+
+sudocheck() {
+ if [[ $EUID -ne 0 ]]; then
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+⛔️ You Must Execute as a SUDO USER (with sudo) or as ROOT!
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+EOF
+ exit 1
+ fi
+}
+
+downloadpg() {
+ rm -rf /opt/plexguide
+ git clone https://github.com/MrDoobPG/PGBlitz.com.git /opt/plexguide && cp /opt/plexguide/menu/interface/alias/templates/pts /bin/
+ cp /opt/plexguide/menu/interface/alias/templates/pts /bin/pts
+}
+
+missingpull() {
+ file="/opt/plexguide/menu/functions/install.sh"
+ if [ ! -e "$file" ]; then
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+⛔️ /opt/pts went missing!
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+EOF
+ sleep 2
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+ 🍖 NOM NOM - Re-Downloading PGBlitz for BoneHead User!
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+ sleep 2
+ downloadpg
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+✅️ Repair Complete! Standby!
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+EOF
+ sleep 2
+ fi
+}
+
+exitcheck() {
+ bash /opt/plexguide/menu/version/file.sh
+ file="/var/plexguide/exited.upgrade"
+ if [ ! -e "$file" ]; then
+ bash /opt/plexguide/menu/interface/ending.sh
+ else
+ rm -rf /var/plexguide/exited.upgrade 1>/dev/null 2>&1
+ echo ""
+ bash /opt/plexguide/menu/interface/ending.sh
+ fi
+}
diff --git a/menu/hetzner/pghetznerigpu.sh b/menu/hetzner/pghetznerigpu.sh
new file mode 100644
index 00000000..caa03a7b
--- /dev/null
+++ b/menu/hetzner/pghetznerigpu.sh
@@ -0,0 +1,124 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Hetzner iGPU / GPU)
+# Author(s): Admin9705 - Deiteq - Sub7Seven
+# Coder : MrDoob | Freelaancer Coder TechLead
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+
+tee <<-EOF
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+ ⌛ Verifiying PG Hetzner iGPU / GPU HW-Transcode !
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+echo "Updating packages"
+apt-get update -yqq 2>&1 >>/dev/null
+export DEBIAN_FRONTEND=noninteractive
+echo "Upgrading packages"
+apt-get upgrade -yqq 2>&1 >>/dev/null
+export DEBIAN_FRONTEND=noninteractive
+echo "Dist-Upgrading packages"
+apt-get dist-upgrade -yqq 2>&1 >>/dev/null
+export DEBIAN_FRONTEND=noninteractive
+echo "Autoremove old Updates"
+apt-get autoremove -yqq 2>&1 >>/dev/null
+export DEBIAN_FRONTEND=noninteractive
+echo "install vainfo"
+sudo apt-get install vainfo -yqq 2>&1 >>/dev/null
+export DEBIAN_FRONTEND=noninteractive
+apt-get install lsb-release -yqq 2>&1 >>/dev/null
+export DEBIAN_FRONTEND=noninteractive
+echo "install complete"
+
+tee <<-EOF
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+ 🚀 PG Hetzner iGPU / GPU HW-Transcode
+
+ NOTE : You MUST have Plex Pass to enable hardware transcoding in the Plex server
+
+ Your Operations System : $(lsb_release -sd)
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ [1] Ubuntu 16.04 LTS
+ [2] Ubuntu 18.04 LTS
+ [3] Debian 9.6
+
+ [4] iGPU / GPU TEST
+
+ [Z] Exit
+
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+EOF
+
+# Standby
+read -p 'Type a Number | Press [ENTER]: ' typed &1 >>/dev/null
+ export DEBIAN_FRONTEND=noninteractive
+ sleep 10
+elif [ "$typed" == "Z" ] || [ "$typed" == "z" ]; then
+ exit
+else
+ bash /opt/plexguide/menu/tools/tools.sh
+ exit
+fi
+
+bash /opt/plexguide/menu/tools/tools.sh
+exit
diff --git a/menu/installer/alias.yml b/menu/installer/alias.yml
new file mode 100644
index 00000000..a7f3d69a
--- /dev/null
+++ b/menu/installer/alias.yml
@@ -0,0 +1,134 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+---
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: Install Status UFSMonitor
+ template:
+ src: sufs
+ dest: /bin/sufs
+ force: yes
+ mode: 0775
+
+ - name: Nano UFSMonitor
+ template:
+ src: nufs
+ dest: /bin/nufs
+ force: yes
+ mode: 0775
+
+ ###### Install PGLog
+
+ - name: Nano PGLog
+ template:
+ src: pglog
+ dest: /bin/pglog
+ force: yes
+ mode: 0775
+
+ ###### Install PGUpdate
+
+ - name: PGUpdate
+ template:
+ src: pgupdate
+ dest: /bin/pgupdate
+ force: yes
+ mode: 0775
+
+ ###### Install PGBlitz
+
+ - name: PGBlitz
+ template:
+ src: plexguide
+ dest: /bin/plexguide
+ force: yes
+ mode: 0775
+ owner: root
+
+ ###### Server reboot
+
+ - name: server reboot
+ template:
+ src: reboot
+ dest: /bin/reboot
+ force: yes
+ mode: 0775
+
+ ###### Check list of services
+
+ - name: list systemd services
+ template:
+ src: slist
+ dest: /bin/slist
+ force: yes
+ mode: 0775
+ owner: root
+
+ ###### Server update
+
+ - name: update server
+ template:
+ src: update
+ dest: /bin/update
+ force: yes
+ mode: 0775
+
+ ###### Server upgrade
+
+ - name: upgrade server
+ template:
+ src: upgrade
+ dest: /bin/upgrade
+ force: yes
+ mode: 0775
+ owner: root
+
+ ###### Install app
+
+ - name: install appname
+ template:
+ src: install
+ dest: /bin/install
+ force: yes
+ mode: 0775
+ owner: root
+
+ ###### Autoremove installed app packages
+
+ - name: autoremove unused packages after app install
+ template:
+ src: autoremove
+ dest: /bin/autoremove
+ force: yes
+ mode: 0775
+ owner: root
+
+ ###### Remove old docker containers (Tip from @barryclamsworth)
+
+ - name: Prune docker containers appname
+ template:
+ src: prune
+ dest: /bin/prune
+ force: yes
+ mode: 0775
+ owner: root
+
+ - name: Install PGFork
+ template:
+ src: pgfork
+ dest: /bin/pgfork
+ force: yes
+ mode: 0775
+
+ - name: Install Backup
+ template:
+ src: backup
+ dest: /bin/backup
+ force: yes
+ mode: 0775
diff --git a/menu/installer/folders.yml b/menu/installer/folders.yml
new file mode 100644
index 00000000..25dac8cd
--- /dev/null
+++ b/menu/installer/folders.yml
@@ -0,0 +1,188 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+---
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: Register Domain
+ shell: 'cat /var/plexguide/server.domain'
+ register: domain
+ ignore_errors: True
+
+ - name: Register IP
+ shell: 'cat /var/plexguide/server.ip'
+ register: ipaddress
+ ignore_errors: True
+
+ - name: Register Ports
+ shell: 'cat /var/plexguide/server.ports'
+ register: ports
+ ignore_errors: True
+
+ - name: Register HD Path
+ shell: 'cat /var/plexguide/server.hd.path'
+ register: path
+ ignore_errors: True
+
+ - name: Register HD Path
+ shell: 'cat /var/plexguide/server.hd.path'
+ register: path
+ ignore_errors: True
+
+ - name: Register Auth Path
+ shell: 'cat /var/plexguide/server.ht'
+ register: auth
+ ignore_errors: True
+
+ - name: Logging Ansible Role Information
+ shell: "echo 'INFO - Ansible Role Folders Started' > /var/plexguide/logs/pg.log && bash /opt/plexguide/menu/log/log.sh"
+
+ ############ GCrypt
+ - name: GCrypt Check
+ stat:
+ path: /mnt/gcrypt
+ register: gcrypt
+
+ - name: Create GCrypt Folders
+ file: 'path={{item}} state=directory mode=0775 owner=1000 group=1000'
+ with_items:
+ - /mnt/gcrypt/tv
+ - /mnt/gcrypt/movies
+ - /mnt/gcrypt/music
+ when: gcrypt.stat.exists == False
+ ignore_errors: yes
+ ############ TCrypt
+ - name: TCrypt Check
+ stat:
+ path: /mnt/tcrypt
+ register: tcrypt
+
+ - name: Create TCrypt Folders
+ file: 'path={{item}} state=directory mode=0775 owner=1000 group=1000'
+ with_items:
+ - /mnt/tcrypt/tv
+ - /mnt/tcrypt/movies
+ - /mnt/tcrypt/music
+ when: tcrypt.stat.exists == False
+ ignore_errors: yes
+
+ ############ Personal Containers Folder
+ - name: MyContainers Check
+ stat:
+ path: /opt/mycontainers
+ register: mycontainers
+
+ - name: Create MyContainers Folders
+ file: 'path={{item}} state=directory mode=0775 owner=1000 group=1000'
+ with_items:
+ - /opt/mycontainers
+ when: mycontainers.stat.exists == False
+ ignore_errors: yes
+
+ ############ PGUnion
+ - name: PGUnion Check
+ stat:
+ path: /mnt/unionfs
+ register: pgunion
+
+ - name: Create PGUnion Folders
+ file: 'path={{item}} state=directory mode=0775 owner=1000 group=1000'
+ with_items:
+ - /mnt/unionfs
+ when: pgunion.stat.exists == False
+ ignore_errors: yes
+
+ ############ TMP
+ - name: TMP Check
+ stat:
+ path: /mnt/tmp
+ register: tmp
+
+ - name: Create GDrive Folders
+ file: 'path={{item}} state=directory'
+ with_items:
+ - /mnt/tmp
+ when: tmp.stat.exists == False
+ ignore_errors: yes
+
+ ############ GDrive
+ - name: GDrive Check
+ stat:
+ path: /mnt/gdrive
+ register: gdrive
+
+ - name: Create GDrive Folders
+ file: 'path={{item}} state=directory mode=0775 owner=1000 group=1000'
+ with_items:
+ - /mnt/gdrive
+ - /mnt/gdrive/tv
+ - /mnt/gdrive/movies
+ - /mnt/gdrive/music
+ when: gdrive.stat.exists == False
+ ignore_errors: yes
+ ############ TCrypt
+ - name: Create GDrive Folders
+ file: 'path={{item}} state=directory mode=0775 owner=1000 group=1000'
+ with_items:
+ - /mnt/tcrypt
+ - /mnt/tcrypt/tv
+ - /mnt/tcrypt/movies
+ - /mnt/tcrypt/music
+ when: gdrive.stat.exists == False
+ ignore_errors: yes
+
+ ############ TDrive
+ - name: TDrive Check
+ stat:
+ path: /mnt/tdrive
+ register: tdrive
+
+ - name: Create TDrive Folders
+ file: 'path={{item}} state=directory mode=0775 owner=1000 group=1000'
+ with_items:
+ - /mnt/tdrive
+ - /mnt/tdrive/tv
+ - /mnt/tdrive/movies
+ - /mnt/tdrive/music
+ when: tdrive.stat.exists == False
+ ignore_errors: yes
+
+ ############ Encrypt
+ - name: Encrypt Check
+ stat:
+ path: /mnt/encrypt
+ register: encrypt
+
+ - name: Create Encrypt Folders
+ file: 'path={{item}} state=directory mode=0775 owner=1000 group=1000'
+ with_items:
+ - /mnt/encrypt/tv
+ - /mnt/encrypt/movies
+ - /mnt/encrypt/music
+ when: encrypt.stat.exists == False
+ ignore_errors: yes
+
+ - name: Create Basic Directories
+ file: 'path={{item}} state=directory mode=0775 owner=1000 group=1000 recurse=true'
+ with_items:
+ - '/opt/appdata/plexguide'
+ - '/opt/communityapps'
+ - '/opt/coreapps'
+ - '/var/plexguide'
+ - '/mnt/move'
+ - '/var/plexguide/logs'
+ - '/opt/appdata/plexguide/keys/unprocessed'
+ - '/opt/appdata/plexguide/keys/processed'
+ - '/opt/appdata/plexguide/keys/badjson'
+ - '{{path.stdout}}/pgops'
+ - '{{path.stdout}}/downloads'
+ - '{{path.stdout}}/nzb'
+ - '{{path.stdout}}/torrent'
+ - '{{path.stdout}}/move'
+ - '/opt/var'
+ ignore_errors: yes
diff --git a/menu/interface/cloudselect.sh b/menu/interface/cloudselect.sh
new file mode 100644
index 00000000..22a47764
--- /dev/null
+++ b/menu/interface/cloudselect.sh
@@ -0,0 +1,43 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+# Menu Interface
+tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+📂 Cloud Service Installer
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+[1] Cloud Instance: Google
+[2] Cloud Instance: Hetzner
+Z - Exit
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+
+# Standby
+read -p 'Type a Number | Press [ENTER]: ' typed /var/plexguide/type.choice
+ bash /opt/plexguide/menu/pgcloner/blitzgce.sh
+elif [ "$typed" == "2" ]; then
+ bash /opt/plexguide/menu/pgcloner/hetzner.sh
+elif [ "$typed" == "Z" ] || [ "$typed" == "z" ]; then
+ exit
+else
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+⛔️ WARNING! - Please Make a Valid Selection!
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+EOF
+
+ bash /opt/plexguide/menu/cloudselect/cloudselect.sh
+ exit
+fi
diff --git a/menu/interface/ending.sh b/menu/interface/ending.sh
new file mode 100644
index 00000000..a9eef7b4
--- /dev/null
+++ b/menu/interface/ending.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+#
+# Title: pts (Reference Title File)
+# Author(s): Admin9705 - FlickerRate
+# URL: https://pts.com - http://github.pts.com
+# GNU: General Public License v3.0
+################################################################################
+source /opt/plexguide/menu/functions/install.sh
+emergency
+
+# PG ascii art with color
+echo ""
+<<-EOF
+┌─────────────────────────────────────┐
+│ -== Team PTS ==- │
+│ ————————————————————————————————————│
+│ Restart PTS: pts │
+│ Update PTS: ptsupdate │
+│ View the PG Blitz Logs: blitz │
+│ Download Your PG Fork: pgfork │
+│ ————————————————————————————————————│
+│ Thanks For Being Part of the Team │
+└─────────────────────────────────────┘
+
+EOF
+
+if [[ ! -e "/bin/pts" ]]; then
+ cp /opt/plexguide/menu/alias/templates/pts /bin
+fi
+
+chown 1000:1000 /bin/pts &>/dev/null &
+chmod 0755 /bin/pts &>/dev/null &
diff --git a/menu/interface/gce/choice.yml b/menu/interface/gce/choice.yml
new file mode 100644
index 00000000..3f3270d7
--- /dev/null
+++ b/menu/interface/gce/choice.yml
@@ -0,0 +1,44 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+---
+- name: Register Project
+ shell: 'cat /var/plexguide/project.final'
+ register: project
+
+- name: Register Account
+ shell: 'cat /var/plexguide/project.account'
+ register: account
+
+- name: Register Processor Count
+ shell: 'cat /var/plexguide/project.processor'
+ register: processor
+
+- name: Register IP region
+ shell: 'cat /var/plexguide/project.ipregion'
+ register: ipregion
+
+- name: Register IP region
+ shell: 'cat /var/plexguide/project.ipaddress'
+ register: ipaddress
+
+- name: Register Deployment Status
+ shell: 'cat /var/plexguide/gce.deployed.status'
+ register: deployment
+
+- name: 'Key Menu Facts'
+ set_fact:
+ head1: "\nPGBlitz GCE Deployment Interface - Make a Selection"
+ head2: ''
+ info2: "\n2. Log-In to Your Account: {{account.stdout}}"
+ info3: "\n3. Build a New Project"
+ info4: "\n4. Establish Project ID : [{{project.stdout}}]"
+ info5: "\n5. Set Processor Count : [{{processor.stdout}}]"
+ info6: "\n6. Set IP Region / Server: [{{ipaddress.stdout}}] - [{{ipregion.stdout}}]"
+ info7: "\n7. Deploy PG GCE Server : [{{deployment.stdout}}]"
+ info8: "\n8. SSH Securely into your GCE Feeder Box"
+ info9: "\n9. Destroy Server"
diff --git a/menu/interface/gce/file.sh b/menu/interface/gce/file.sh
new file mode 100644
index 00000000..29f67e1d
--- /dev/null
+++ b/menu/interface/gce/file.sh
@@ -0,0 +1,826 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+menu=$(cat /var/plexguide/final.choice)
+
+if [ "$menu" == "2" ]; then
+ ########## Server Must Not Be Deployed - START
+ echo ""
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Checking Existing Deployment"
+ echo "--------------------------------------------------------"
+ echo ""
+ inslist=$(gcloud compute instances list | grep pg-gce)
+ if [ "$inslist" != "" ]; then
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Failed! Must Delete Current Server!"
+ echo "--------------------------------------------------------"
+ echo ""
+ echo "NOTE: Prevents Conflicts with Changes!"
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ exit
+ fi
+ ########## Server Must Not Be Deployed - END
+
+ gcloud auth login
+ echo "[NOT SET]" >/var/plexguide/project.final
+fi
+
+if [ "$menu" == "3" ]; then
+ ############################## BILLING CHECKS - START
+ billing=$(gcloud beta billing accounts list | grep "\")
+ if [ "$billing" == "" ]; then
+ echo ""
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Google Cloud Billing is Not Turned On!"
+ echo "--------------------------------------------------------"
+ echo ""
+ echo "NOTE: You Must Turn On Your Billing! PG is checking for the word >>> True"
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ echo ""
+ exit
+ fi
+ ############################## BILLING CHECKS - END
+
+ echo ""
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Creating Project ID"
+ echo "--------------------------------------------------------"
+ echo ""
+ date=$(date +%m%d)
+ rand=$(echo $((1 + RANDOM + RANDOM + RANDOM + RANDOM + RANDOM + RANDOM + RANDOM + RANDOM + RANDOM + RANDOM)))
+ projectid="pg-$date-$rand"
+ gcloud projects create $projectid
+ sleep 1
+ echo ""
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Linking Project to the Billing Account"
+ echo "--------------------------------------------------------"
+ echo ""
+
+ echo ""
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Created - Project $projectid"
+ echo "--------------------------------------------------------"
+ echo ""
+ echo "NOTE: If using this project, ENSURE to SET this project!"
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+fi
+
+if [ "$menu" == "4" ]; then
+ ############################## BILLING CHECKS - START
+ billing=$(gcloud beta billing accounts list | grep "\")
+ if [ "$billing" == "" ]; then
+ echo ""
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Google Cloud Billing is Not Turned On!"
+ echo "--------------------------------------------------------"
+ echo ""
+ echo "NOTE: You Must Turn On Your Billing! PG is checking for the word >>> True"
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ echo ""
+ exit
+ fi
+ ############################## BILLING CHECKS - END
+ ########## Server Must Not Be Deployed - START
+ echo ""
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Checking Existing Deployment"
+ echo "--------------------------------------------------------"
+ echo ""
+
+ inslist=$(gcloud compute instances list | grep pg-gce)
+ if [ "$inslist" != "" ]; then
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Failed! Must Delete Current Server!"
+ echo "--------------------------------------------------------"
+ echo ""
+ echo "NOTE: Prevents Conflicts with Changes!"
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ exit
+ fi
+ ########## Server Must Not Be Deployed - END
+
+ gcloud projects list && gcloud projects list >/var/plexguide/projects.list
+ echo ""
+ echo "------------------------------------------------------------------------------"
+ echo "SYSTEM MESSAGE: GCloud Project Interface"
+ echo "------------------------------------------------------------------------------"
+ echo ""
+ echo "NOTE: If no project is listed, please visit https://project.pgblitz.com and"
+ echo " review the wiki on how to build a project! Without one, this will fail!"
+ echo ""
+ read -p "Set or Change the Project ID (y/n)? " -n 1 -r
+ echo # move cursor to a new line
+ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
+ echo ""
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: [Y] Key was NOT Selected - Exiting!"
+ echo "--------------------------------------------------------"
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ echo ""
+ exit 1
+ else
+ echo "" # leave if statement and continue.
+ fi
+
+ typed=nullstart
+ while [ "$typed" != "$list" ]; do
+ echo "------------------------------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Project Selection Interface"
+ echo "------------------------------------------------------------------------------"
+ echo ""
+ cat /var/plexguide/projects.list | cut -d' ' -f1 | tail -n +2
+ cat /var/plexguide/projects.list | cut -d' ' -f1 | tail -n +2 >/var/plexguide/project.cut
+ echo ""
+ echo "NOTE: Type the Name of the Project you want to utilize!"
+ read -p 'Type the Name of the Project to Utlize & Press [ENTER]: ' typed
+ list=$(cat /var/plexguide/project.cut | grep $typed)
+ echo ""
+
+ if [ "$typed" != "$list" ]; then
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Failed! Please type the exact name!"
+ echo "--------------------------------------------------------"
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ else
+ echo "----------------------------------------------"
+ echo "SYSTEM MESSAGE: Passed the Validation Checks!"
+ echo "----------------------------------------------"
+ echo ""
+ echo "Set Project is: $list"
+ gcloud config set project $typed
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ echo ""
+ echo ""
+ echo "----------------------------------------------"
+ echo "SYSTEM MESSAGE: Enabling Your API!"
+ echo "----------------------------------------------"
+ echo ""
+ echo "NOTE: Enabling Compute API - Please Standby!"
+ gcloud services enable compute.googleapis.com
+ echo ""
+ echo "NOTE: Enabling GDrive API for Project - $typed"
+ gcloud services enable drive.googleapis.com --project $typed
+ echo ""
+ sleep 1
+ echo "----------------------------------------------"
+ echo "SYSTEM MESSAGE: Finished!"
+ echo "----------------------------------------------"
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ fi
+ done
+
+ echo $typed >/var/plexguide/project.final
+ echo 'INFO - Selected: Exiting Application Suite Interface' >/var/plexguide/logs/pg.log && bash /opt/plexguide/menu/log/log.sh
+ exit
+fi
+
+if [ "$menu" == "5" ]; then
+ ############################## BILLING CHECKS - START
+ billing=$(gcloud beta billing accounts list | grep "\")
+ if [ "$billing" == "" ]; then
+ echo ""
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Google Cloud Billing is Not Turned On!"
+ echo "--------------------------------------------------------"
+ echo ""
+ echo "NOTE: You Must Turn On Your Billing! PG is checking for the word >>> True"
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ echo ""
+ exit
+ fi
+ ############################## BILLING CHECKS - END
+ ############################## PROJECT BILLING CHECKS - START
+ project=$(cat /var/plexguide/project.final)
+ projectlink=$(gcloud beta billing accounts list | grep "\" | awk '{ print $1 }')
+ billingcheck=$(gcloud beta billing projects link $project --billing-account $projectlink | grep "billingEnabled: true")
+ if [ "$billingcheck" == "" ]; then
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Billing Failed - Turn It On Or Check"
+ echo "--------------------------------------------------------"
+ echo ""
+ echo "NOTE: Common Billing Issue for GCE Credits"
+ echo "NOTE: Cannot Continue with GCE"
+ echo ""
+ echo "1. Too Many Projects - Delete Unused Ones!"
+ echo "2. Ran Out of Credits & Must Turn On (Warning - Expensive)"
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ echo ""
+ exit
+ fi
+ ############################## PROJECT BILLING CHECKS - END
+
+ ########## Server Must Not Be Deployed - START
+ echo ""
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Checking Existing Deployment"
+ echo "--------------------------------------------------------"
+ echo ""
+
+ inslist=$(gcloud compute instances list | grep pg-gce)
+ if [ "$inslist" != "" ]; then
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Failed! Must Delete Current Server!"
+ echo "--------------------------------------------------------"
+ echo ""
+ echo "NOTE: Prevents Conflicts with Changes!"
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ exit
+ fi
+ ########## Server Must Not Be Deployed - END
+
+ ### Part 1
+ pcount=$(cat /var/plexguide/project.processor)
+ echo ""
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Current Processor Count Interface"
+ echo "--------------------------------------------------------"
+ echo ""
+ echo "NOTE: Processor Count: [$pcount]"
+ echo ""
+ read -p "Set or Change the Processor Count (y/n)? " -n 1 -r
+ echo # move cursor to a new line
+ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
+ echo ""
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: [Y] Key was NOT Selected - Exiting!"
+ echo "--------------------------------------------------------"
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ echo ""
+ exit 1
+ else
+ echo ""
+ fi
+
+ ### part 2
+ typed=nullstart
+ prange="2 4 6"
+ tcheck=""
+ break=off
+ while [ "$break" == "off" ]; do
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Processor Count Interface"
+ echo "--------------------------------------------------------"
+ echo ""
+ echo "Ideal Processor Usage = 4"
+ echo "Set Your Processor Count | Range 2, 4 or 6"
+ echo ""
+ echo "NOTE: More Processors = Faster Credit Drain"
+ echo ""
+ read -p 'Type a Number 2, 4 or 6 | PRESS [ENTER]: ' typed
+ tcheck=$(echo $prange | grep $typed)
+ echo ""
+
+ if [ "$tcheck" == "" ]; then
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Failed! Type a Number from 2, 4, or 6"
+ echo "--------------------------------------------------------"
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ echo ""
+ echo ""
+ else
+ echo "----------------------------------------------"
+ echo "SYSTEM MESSAGE: Passed! Process Count $typed Set"
+ echo "----------------------------------------------"
+ echo ""
+ echo $typed >/var/plexguide/project.processor
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ break=on
+ fi
+ done
+
+fi
+
+if [ "$menu" == "6" ]; then
+ ############################## BILLING CHECKS - START
+ billing=$(gcloud beta billing accounts list | grep "\")
+ if [ "$billing" == "" ]; then
+ echo ""
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Google Cloud Billing is Not Turned On!"
+ echo "--------------------------------------------------------"
+ echo ""
+ echo "NOTE: You Must Turn On Your Billing! PG is checking for the word >>> True"
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ echo ""
+ exit
+ fi
+ ############################## BILLING CHECKS - END
+ ############################## PROJECT BILLING CHECKS - START
+ project=$(cat /var/plexguide/project.final)
+ projectlink=$(gcloud beta billing accounts list | grep "\" | awk '{ print $1 }')
+ billingcheck=$(gcloud beta billing projects link $project --billing-account $projectlink | grep "billingEnabled: true")
+ if [ "$billingcheck" == "" ]; then
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Billing Failed - Turn It On Or Check"
+ echo "--------------------------------------------------------"
+ echo ""
+ echo "NOTE: Common Billing Issue for GCE Credits"
+ echo "NOTE: Cannot Continue with GCE"
+ echo ""
+ echo "1. Too Many Projects - Delete Unused Ones!"
+ echo "2. Ran Out of Credits & Must Turn On (Warning - Expensive)"
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ echo ""
+ exit
+ fi
+ ############################## PROJECT BILLING CHECKS - END
+
+ ########## Server Must Not Be Deployed - START
+ echo ""
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Checking Existing Deployment"
+ echo "--------------------------------------------------------"
+ echo ""
+
+ inslist=$(gcloud compute instances list | grep pg-gce)
+ if [ "$inslist" != "" ]; then
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Failed! Must Delete Current Server!"
+ echo "--------------------------------------------------------"
+ echo ""
+ echo "NOTE: Prevents Conflicts with Changes!"
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ exit
+ fi
+ ########## Server Must Not Be Deployed - END
+
+ gcloud compute regions list | awk '{print $1}' | tail -n +2 >/tmp/regions.list
+ num=0
+ echo " " >/tmp/regions.print
+
+ while read p; do
+ echo -n $p >>/tmp/regions.print
+ echo -n " " >>/tmp/regions.print
+
+ num=$((num + 1))
+ if [ $num == 5 ]; then
+ num=0
+ echo " " >>/tmp/regions.print
+ fi
+ done /var/plexguide/project.region
+
+ typed=nullstart
+ prange=$(cat /tmp/regions.print)
+ tcheck=""
+ break=off
+ while [ "$break" == "off" ]; do
+ echo ""
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Google Cloud IP Regions List"
+ echo "--------------------------------------------------------"
+ cat /tmp/regions.print
+ echo "" && echo ""
+ read -p 'Type the Name of an IP Region | PRESS [ENTER]: ' typed
+ echo ""
+ tcheck=$(echo $prange | grep $typed)
+
+ if [ "$tcheck" == "" ]; then
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Failed! Type an IP Region Name"
+ echo "--------------------------------------------------------"
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ echo ""
+ echo ""
+ else
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Passed! IP Region $typed Set"
+ echo "--------------------------------------------------------"
+ echo ""
+ echo $typed >/var/plexguide/project.ipregion
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ echo ""
+ echo ""
+ break=on
+ fi
+ done
+
+ ############## IP Address - Part 2
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Deleting Any Prior GCE IP Addresses"
+ echo "--------------------------------------------------------"
+ echo ""
+ echo "NOTE: Please Standby"
+
+ break=off
+ while [ "$break" == off ]; do
+
+ gcloud compute addresses list | grep pg-gce | tail -n +1 >/tmp/ip.delete
+ ipdelete=$(cat /tmp/ip.delete)
+ if [ "$ipdelete" != "" ]; then
+ regdelete=$(gcloud compute addresses list | grep pg-gce | head -n +1 | awk '{print $2}')
+ addprint=$(gcloud compute addresses list | grep pg-gce | head -n +1 | awk '{print $3}')
+ gcloud compute addresses delete pg-gce --region=$regdelete --quiet
+ echo ""
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Deleted $regdelete - $addprint"
+ echo "--------------------------------------------------------"
+ else
+ break=on
+ fi
+ done
+
+ echo ""
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Creating New IP Address"
+ echo "--------------------------------------------------------"
+ echo ""
+ echo "NOTE: Please Standby"
+ echo ""
+ projectname=$(cat /var/plexguide/project.final)
+ region=$(cat /var/plexguide/project.ipregion)
+ gcloud compute addresses create pg-gce --region $region --project $projectname
+ gcloud compute addresses list | grep pg-gce | awk '{print $3}' >/var/plexguide/project.ipaddress
+ ipaddress=$(cat /var/plexguide/project.ipaddress)
+ sleep 1.5
+ echo "" &
+ echo ""
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Passed! GCE IP: $ipaddress"
+ echo "--------------------------------------------------------"
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ echo ""
+
+ ########## Server Must Not Be Deployed - START
+ echo ""
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Checking Existing Deployment"
+ echo "--------------------------------------------------------"
+ echo ""
+
+ inslist=$(gcloud compute instances list | grep pg-gce)
+ if [ "$inslist" != "" ]; then
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Failed! Must Delete Current Server!"
+ echo "--------------------------------------------------------"
+ echo ""
+ echo "NOTE: Prevents Conflicts with Changes!"
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ exit
+ fi
+ ########## Server Must Not Be Deployed - END
+
+ ### Part 1
+ ipregion=$(cat /var/plexguide/project.ipregion)
+ gcloud compute zones list | awk '{print $1}' | tail -n +2 | grep $ipregion >/tmp/zones.list
+ num=0
+ echo " " >/tmp/zones.print
+
+ while read p; do
+ echo -n $p >>/tmp/zones.print
+ echo -n " " >>/tmp/zones.print
+
+ num=$((num + 1))
+ if [ $num == 4 ]; then
+ num=0
+ echo " " >>/tmp/zones.print
+ fi
+ done /var/plexguide/project.location
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ break=on
+ fi
+ done
+
+fi
+
+################################################################################ DEPLOY END
+
+if [ "$menu" == "7" ]; then
+ ############################## BILLING CHECKS - START
+ billing=$(gcloud beta billing accounts list | grep "\")
+ if [ "$billing" == "" ]; then
+ echo ""
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Google Cloud Billing is Not Turned On!"
+ echo "--------------------------------------------------------"
+ echo ""
+ echo "NOTE: You Must Turn On Your Billing! PG is checking for the word >>> True"
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ echo ""
+ exit
+ fi
+ ############################## BILLING CHECKS - END
+ ############################## PROJECT BILLING CHECKS - START
+ project=$(cat /var/plexguide/project.final)
+ projectlink=$(gcloud beta billing accounts list | grep "\" | awk '{ print $1 }')
+ billingcheck=$(gcloud beta billing projects link $project --billing-account $projectlink | grep "billingEnabled: true")
+ if [ "$billingcheck" == "" ]; then
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Billing Failed - Turn It On Or Check"
+ echo "--------------------------------------------------------"
+ echo ""
+ echo "NOTE: Common Billing Issue for GCE Credits"
+ echo "NOTE: Cannot Continue with GCE"
+ echo ""
+ echo "1. Too Many Projects - Delete Unused Ones!"
+ echo "2. Ran Out of Credits & Must Turn On (Warning - Expensive)"
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ echo ""
+ exit
+ fi
+ ############################## PROJECT BILLING CHECKS - END
+
+ ########## Server Must Not Be Deployed - START
+ echo ""
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Checking Existing Deployment"
+ echo "--------------------------------------------------------"
+ echo ""
+ ##########
+ project=$(cat /var/plexguide/project.final)
+ ipaddress=$(cat /var/plexguide/project.ipaddress)
+ location=$(cat /var/plexguide/project.location)
+ region=$(cat /var/plexguide/project.ipregion)
+ cpu=$(cat /var/plexguide/project.processor)
+
+ inslist=$(gcloud compute instances list | grep pg-gce)
+ if [ "$inslist" != "" ]; then
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Failed! Must Delete Current Server!"
+ echo "--------------------------------------------------------"
+ echo ""
+ echo "NOTE: Prevents Conflicts with Changes!"
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ exit
+ fi
+ ########## Server Must Not Be Deployed - END
+
+ ############ FireWall
+ echo ""
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Checking PG GCE Firewall Rules"
+ echo "--------------------------------------------------------"
+ echo ""
+
+ inslist=$(gcloud compute firewall-rules list | grep plexguide)
+ if [ "$inslist" == "" ]; then
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: FireWall Rules Do Not Exist!"
+ echo "--------------------------------------------------------"
+ echo ""
+ echo "NOTE: Building Firewall Rules! Please Wait"
+ echo ""
+ gcloud compute firewall-rules create plexguide --allow all
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ fi
+
+ ########### Deployment
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Building PG GCE Template"
+ echo "--------------------------------------------------------"
+ echo ""
+ echo "NOTE: Please Standby!"
+ echo ""
+
+ blueprint=$(gcloud compute instance-templates list | grep pg-gce-blueprint)
+ if [ "$blueprint" != "" ]; then
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Deleting Old Templates"
+ echo "--------------------------------------------------------"
+ echo ""
+ echo "NOTE: Please Standby!"
+ echo ""
+ gcloud compute instance-templates delete pg-gce-blueprint --quiet
+ echo ""
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Building New Template"
+ echo "--------------------------------------------------------"
+ echo ""
+ echo "NOTE: Please Standby!"
+ echo ""
+ fi
+
+ gcloud compute instance-templates create pg-gce-blueprint \
+ --custom-cpu $cpu --custom-memory 8GB \
+ --image-family ubuntu-1804-lts --image-project ubuntu-os-cloud \
+ --boot-disk-auto-delete --boot-disk-size 100GB \
+ --local-ssd interface=nvme
+
+ sleep .5
+
+ echo ""
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Deploying PG GCE Server"
+ echo "--------------------------------------------------------"
+ echo ""
+ echo "NOTE: Please Standby!"
+ echo ""
+ gcloud compute instances create pg-gce --source-instance-template pg-gce-blueprint --zone $location
+ echo ""
+
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Assigning the IP Address to the GCE"
+ echo "--------------------------------------------------------"
+ echo ""
+ echo "NOTE: Please Standby"
+ echo ""
+
+ gcloud compute instances delete-access-config pg-gce --access-config-name "external-nat" --zone $location --quiet
+ echo ""
+ gcloud compute instances add-access-config pg-gce --access-config-name "external-nat" --address $ipaddress
+ echo ""
+
+ ######## Final Checks
+ finalchecks=$(gcloud compute instances list | grep pg-gce)
+ if [ "finalchecks" != "" ]; then
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Deployment Complete"
+ echo "--------------------------------------------------------"
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ touch /var/plexguide/gce.deployed
+ else
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Deployment Failed"
+ echo "--------------------------------------------------------"
+ echo ""
+ echo "NOTE: Unable to detect a running PG-GCE Server!"
+ echo "Please check your configs, billings, and permissions"
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ fi
+fi
+
+################################################################################ DEPLOY END
+if [ "$menu" == "8" ]; then
+ ############################## BILLING CHECKS - START
+ billing=$(gcloud beta billing accounts list | grep "\")
+ if [ "$billing" == "" ]; then
+ echo ""
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Google Cloud Billing is Not Turned On!"
+ echo "--------------------------------------------------------"
+ echo ""
+ echo "NOTE: You Must Turn On Your Billing! PG is checking for the word >>> True"
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ echo ""
+ exit
+ fi
+ ############################## BILLING CHECKS - END
+ ############################## PROJECT BILLING CHECKS - START
+ project=$(cat /var/plexguide/project.final)
+ projectlink=$(gcloud beta billing accounts list | grep "\" | awk '{ print $1 }')
+ billingcheck=$(gcloud beta billing projects link $project --billing-account $projectlink | grep "billingEnabled: true")
+ if [ "$billingcheck" == "" ]; then
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Billing Failed - Turn It On Or Check"
+ echo "--------------------------------------------------------"
+ echo ""
+ echo "NOTE: Common Billing Issue for GCE Credits"
+ echo "NOTE: Cannot Continue with GCE"
+ echo ""
+ echo "1. Too Many Projects - Delete Unused Ones!"
+ echo "2. Ran Out of Credits & Must Turn On (Warning - Expensive)"
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ echo ""
+ exit
+ fi
+ ############################## PROJECT BILLING CHECKS - END
+
+ ######## Final Message
+ echo ""
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Securely Entering Your GCE Feeder Box"
+ echo "--------------------------------------------------------"
+ echo ""
+ echo "NOTE: If asked to create keys, remember the passcodes!"
+ echo "1. To exit the GCE, type exit!"
+ echo "2. Install PG on your GCE and Select Feeder Edition!"
+ echo "3. Problems? Try rm -rf /root/.ssh/google_compute_engine"
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ echo ""
+ echo ""
+ ipproject=$(cat /var/plexguide/project.location)
+ gcloud compute ssh pg-gce --zone "$ipproject"
+ echo ""
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Welcome Back To Your Main Server"
+ echo "--------------------------------------------------------"
+ echo ""
+ echo "NOTE: Sanity Check - You Exited Your GCE Feeder Box"
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+fi
+
+if [ "$menu" == "9" ]; then
+ ############################## BILLING CHECKS - START
+ billing=$(gcloud beta billing accounts list | grep "\")
+ if [ "$billing" == "" ]; then
+ echo ""
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Google Cloud Billing is Not Turned On!"
+ echo "--------------------------------------------------------"
+ echo ""
+ echo "NOTE: You Must Turn On Your Billing! PG is checking for the word >>> True"
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ echo ""
+ exit
+ fi
+ ############################## BILLING CHECKS - END
+ ############################## PROJECT BILLING CHECKS - START
+ project=$(cat /var/plexguide/project.final)
+ projectlink=$(gcloud beta billing accounts list | grep "\" | awk '{ print $1 }')
+ billingcheck=$(gcloud beta billing projects link $project --billing-account $projectlink | grep "billingEnabled: true")
+ if [ "$billingcheck" == "" ]; then
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Billing Failed - Turn It On Or Check"
+ echo "--------------------------------------------------------"
+ echo ""
+ echo "NOTE: Common Billing Issue for GCE Credits"
+ echo "NOTE: Cannot Continue with GCE"
+ echo ""
+ echo "1. Too Many Projects - Delete Unused Ones!"
+ echo "2. Ran Out of Credits & Must Turn On (Warning - Expensive)"
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ echo ""
+ exit
+ fi
+ ############################## PROJECT BILLING CHECKS - END
+
+ echo ""
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Destroying GCE Server"
+ echo "--------------------------------------------------------"
+ echo ""
+ location=$(cat /var/plexguide/project.location)
+ echo "NOTE: Please Standby"
+ echo ""
+ gcloud compute instances delete pg-gce --quiet --zone "$location"
+ rm -rf /root/.ssh/google_compute_engine 1>/dev/null 2>&1
+ rm -rf /var/plexguide/gce.deployed 1>/dev/null 2>&1
+ echo ""
+ echo "--------------------------------------------------------"
+ echo "SYSTEM MESSAGE: PG GCE Server Destroyed!"
+ echo "--------------------------------------------------------"
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+fi
diff --git a/menu/interface/gce/var.sh b/menu/interface/gce/var.sh
new file mode 100644
index 00000000..088c171c
--- /dev/null
+++ b/menu/interface/gce/var.sh
@@ -0,0 +1,42 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+echo 9 >/var/plexguide/menu.number
+
+gcloud info | grep Account: | cut -c 10- >/var/plexguide/project.account
+
+file="/var/plexguide/project.final"
+if [ ! -e "$file" ]; then
+ echo "[NOT SET]" >/var/plexguide/project.final
+fi
+
+file="/var/plexguide/project.processor"
+if [ ! -e "$file" ]; then
+ echo "NOT-SET" >/var/plexguide/project.processor
+fi
+
+file="/var/plexguide/project.location"
+if [ ! -e "$file" ]; then
+ echo "NOT-SET" >/var/plexguide/project.location
+fi
+
+file="/var/plexguide/project.ipregion"
+if [ ! -e "$file" ]; then
+ echo "NOT-SET" >/var/plexguide/project.ipregion
+fi
+
+file="/var/plexguide/project.ipaddress"
+if [ ! -e "$file" ]; then
+ echo "IP NOT-SET" >/var/plexguide/project.ipaddress
+fi
+
+file="/var/plexguide/gce.deployed"
+if [ -e "$file" ]; then
+ echo "Server Deployed" >/var/plexguide/gce.deployed.status
+else
+ echo "Not Deployed" >/var/plexguide/gce.deployed.status
+fi
diff --git a/menu/interface/install/scripts/ansible.sh b/menu/interface/install/scripts/ansible.sh
new file mode 100644
index 00000000..908e46b6
--- /dev/null
+++ b/menu/interface/install/scripts/ansible.sh
@@ -0,0 +1,46 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+
+######################################################## Declare Variables
+sname="Ansible - Install"
+pg_ansible=$(cat /var/plexguide/pg.ansible)
+pg_ansible_stored=$(cat /var/plexguide/pg.ansible.stored)
+######################################################## START: PG Log
+sudo echo "INFO - Start of Script: $sname" >/var/plexguide/logs/pg.log
+sudo bash /opt/plexguide/menu/log/log.sh
+######################################################## START: Main Script
+if [ "$pg_ansible" == "$pg_ansible_stored" ]; then
+ echo "" 1>/dev/null 2>&1
+else
+ echo "Installing / Upgrading Ansible" >/var/plexguide/message.phase
+ bash /opt/plexguide/menu/interface/install/scripts/message.sh
+ echo ""
+ python -m pip install --disable-pip-version-check --upgrade --force-reinstall ansible==${1-2.5.11}
+ ############# FOR ANSIBLE
+ mkdir -p /etc/ansible/inventories/ 1>/dev/null 2>&1
+ echo "[local]" >/etc/ansible/inventories/local
+ echo "127.0.0.1 ansible_connection=local" >>/etc/ansible/inventories/local
+
+ ### Reference: https://docs.ansible.com/ansible/2.4/intro_configuration.html
+ echo "[defaults]" >/etc/ansible/ansible.cfg
+ echo "deprecation_warnings=False" >>/etc/ansible/ansible.cfg
+ echo "command_warnings = False" >>/etc/ansible/ansible.cfg
+ echo "callback_whitelist = profile_tasks" >>/etc/ansible/ansible.cfg
+ echo "inventory = /etc/ansible/inventories/local" >>/etc/ansible/ansible.cfg
+
+ ### Disabling cows for people that have cowsay installed
+ echo "nocows = 1" >>/etc/ansible/ansible.cfg
+
+ cat /var/plexguide/pg.ansible >/var/plexguide/pg.ansible.stored
+fi
+######################################################## END: Main Script
+#
+#
+######################################################## END: PG Log
+sudo echo "INFO - END of Script: $sname" >/var/plexguide/logs/pg.log
+sudo bash /opt/plexguide/menu/log/log.sh
diff --git a/menu/interface/install/scripts/edition.sh b/menu/interface/install/scripts/edition.sh
new file mode 100644
index 00000000..a50cfddc
--- /dev/null
+++ b/menu/interface/install/scripts/edition.sh
@@ -0,0 +1,27 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+
+######################################################## Declare Variables
+sname="PG Installer: Set PG Edition"
+pg_edition=$(cat /var/plexguide/pg.edition)
+pg_edition_stored=$(cat /var/plexguide/pg.edition.stored)
+######################################################## START: PG Log
+sudo echo "INFO - Start of Script: $sname" >/var/plexguide/logs/pg.log
+sudo bash /opt/plexguide/menu/log/log.sh
+######################################################## START: Main Script
+if [ "$pg_edition" == "$pg_edition_stored" ]; then
+ echo "" 1>/dev/null 2>&1
+else
+ bash /opt/plexguide/menu/editions/editions.sh
+fi
+######################################################## END: Main Script
+#
+#
+######################################################## END: PG Log
+sudo echo "INFO - END of Script: $sname" >/var/plexguide/logs/pg.log
+sudo bash /opt/plexguide/menu/log/log.sh
diff --git a/menu/interface/install/scripts/message.sh b/menu/interface/install/scripts/message.sh
new file mode 100644
index 00000000..84c38f51
--- /dev/null
+++ b/menu/interface/install/scripts/message.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+message=$(cat /var/plexguide/message.phase)
+
+echo ""
+echo "----------------------------------------------------"
+echo "PLEASE STANDBY"
+echo "System Message: $message"
+echo "----------------------------------------------------"
+sleep 2
diff --git a/menu/interface/serverid.sh b/menu/interface/serverid.sh
new file mode 100644
index 00000000..72257b05
--- /dev/null
+++ b/menu/interface/serverid.sh
@@ -0,0 +1,51 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+touch /var/plexguide/server.id.stored
+start=$(cat /var/plexguide/server.id)
+stored=$(cat /var/plexguide/server.id.stored)
+
+if [ "$start" != "$stored" ]; then
+
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+↘️ ESTABLISHING ~ Server's Identification
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+⚠️ WARNING: Use > One Word - All LowerCase & Keep it Simple!
+
+EOF
+
+ # Standby
+ read -p '🌏 TYPE Server ID | Press [ENTER]: ' typed /var/plexguide/server.id
+ cat /var/plexguide/server.id >/var/plexguide/server.id.stored
+
+ sleep 3
+ fi
+
+fi
diff --git a/menu/interface/serverid/choice.yml b/menu/interface/serverid/choice.yml
new file mode 100644
index 00000000..917c6dc2
--- /dev/null
+++ b/menu/interface/serverid/choice.yml
@@ -0,0 +1,21 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq - Sub7Seven
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+---
+- name: Register Project
+ shell: 'cat /var/plexguide/server.id'
+ register: tempid
+
+- name: 'Set Server ID'
+ set_fact:
+ serverid: '{{tempid.stdout}}'
+
+- name: 'Key Menu Facts'
+ set_fact:
+ head1: "\nPG Server Identification Interface"
+ head2: "\nServer ID: {{serverid}}"
+ info2: "\n2. Server ID: Change It"
diff --git a/menu/interface/serverid/file.sh b/menu/interface/serverid/file.sh
new file mode 100644
index 00000000..0e948221
--- /dev/null
+++ b/menu/interface/serverid/file.sh
@@ -0,0 +1,90 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq - Sub7Seven
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+menu=$(cat /var/plexguide/final.choice)
+
+if [ "$menu" == "2" ]; then
+ echo ""
+ echo "-----------------------------------------------------"
+ echo "SYSTEM MESSAGE: Please Read the Following Information"
+ echo "-----------------------------------------------------"
+ echo ""
+ echo "NOTE: Setting the Server ID enables the Server to have"
+ echo "a unique name for backup and setup purposes."
+ echo ""
+ echo "Remember KISS: Keep-It-Simple Stupid! Create a simple"
+ echo "one word server ID such as hetzner1 or myserver"
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue"
+ echo ""
+ echo ""
+ read -p "Set or Change the Server ID (y/n)? " -n 1 -r
+ echo # move cursor to a new line
+ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
+ echo ""
+ echo "---------------------------------------------------"
+ echo "SYSTEM MESSAGE: [Y] Key was NOT Selected - Exiting!"
+ echo "---------------------------------------------------"
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ echo ""
+ exit 1
+ fi
+
+ break=no
+ while [ "$break" == "no" ]; do
+ echo ""
+ read -p 'Type a Sever ID & Then Press [ENTER]: ' typed
+ #typed=typed+0
+ echo ""
+ echo "-------------------------------------------------"
+ echo "SYSTEM MESSAGE: Server ID - $typed"
+ echo "-------------------------------------------------"
+ echo ""
+ read -p "Continue with the Set Server ID (y/n)? " -n 1 -r
+
+ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
+ echo ""
+ echo "---------------------------------------------------"
+ echo "SYSTEM MESSAGE: [Y] Key was NOT Selected"
+ echo "---------------------------------------------------"
+ echo ""
+ echo "You will be able to set the Server ID Again!"
+ echo
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ echo ""
+ else
+ echo ""
+ echo "---------------------------------------------------"
+ echo "SYSTEM MESSAGE: Server ID - $typed"
+ echo "---------------------------------------------------"
+ echo ""
+ echo "Your Server ID is Now Set! Thank you!"
+ echo ""
+ echo $typed >/var/plexguide/server.id
+ break=yes
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ echo ""
+ fi
+ done
+
+#### Final fi
+fi
+
+idtest=$(cat /var/plexguide/server.id)
+if [ "$idtest" == "NOT-SET" ]; then
+ echo ""
+ echo "---------------------------------------------------"
+ echo "SYSTEM MESSAGE: You Must Create a SERVER ID!"
+ echo "---------------------------------------------------"
+ echo ""
+ echo "Restarting the Process"
+ echo
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+ echo serverid >/var/plexguide/type.choice && bash /opt/plexguide/menu/core/scripts/main.sh
+ exit
+fi
diff --git a/menu/interface/serverid/var.sh b/menu/interface/serverid/var.sh
new file mode 100644
index 00000000..562eab07
--- /dev/null
+++ b/menu/interface/serverid/var.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq - Sub7Seven
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+echo 2 >/var/plexguide/menu.number
+
+file="/var/plexguide/server.id"
+if [ ! -e "$file" ]; then
+ echo NOT-SET >/var/plexguide/server.id
+fi
diff --git a/menu/interface/settings.sh b/menu/interface/settings.sh
new file mode 100644
index 00000000..a24fa886
--- /dev/null
+++ b/menu/interface/settings.sh
@@ -0,0 +1,94 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+source /opt/plexguide/menu/functions/functions.sh
+source /opt/plexguide/menu/functions/install.sh
+# Menu Interface
+setstart() {
+
+ emdisplay=$(cat /var/plexguide/emergency.display)
+ switchcheck=$(cat /var/plexguide/pgui.switch)
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+🚀 PG Settings Interface Menu
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+[1] Download Path : Change the Processing Location
+[2] MultiHD : Add Multiple HDs and/or Mount Points to MergerFS
+[3] Processor : Enhance the CPU Processing Power
+[4] WatchTower : Auto-Update Application Manager
+[5] Change Time : Change the Server Time
+[6] PG UI : $switchcheck | Port 8555 | pgui.domain.com
+[7] Emergency Display: $emdisplay
+
+[Z] Exit
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+EOF
+
+ # Standby
+ read -p 'Type a Number | Press [ENTER]: ' typed /var/plexguide/pgui.switch
+ docker stop pgui
+ docker rm pgui
+ service localspace stop
+ rm -f /etc/systemd/system/localspace.servive
+ rm -f /etc/systemd/system/localspace.service
+ else
+ echo "On" >/var/plexguide/pgui.switch
+ bash /opt/plexguide/menu/pgcloner/solo/pgui.sh
+ ansible-playbook /opt/pgui/pgui.yml
+ service localspace start
+ fi
+ setstart
+ ;;
+ 7)
+ if [[ "$emdisplay" == "On" ]]; then
+ echo "Off" >/var/plexguide/emergency.display
+ else echo "On" >/var/plexguide/emergency.display; fi
+ setstart
+ ;;
+ z)
+ exit
+ ;;
+ Z)
+ exit
+ ;;
+ *)
+ setstart
+ ;;
+ esac
+
+}
+
+setstart
diff --git a/menu/interface/uninstall/choice.yml b/menu/interface/uninstall/choice.yml
new file mode 100644
index 00000000..d7d2d47c
--- /dev/null
+++ b/menu/interface/uninstall/choice.yml
@@ -0,0 +1,13 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq - Sub7Seven
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+---
+- name: 'Key Menu Facts'
+ set_fact:
+ head1: "\nPGBlitz UnInstaller Interface"
+ head2: "\nWARNING! Ensure to Backup any Data!"
+ info2: "\n2. I want to UnInstall PGBlitz!"
diff --git a/menu/interface/uninstall/file.sh b/menu/interface/uninstall/file.sh
new file mode 100644
index 00000000..570a86e0
--- /dev/null
+++ b/menu/interface/uninstall/file.sh
@@ -0,0 +1,110 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq - Sub7Seven
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+menu=$(cat /var/plexguide/final.choice)
+
+if [ "$menu" == "2" ]; then
+ #read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+
+ echo ""
+ echo "-----------------------------------------------------------"
+ echo "SYSTEM MESSAGE: WARNING! PGBlitz Uninstall Interface!"
+ echo "-----------------------------------------------------------"
+ echo ""
+ sleep 3
+
+ while true; do
+ read -p "Pay Attention! Do YOU WANT to Continue Uninstalling PG (y or n)!? " yn
+ case $yn in
+ [Yy]*)
+ echo ""
+ echo "Ok... we are just double checking!"
+ sleep 2
+ break
+ ;;
+ [Nn]*) echo "Ok! Exiting the Interface!" && echo "" && sleep 3 && exit ;;
+ *) echo "Please answer y or n (for yes or no)" ;;
+ esac
+ done
+
+ echo ""
+ echo "-----------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Uninstalling PG! May the Force Be With You!"
+ echo "-----------------------------------------------------------"
+ echo ""
+ sleep 3
+
+ echo "0" >/var/plexguide/pg.preinstall.stored
+ echo "0" >/var/plexguide/pg.ansible.stored
+ echo "0" >/var/plexguide/pg.rclone.stored
+ echo "0" >/var/plexguide/pg.python.stored
+ echo "0" >/var/plexguide/pg.docker.stored
+ echo "0" >/var/plexguide/pg.docstart.stored
+ echo "0" >/var/plexguide/pg.watchtower.stored
+ echo "0" >/var/plexguide/pg.label.stored
+ echo "0" >/var/plexguide/pg.alias.stored
+ echo "0" >/var/plexguide/pg.dep
+ rm -rf /var/plexguide/dep* 1>/dev/null 2>&1
+
+ echo ""
+ echo "-----------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Removing All PGBlitz Dependent Services"
+ echo "-----------------------------------------------------------"
+ echo ""
+ sleep 2
+ ansible-playbook /opt/plexguide/menu/interface/uninstall/remove-service.yml
+
+ echo ""
+ echo "-----------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Removing All PGBlitz File Directories"
+ echo "-----------------------------------------------------------"
+ echo ""
+ sleep 2
+ rm -rf /var/plexguide
+
+ echo ""
+ echo "-----------------------------------------------------------"
+ echo "SYSTEM MESSAGE: Uninstalling Docker & Generated Containers"
+ echo "-----------------------------------------------------------"
+ echo ""
+ sleep 2
+ rm -rf /etc/docker
+ apt-get purge docker-ce -y --allow-change-held-packages
+ rm -rf /var/lib/docker
+
+ while true; do
+ read -p "Pay Attention! Do you want to DELETE /opt/appdata (y or n)? " yn
+ case $yn in
+ [Yy]*)
+ echo ""
+ echo "Deleting Your Data Forever - Please Wait!"
+ rm -rf /opt/appdata
+ sleep 3
+ echo "I'm here, I'm there, wait...I'm your DATA! Poof! I'm gone!"
+ sleep 3
+ break
+ ;;
+ [Nn]*) echo "Data Will NOT be deleted!" && break ;;
+ *) echo "Please answer y or n (for yes or no)" ;;
+ esac
+ done
+
+ echo ""
+ echo "---------------------------------------------------"
+ echo "SYSTEM MESSAGE: Success! PG Uninstalled! Rebooting!"
+ echo "---------------------------------------------------"
+ echo ""
+ sleep 3
+ echo ""
+ echo "----------------------------------------------------"
+ echo "SYSTEM MESSAGE: PGBlitz Will Never Die! GoodBye!"
+ echo "----------------------------------------------------"
+ echo ""
+ sleep 2
+ reboot
+
+fi
diff --git a/menu/interface/uninstall/remove-service.yml b/menu/interface/uninstall/remove-service.yml
new file mode 100644
index 00000000..ce8f8ccc
--- /dev/null
+++ b/menu/interface/uninstall/remove-service.yml
@@ -0,0 +1,44 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq - Sub7Seven
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+---
+- hosts: localhost
+ gather_facts: false
+ vars:
+ service_vars:
+ - { name: crypt.service }
+ - { name: pgdrive.service }
+ - { name: gdrive.service }
+ - { name: gcrypt.service }
+ - { name: tdrive.service }
+ - { name: tcrypt.service }
+ - { name: supertransfer2.service }
+ - { name: unionfs.service }
+ - { name: pgmove.service }
+ - { name: pgunion.service }
+ - { name: move.service }
+ - { name: pgblitz.service }
+ - { name: plexdrive.service }
+ - { name: st2monitor.service }
+ tasks:
+ - name: Checking Existing Service Name
+ stat:
+ path: '/etc/systemd/system/{{ item.name }}'
+ with_items: '{{ service_vars }}'
+ register: check_service_name
+
+ - name: Stop If Service Is Running
+ systemd: state=stopped name={{ item.item.name }} daemon_reload=yes enabled=no
+ with_items: '{{ check_service_name.results }}'
+ when: item.stat.exists
+
+ - name: Remove Services
+ file:
+ path: '/etc/systemd/system/{{ item.item.name }}'
+ state: absent
+ with_items: '{{ check_service_name.results }}'
+ when: item.stat.exists
diff --git a/menu/interface/uninstall/unfiles.yml b/menu/interface/uninstall/unfiles.yml
new file mode 100644
index 00000000..0ed0eaee
--- /dev/null
+++ b/menu/interface/uninstall/unfiles.yml
@@ -0,0 +1,20 @@
+---
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: Removing File Directories
+ file:
+ state: absent
+ path: "{{ item }}"
+ with_items:
+ - /var/plexguide
+ - /opt/appdata/plexguide
+ - {{path.stdout}}/nzbget
+ - {{path.stdout}}/sab
+ - {{path.stdout}}/rutorrent
+ - {{path.stdout}}/move
+ - {{path.stdout}}/gcrypt
+ - {{path.stdout}}/deluge
+ - {{path.stdout}}/torrentvpn
+ - {{path.stdout}}/qbittorrent
+ ignore_errors: yes
diff --git a/menu/interface/uninstall/var.sh b/menu/interface/uninstall/var.sh
new file mode 100644
index 00000000..7c78d74f
--- /dev/null
+++ b/menu/interface/uninstall/var.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq - Sub7Seven
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+echo 2 >/var/plexguide/menu.number
diff --git a/menu/interface/version/choice.yml b/menu/interface/version/choice.yml
new file mode 100644
index 00000000..f8de2a69
--- /dev/null
+++ b/menu/interface/version/choice.yml
@@ -0,0 +1,25 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+---
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: Register Project
+ shell: 'cat /var/plexguide/pg.number'
+ register: serverid
+
+ - name: Installing Version master
+ git:
+ repo: 'https://github.com/MrDoobPG/PGBlitz.com'
+ dest: '/opt/plexguide'
+ version: master
+ force: yes
+
+ - name: 'Stops First Time Run'
+ shell: 'touch /var/plexguide/ask.yes'
+ register: program
\ No newline at end of file
diff --git a/menu/interface/version/choicedev.yml b/menu/interface/version/choicedev.yml
new file mode 100644
index 00000000..0e39f02e
--- /dev/null
+++ b/menu/interface/version/choicedev.yml
@@ -0,0 +1,25 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+---
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: Register Project
+ shell: 'cat /var/plexguide/pg.number'
+ register: serverid
+
+ - name: Installing Version master
+ git:
+ repo: 'https://github.com/MrDoobPG/PGBlitz.com'
+ dest: '/opt/plexguide'
+ version: dev
+ force: yes
+
+ - name: 'Stops First Time Run'
+ shell: 'touch /var/plexguide/ask.yes'
+ register: program
\ No newline at end of file
diff --git a/menu/interface/version/file.sh b/menu/interface/version/file.sh
new file mode 100644
index 00000000..a9bf65a4
--- /dev/null
+++ b/menu/interface/version/file.sh
@@ -0,0 +1,77 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+rm -rf /var/plexguide/ver.temp 1>/dev/null 2>&1
+touch /var/plexguide/ver.temp
+
+sleep 4
+## Builds Version List for Display
+while read p; do
+ echo $p >>/var/plexguide/ver.temp
+done /var/plexguide/pg.number
+ ansible-playbook /opt/plexguide/menu/interface/version/choice.yml
+
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+✅️ SYSTEM MESSAGE: Installed Verison - $storage - Standby!
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+
+ elif [ "$storage" == "dev" ]; then
+ break=yes
+ echo $storage >/var/plexguide/pg.number
+ ansible-playbook /opt/plexguide/menu/interface/version/choice-dev.yml
+
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+✅️ SYSTEM MESSAGE: Installed Verison - $storage - Standby!
+✅️ THATS an dev Branche
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+ sleep 4
+ else
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+⛔️ SYSTEM MESSAGE: Version $storage does not exist! - Standby!
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+ sleep 4
+ cat /var/plexguide/ver.temp
+ echo ""
+ fi
+
+done
diff --git a/menu/interface/vpnserver/choice.yml b/menu/interface/vpnserver/choice.yml
new file mode 100644
index 00000000..9a7a66e9
--- /dev/null
+++ b/menu/interface/vpnserver/choice.yml
@@ -0,0 +1,14 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+---
+- name: 'Key Menu Facts'
+ set_fact:
+ head1: "\nWelcome to the PG VPN Server Access Interface!"
+ head2: "\nCommand to View VPN Information: pgvpn"
+ info2: "\n2. VPNServer: Install"
+ info3: "\n3. VPNServer: UnInstall"
diff --git a/menu/interface/vpnserver/file.sh b/menu/interface/vpnserver/file.sh
new file mode 100644
index 00000000..f1c5b7e6
--- /dev/null
+++ b/menu/interface/vpnserver/file.sh
@@ -0,0 +1,42 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+menu=$(cat /var/plexguide/final.choice)
+
+if [ "$menu" == "2" ]; then
+ echo ""
+ echo "-----------------------------------------------------"
+ echo "SYSTEM MESSAGE: Installing - Please Standby!"
+ echo "-----------------------------------------------------"
+ echo ""
+ echo "NOTE: Install Time: 2 to 4 Minutes!"
+ sleep 2
+ echo ""
+ wget https://git.io/vpnsetup -O vpnsetup.sh 1>/dev/null 2>&1
+ sudo sh vpnsetup.sh >/opt/appdata/plexguide/vpninfo.raw
+ cat /opt/appdata/plexguide/vpninfo.raw | tail -n -12 | head -n +4 >/opt/appdata/plexguide/vpn.info
+ rm -rf /opt/appdata/plexguide/vpninfo.raw
+ echo
+ echo "-----------------------------------------------------"
+ echo "SYSTEM MESSAGE: Please Copy Your Information"
+ echo "-----------------------------------------------------"
+ echo ""
+ cat /opt/appdata/plexguide/vpn.info
+ echo ""
+ echo "Config Info: Visit http://pgvpn.pgblitz.com or WIKI"
+ echo "Note: pgvpn <<< command to recall your vpn info"
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+else
+ echo "" # leave if statement and continue.
+fi
+
+if [ "$menu" == "3" ]; then
+ echo "Uninstaller Not Ready!"
+ echo ""
+ read -n 1 -s -r -p "Press [ANY KEY] to Continue "
+fi
diff --git a/menu/interface/vpnserver/var.sh b/menu/interface/vpnserver/var.sh
new file mode 100644
index 00000000..1f7a49ea
--- /dev/null
+++ b/menu/interface/vpnserver/var.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+echo 3 >/var/plexguide/menu.number
diff --git a/menu/log/log.sh b/menu/log/log.sh
new file mode 100644
index 00000000..fc0be0df
--- /dev/null
+++ b/menu/log/log.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+dt=$(date '+%d/%m/%Y %H:%M:%S')
+log=$(cat /var/plexguide/logs/pg.log)
+echo "$dt $log" >>"/var/plexguide/logs/pg.log"
diff --git a/menu/motd/10-hostname-color b/menu/motd/10-hostname-color
new file mode 100644
index 00000000..a12023a5
--- /dev/null
+++ b/menu/motd/10-hostname-color
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+/usr/bin/env figlet "$(hostname)" | /usr/bin/env lolcat -f
diff --git a/menu/motd/20-sysinfo b/menu/motd/20-sysinfo
new file mode 100644
index 00000000..1c057e30
--- /dev/null
+++ b/menu/motd/20-sysinfo
@@ -0,0 +1,30 @@
+#!/bin/bash
+
+# get load averages
+IFS=" " read LOAD1 LOAD5 LOAD15 <<<$(cat /proc/loadavg | awk '{ print $1,$2,$3 }')
+# get free memory
+IFS=" " read USED FREE TOTAL <<<$(free -htm | grep "Mem" | awk {'print $3,$4,$2'})
+# get processes
+PROCESS=$(ps -eo user= | sort | uniq -c | awk '{ print $2 " " $1 }')
+PROCESS_ALL=$(echo "$PROCESS" | awk {'print $2'} | awk '{ SUM += $1} END { print SUM }')
+PROCESS_ROOT=$(echo "$PROCESS" | grep root | awk {'print $2'})
+PROCESS_USER=$(echo "$PROCESS" | grep -v root | awk {'print $2'} | awk '{ SUM += $1} END { print SUM }')
+# get processors
+PROCESSOR_NAME=$(grep "model name" /proc/cpuinfo | cut -d ' ' -f3- | awk {'print $0'} | head -1)
+PROCESSOR_COUNT=$(grep -ioP 'processor\t:' /proc/cpuinfo | wc -l)
+IP_ADDRESS=$(ip a | grep glo | awk '{print $2}' | head -1 | cut -f1 -d/)
+W="\e[0;39m"
+G="\e[1;32m"
+
+echo -e "
+${W}system:
+$W Distro.......: $W$(cat /etc/*release | grep "PRETTY_NAME" | cut -d "=" -f 2- | sed 's/"//g')
+$W Kernel.......: $W$(uname -sr)
+
+$W Uptime.......: $W$(uptime -p)
+$W Load.........: $G$LOAD1$W (1m), $G$LOAD5$W (5m), $G$LOAD15$W (15m)
+$W Processes....: $W$G$PROCESS_ROOT$W (root), $G$PROCESS_USER$W (user), $G$PROCESS_ALL$W (total)
+
+$W CPU..........: $W$PROCESSOR_NAME ($G$PROCESSOR_COUNT$W vCPU)
+$W Memory.......: $G$USED$W used, $G$FREE$W free, $G$TOTAL$W total$W
+$W Network......: $G$IP_ADDRESS$W"
diff --git a/menu/motd/30-diskinfo b/menu/motd/30-diskinfo
new file mode 100644
index 00000000..161a32a7
--- /dev/null
+++ b/menu/motd/30-diskinfo
@@ -0,0 +1,42 @@
+#!/bin/bash
+
+# config
+drives="sda sdb sdc"
+max_usage=90
+bar_width=50
+target_temp=22
+# colors
+white="\e[39m"
+green="\e[1;32m"
+red="\e[1;31m"
+dim="\e[2m"
+undim="\e[0m"
+
+# disk usage: ignore zfs, squashfs & tmpfs
+mapfile -t dfs < <(df -H -x fuse.rclone -x fuse.unionfs -x proc -x sys -x dev -x zfs -x squashfs -x tmpfs -x devtmpfs --output=target,pcent,size,avail,used | tail -n+2)
+printf "\nstorage:\n"
+
+for line in "${dfs[@]}"; do
+
+ if [[ $line =~ .*docker.* ]]; then
+ continue
+ fi
+
+ # get disk usage
+ usage=$(echo "$line" | awk '{print $2}' | sed 's/%//')
+ path=$(echo "$line" | awk '{print $1}' | sed 's/%//')
+ total=$(echo "$line" | awk '{print $3}' | sed 's/%//')
+ free=$(echo "$line" | awk '{print $4}' | sed 's/%//')
+ used=$(echo "$line" | awk '{print $5}' | sed 's/%//')
+ # color is green if usage < max_usage, else red
+ if [ "${usage}" -ge "${max_usage}" ]; then
+ color=$red
+ else
+ color=$green
+ fi
+
+ output+=$(echo " ${path} ${color}${used}${undim} used, ${color}${free}${undim} free, ${color}${total}${undim} total\n")
+
+done
+printf "${output}" |
+ awk '{printf " %.15s %s\n", $1 "............:", $2 " " $3 " " $4 " " $5 " " $6 " " $7}'
diff --git a/menu/motd/motd.yml b/menu/motd/motd.yml
new file mode 100644
index 00000000..a20258dd
--- /dev/null
+++ b/menu/motd/motd.yml
@@ -0,0 +1,43 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq - Sub7Seven
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+---
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: Disable existing MOTD
+ shell: chmod -x /etc/update-motd.d/*
+
+ - name: Enable update messages
+ shell: chmod +x /etc/update-motd.d/9*
+
+ - name: Deploy Dynamic MOTD
+ file:
+ path: /etc/update-motd.d
+ state: directory
+ mode: 0775
+
+ - name: Import MOTD Files
+ copy: 'src={{item}} dest=/etc/update-motd.d/{{item}} force=yes mode=0775'
+ with_items:
+ - 10-hostname-color
+ - 20-sysinfo
+ - 30-diskinfo
+
+ - name: Install lolcat pip module
+ pip:
+ name: lolcat
+ state: latest
+ ignore_errors: yes
+
+ - cron:
+ name: 'Update MOTD'
+ user: 'root'
+ minute: '10'
+ job: 'update-motd'
+ state: absent
+ become_user: root
diff --git a/menu/network/network.sh b/menu/network/network.sh
new file mode 100644
index 00000000..5023a6d9
--- /dev/null
+++ b/menu/network/network.sh
@@ -0,0 +1,59 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+source /opt/plexguide/menu/functions/functions.sh
+# Menu Interface
+question1() {
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+📂 PG System & Network Auditor
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+[1] System & Network Benchmark - Basic
+[2] System & Network Benchmark - Advanced
+[3] Simple SpeedTest
+
+[Z] Exit
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+EOF
+
+ # Standby
+ read -p 'Type a Number | Press [ENTER]: ' typed &1 >>/dev/null
+export DEBIAN_FRONTEND=noninteractive
+echo "Upgrading packages"
+apt-get upgrade -yqq 2>&1 >>/dev/null
+export DEBIAN_FRONTEND=noninteractive
+echo "Dist-Upgrading packages"
+apt-get dist-upgrade -yqq 2>&1 >>/dev/null
+export DEBIAN_FRONTEND=noninteractive
+echo "Autoremove old Updates"
+apt-get autoremove -yqq 2>&1 >>/dev/null
+export DEBIAN_FRONTEND=noninteractive
+echo "install complete"
+
+tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+🚀 PG System Tweaker
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+💬 PG System Tweaker
+
+[1] Network Tweaker ( Debian 9 & Ubuntu 18 only )
+[2] Docker Swapness
+[3] PGBlitz logrotator
+[4] VnStat autoinstaller
+
+[Z] Exit
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+
+# Standby
+read -p 'Type a Number | Press [ENTER]: ' typed &1 >>/dev/null
+ export DEBIAN_FRONTEND=noninteractive
+ echo "networktools installed"
+ sleep 2
+ network=$(ifconfig | grep -E 'eno1|enp|ens5' | awk '{print $1}' | sed -e 's/://g')
+ sleep 2
+ echo $network "network detected"
+ ethtool -K $network tso off tx off
+ sed -i '$a\' /etc/crontab
+ sed -i '$a\#################################' /etc/crontab
+ sed -i '$a\## PG Network tweak ' /etc/crontab
+ sed -i '$a\#################################' /etc/crontab
+ sed -i '$a\@reboot ethtool -K '$network' tso off tx off\' /etc/crontab
+ sleep 2
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo ""
+ echo " ✅ PASSED ! Network Tweak done"
+ echo " ✅ PASSED ! crontab line added"
+ echo ""
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" && sleep 10
+
+elif [ "$typed" == "2" ]; then
+ sudo sysctl vm.swappiness=0
+ sudo sysctl vm.overcommit_memory=1
+ sed -i '$a\' /etc/sysctl.conf
+ sed -i '$a\' /etc/sysctl.conf
+ sed -i '$a\#########################################' /etc/sysctl.conf
+ sed -i '$a\## Docker PG Swapness changes ' /etc/sysctl.conf
+ sed -i '$a\#########################################' /etc/sysctl.conf
+ sed -i '$a\vm.swappiness=0\' /etc/sysctl.conf
+ sed -i '$a\vm.overcommit_memory=1\' /etc/sysctl.conf
+ sleep 2
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo ""
+ echo " ✅ PASSED ! Docker swappiness offline"
+ echo " ✅ PASSED ! systctl edit"
+ echo ""
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" && sleep 10
+
+elif [ "$typed" == "3" ]; then
+ username=$(grep "1000" /etc/passwd | cut -d: -f1 | awk '{print $1}')
+
+ sed -i '/#compress/s/^#*//g' /etc/logrotate.conf
+ sed -i 's/weekly/daily/g' /etc/logrotate.conf
+ sed -i 's/rotate 4/rotate 1/g' /etc/logrotate.conf
+
+ sleep 2
+
+ sed -i '$a\ ' /etc/logrotate.conf
+ sed -i '$a\########################################' /etc/logrotate.conf
+ sed -i '$a\## PGBlitz Upload logrotate ' /etc/logrotate.conf
+ sed -i '$a\########################################' /etc/logrotate.conf
+ sed -i '$a\ ' /etc/logrotate.conf
+ sed -i '$a\/var/plexguide/logs/*.log {' /etc/logrotate.conf
+ sed -i '$a\ su '$username' '$username' ' /etc/logrotate.conf
+ sed -i '$a\ rotate 7' /etc/logrotate.conf
+ sed -i '$a\ daily' /etc/logrotate.conf
+ sed -i '$a\ compress' /etc/logrotate.conf
+ sed -i '$a\ missingok' /etc/logrotate.conf
+ sed -i '$a\ notifempty' /etc/logrotate.conf
+ sed -i '$a\ maxage 7' /etc/logrotate.conf
+ sed -i '$a\ create 755 '$username' '$username'' /etc/logrotate.conf
+ sed -i '$a\}' /etc/logrotate.conf
+ sed -i '$a\ ' /etc/logrotate.conf
+ sed -i '$a\######################################' /etc/logrotate.conf
+ sed -i '$a\## PGBlitz Upload logrotate ' /etc/logrotate.conf
+ sed -i '$a\######################################' /etc/logrotate.conf
+ sleep 2
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo ""
+ echo " ✅ PASSED ! PGBlitz logrotate installed"
+ echo " ✅ PASSED ! Daily backup from the logs"
+ echo " ✅ PASSED ! max age 7 Days "
+ echo " ✅ PASSED ! auto delete older logs"
+ echo ""
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" && sleep 10
+
+elif [ "$typed" == "4" ]; then
+ echo "networktools | vnstat | vnstati install | please wait"
+ apt-get install ethtool vnstat vnstati -yqq 2>&1 >>/dev/null
+ export DEBIAN_FRONTEND=noninteractive
+ echo "networktools | vnstat | vnstati installed"
+ sleep 2
+ network=$(ifconfig | grep -E 'eno1|enp|ens5' | awk '{print $1}' | sed -e 's/://g')
+ sleep 2
+ echo $network "network detected"
+ sed -i 's/eth0/'$network'/g' /etc/vnstat.conf
+ sed -i 's/UnitMode 0/UnitMode 1/g' /etc/vnstat.conf
+ sed -i 's/RateUnit 1/RateUnit 0/g' /etc/vnstat.conf
+ sed -i 's/Locale "-"/Locale "LC_ALL=en_US.UTF-8"/g' /etc/vnstat.conf
+ sleep 2
+ /etc/init.d/vnstat restart 2>&1 >>/dev/null
+ sleep 2
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo ""
+ echo " ✅ PASSED ! vnstat installed"
+ echo " ✅ PASSED ! vnstat -l [ live traffic ]"
+ echo " ✅ PASSED ! vnstat -d [ daily traffic ]"
+ echo " ✅ PASSED ! vnstat -w [ weekly traffic ]"
+ echo " ✅ PASSED ! vnstat -m [ month traffic ]"
+ echo ""
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" && sleep 10
+
+elif [ "$typed" == "Z" ] || [ "$typed" == "z" ]; then
+ exit
+else
+ bash /opt/plexguide/menu/tools/tools.sh
+ exit
+fi
+bash /opt/plexguide/menu/tools/tools.sh
+exit
diff --git a/menu/pg.yml b/menu/pg.yml
new file mode 100644
index 00000000..ea818778
--- /dev/null
+++ b/menu/pg.yml
@@ -0,0 +1,12 @@
+---
+- hosts: localhost
+ vars:
+ extra: ''
+
+ roles:
+ - { role: docker, tags: ['docker'] }
+ - { role: docstart, tags: ['docstart'] }
+ - { role: dockerdeb, tags: ['dockerdeb'] }
+ - { role: autodelete, tags: ['autodelete'] }
+ - { role: clean, tags: ['clean'] }
+ - { role: clean-encrypt, tags: ['clean-encrypt'] }
diff --git a/menu/pgbox/cname.sh b/menu/pgbox/cname.sh
new file mode 100644
index 00000000..982a07d5
--- /dev/null
+++ b/menu/pgbox/cname.sh
@@ -0,0 +1,101 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): LooseSeal2
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+source /opt/plexguide/menu/functions/functions.sh
+
+# vars
+program=$(cat /tmp/program_var)
+domain=$(cat "/var/plexguide/server.domain")
+
+variable /var/plexguide/"$program".cname "$program"
+
+variable /var/plexguide/"$program".port ""
+
+# FIRST QUESTION
+question1() {
+ cname=$(cat "/var/plexguide/$program.cname")
+ port=$(cat "/var/plexguide/$program.port")
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+⌛ $program - Set subdomains & ports
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+⚡ Reference: http://$program.pgblitz.com
+
+EOF
+ if [[ $port != "" ]]; then
+ tee <<-EOF
+External Url: https://$cname.$domain:$port
+EOF
+ else
+ tee <<-EOF
+External Url: https://$cname.$domain
+EOF
+ fi
+
+ tee <<-EOF
+
+[1] Change subdomain
+[2] Change external port
+
+EOF
+
+ if [[ $port != "" ]]; then
+ tee <<-EOF
+[A] Use https://$cname.$domain:$port
+EOF
+ else
+ tee <<-EOF
+[A] Use https://$cname.$domain
+EOF
+ fi
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+EOF
+
+ read -p '↘️ Type Number | Press [ENTER]: ' typed "/var/plexguide/$program.cname"
+ question1
+ fi
+ fi
+ elif [ "$typed" == "2" ]; then
+ read -p "🌍 Type port 1025-65535 to use for $program | blank for default | Press [ENTER]: " typed "/var/plexguide/$program.port"
+ else
+ if ! [[ "$typed" =~ ^[0-9]+$ && "$typed" -ge 1025 && "$typed" -le 65535 ]]; then
+ badinput1
+ else
+ echo "$typed" >"/var/plexguide/$program.port"
+ fi
+ fi
+ question1
+ else badinput1; fi
+}
+
+question1
+
+manualuser() {
+ while read p; do
+ echo "$p" >"/var/plexguide/$program.cname"
+ done ")
+ if [ "$croncheck" == "0" ]; then bash /opt/plexguide/menu/cron/cron.sh; fi
+}
+
+cronmass() {
+ croncheck=$(cat /opt/communityapps/apps/_cron.list | grep -c "\<$p\>")
+ if [ "$croncheck" == "0" ]; then bash /opt/plexguide/menu/cron/cron.sh; fi
+}
+
+initial() {
+ rm -rf /var/plexguide/pgbox.output 1>/dev/null 2>&1
+ rm -rf /var/plexguide/pgbox.buildup 1>/dev/null 2>&1
+ rm -rf /var/plexguide/program.temp 1>/dev/null 2>&1
+ rm -rf /var/plexguide/app.list 1>/dev/null 2>&1
+ touch /var/plexguide/pgbox.output
+ touch /var/plexguide/program.temp
+ touch /var/plexguide/app.list
+ touch /var/plexguide/pgbox.buildup
+
+ mkdir -p /opt/communityapps
+
+ if [ "$boxversion" == "official" ]; then
+ ansible-playbook /opt/plexguide/menu/pgbox/pgboxcommunity.yml
+ else ansible-playbook /opt/plexguide/menu/pgbox/pgbox_communitypersonal.yml; fi
+
+ echo ""
+ echo "💬 Pulling Update Files - Please Wait"
+ file="/opt/communityapps/place.holder"
+ waitvar=0
+ while [ "$waitvar" == "0" ]; do
+ sleep .5
+ if [ -e "$file" ]; then waitvar=1; fi
+ done
+ customcontainers
+}
+
+question1() {
+
+ ### Remove Running Apps
+ while read p; do
+ sed -i "/^$p\b/Id" /var/plexguide/app.list
+ done /var/plexguide/app.list
+ while read p; do
+ echo "" >>/opt/communityapps/apps/$p.yml
+ echo "##PG-Community" >>/opt/communityapps/apps/$p.yml
+
+ mkdir -p /opt/mycontainers
+ touch /opt/appdata/plexguide/rclone.conf
+ done /var/plexguide/pgbox.running
+
+ ### Remove Official Apps
+ while read p; do
+ # reminder, need one for custom apps
+ baseline=$(cat /opt/communityapps/apps/$p.yml | grep "##PG-Community")
+ if [ "$baseline" == "" ]; then sed -i -e "/$p/d" /var/plexguide/app.list; fi
+ done >/var/plexguide/program.temp
+ echo -n " " >>/var/plexguide/program.temp
+ num=$((num + 1))
+ if [[ "$num" == "7" ]]; then
+ num=0
+ echo " " >>/var/plexguide/program.temp
+ fi
+ done ")
+ if [ "$current" != "" ]; then queued && question1; fi
+
+ current=$(cat /var/plexguide/pgbox.running | grep "\<$typed\>")
+ if [ "$current" != "" ]; then exists && question1; fi
+
+ current=$(cat /var/plexguide/program.temp | grep "\<$typed\>")
+ if [ "$current" == "" ]; then badinput1 && question1; fi
+
+ part1
+}
+
+part1() {
+ echo "$typed" >>/var/plexguide/pgbox.buildup
+ num=0
+
+ touch /var/plexguide/pgbox.output && rm -rf /var/plexguide/pgbox.output
+
+ while read p; do
+ echo -n $p >>/var/plexguide/pgbox.output
+ echo -n " " >>/var/plexguide/pgbox.output
+ if [[ "$num" == 7 ]]; then
+ num=0
+ echo " " >>/var/plexguide/pgbox.output
+ fi
+ done /tmp/program_var
+
+ bash /opt/communityapps/apps/image/_image.sh
+ done /var/plexguide/cron.count
+ if [ "$croncount" -ge 2 ]; then bash /opt/plexguide/menu/cron/mass.sh; fi
+ fi
+
+ # CName & Port Execution
+ bash /opt/plexguide/menu/pgbox/cname.sh
+
+ while read p; do
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+$p - Now Installing!
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+EOF
+
+ sleep 1
+
+ if [ "$p" == "plex" ]; then
+ bash /opt/plexguide/menu/plex/plex.sh
+ elif [ "$p" == "nzbthrottle" ]; then nzbt; fi
+
+ # Store Used Program
+ echo "$p" >/tmp/program_var
+
+ # Execute Main Program
+ ansible-playbook /opt/communityapps/apps/$p.yml
+
+ if [[ "$edition" == "PG Edition - HD Solo" ]]; then
+ a=b
+ elif [ "$croncount" -eq "1" ]; then cronexe; fi
+
+ # End Banner
+ bash /opt/plexguide/menu/pgbox/endbanner.sh >>/tmp/output.info
+
+ sleep 2
+ done >/tmp/output.info
+ cat /tmp/output.info
+ final
+}
+
+pinterface() {
+
+ boxuser=$(cat /var/plexguide/boxcommunity.user)
+ boxbranch=$(cat /var/plexguide/boxcommunity.branch)
+
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+🚀 PG Community Box Edition! 📓 Reference: community.pgblitz.com
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+💬 User: $boxuser | Branch: $boxbranch
+
+[1] Change User Name & Branch
+[2] Deploy Community Box - Personal (Forked)
+
+[Z] Exit
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+EOF
+
+ read -p 'Type a Selection | Press [ENTER]: ' typed /var/plexguide/boxcommunity.user
+ echo "$boxbranch" >/var/plexguide/boxcommunity.branch
+ pinterface
+ ;;
+ 2)
+ existcheck=$(git ls-remote --exit-code -h "https://github.com/$boxuser/Apps-Community" | grep "$boxbranch")
+ if [ "$existcheck" == "" ]; then
+ echo
+ read -p '💬 Exiting! Forked Version Does Not Exist! | Press [ENTER]: ' typed /tmp/output.info
+mainbanner
diff --git a/menu/pgbox/pgboxcommunity.yml b/menu/pgbox/pgboxcommunity.yml
new file mode 100644
index 00000000..6ecad41f
--- /dev/null
+++ b/menu/pgbox/pgboxcommunity.yml
@@ -0,0 +1,19 @@
+---
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: Check if Image Variable Exists
+ stat:
+ path: '/opt/communityapps'
+ register: pathcheck
+
+ - name: 'Transfer Image Variable'
+ shell: 'rm -rf /opt/communityapps'
+ when: pathcheck.stat.exists
+
+ - name: Cloning Community Apps
+ git:
+ repo: 'https://github.com/PGBlitz/Apps-Community'
+ dest: /opt/communityapps
+ version: 'v8.6'
+ force: yes
diff --git a/menu/pgbox/pgboxcore.sh b/menu/pgbox/pgboxcore.sh
new file mode 100644
index 00000000..f6ba25d6
--- /dev/null
+++ b/menu/pgbox/pgboxcore.sh
@@ -0,0 +1,369 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+
+# FUNCTIONS START ##############################################################
+source /opt/plexguide/menu/functions/functions.sh
+
+queued() {
+ echo
+ read -p "⛔️ ERROR - $typed Already Queued! | Press [ENTER] " typed ")
+ if [ "$croncheck" == "0" ]; then bash /opt/plexguide/menu/cron/cron.sh; fi
+}
+
+cronmass() {
+ croncheck=$(cat /opt/coreapps/apps/_cron.list | grep -c "\<$p\>")
+ if [ "$croncheck" == "0" ]; then bash /opt/plexguide/menu/cron/cron.sh; fi
+}
+
+initial() {
+ rm -rf /var/plexguide/pgbox.output 1>/dev/null 2>&1
+ rm -rf /var/plexguide/pgbox.buildup 1>/dev/null 2>&1
+ rm -rf /var/plexguide/program.temp 1>/dev/null 2>&1
+ rm -rf /var/plexguide/app.list 1>/dev/null 2>&1
+ touch /var/plexguide/pgbox.output
+ touch /var/plexguide/program.temp
+ touch /var/plexguide/app.list
+ touch /var/plexguide/pgbox.buildup
+
+ mkdir -p /opt/coreapps
+
+ if [ "$boxversion" == "official" ]; then
+ ansible-playbook /opt/plexguide/menu/pgbox/pgboxcore.yml
+ else ansible-playbook /opt/plexguide/menu/pgbox/pgbox_corepersonal.yml; fi
+
+ echo ""
+ echo "💬 Pulling Update Files - Please Wait"
+ file="/opt/coreapps/place.holder"
+ waitvar=0
+ while [ "$waitvar" == "0" ]; do
+ sleep .5
+ if [ -e "$file" ]; then waitvar=1; fi
+ done
+
+}
+
+question1() {
+
+ ### Remove Running Apps
+ while read p; do
+ sed -i "/^$p\b/Id" /var/plexguide/app.list
+ done /var/plexguide/app.list
+ while read p; do
+ echo "" >>/opt/coreapps/apps/$p.yml
+ echo "##PG-Core" >>/opt/coreapps/apps/$p.yml
+
+ mkdir -p /opt/mycontainers
+ touch /opt/appdata/plexguide/rclone.conf
+ done /var/plexguide/pgbox.running
+
+ ### Remove Official Apps
+ while read p; do
+ # reminder, need one for custom apps
+ baseline=$(cat /opt/coreapps/apps/$p.yml | grep "##PG-Core")
+ if [ "$baseline" == "" ]; then sed -i -e "/$p/d" /var/plexguide/app.list; fi
+ done >/var/plexguide/program.temp
+ echo -n " " >>/var/plexguide/program.temp
+ num=$((num + 1))
+ if [[ "$num" == "7" ]]; then
+ num=0
+ echo " " >>/var/plexguide/program.temp
+ fi
+ done ")
+ if [ "$current" != "" ]; then queued && question1; fi
+
+ current=$(cat /var/plexguide/pgbox.running | grep "\<$typed\>")
+ if [ "$current" != "" ]; then exists && question1; fi
+
+ current=$(cat /var/plexguide/program.temp | grep "\<$typed\>")
+ if [ "$current" == "" ]; then badinput1 && question1; fi
+
+ part1
+}
+
+part1() {
+ echo "$typed" >>/var/plexguide/pgbox.buildup
+ num=0
+
+ touch /var/plexguide/pgbox.output && rm -rf /var/plexguide/pgbox.output
+
+ while read p; do
+ echo -n $p >>/var/plexguide/pgbox.output
+ echo -n " " >>/var/plexguide/pgbox.output
+ if [[ "$num" == 7 ]]; then
+ num=0
+ echo " " >>/var/plexguide/pgbox.output
+ fi
+ done /tmp/program_var
+
+ bash /opt/coreapps/apps/image/_image.sh
+
+ # CName & Port Execution
+ bash /opt/plexguide/menu/pgbox/cname.sh
+ done /var/plexguide/cron.count
+ if [ "$croncount" -ge 2 ]; then bash /opt/plexguide/menu/cron/mass.sh; fi
+ fi
+
+ while read p; do
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+$p - Now Installing!
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+EOF
+
+ sleep 1
+
+ if [ "$p" == "plex" ]; then
+ bash /opt/plexguide/menu/plex/plex.sh
+ elif [ "$p" == "nzbthrottle" ]; then nzbt; fi
+
+ # Store Used Program
+ echo "$p" >/tmp/program_var
+ # Execute Main Program
+ ansible-playbook /opt/coreapps/apps/$p.yml
+
+ if [[ "$edition" == "PG Edition - HD Solo" ]]; then
+ a=b
+ elif [ "$croncount" -eq "1" ]; then cronexe; fi
+
+ # End Banner
+ bash /opt/plexguide/menu/pgbox/endbanner.sh >>/tmp/output.info
+
+ sleep 2
+ done >/tmp/output.info
+ cat /tmp/output.info
+ final
+}
+
+pinterface() {
+
+ boxuser=$(cat /var/plexguide/boxcore.user)
+ boxrepo=$(cat /var/plexguide/boxrepo.repo)
+ boxbranch=$(cat /var/plexguide/boxcore.branch)
+
+
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+🚀 PG Core Box Edition!
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+💬
+User: $boxuser
+Repo: $boxrepo
+Branch: $boxbranch
+
+[1] Change User Name & Branch
+[2] Deploy Core Box - Personal (Forked)
+
+[Z] Exit
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+EOF
+
+ read -p 'Type a Selection | Press [ENTER]: ' typed /var/plexguide/boxcore.user
+ echo "$boxrepo" >/var/plexguide/boxrepo.repo
+ echo "$boxbranch" >/var/plexguide/boxcore.branch
+ pinterface
+ ;;
+ 2)
+ existcheck=$(git ls-remote --exit-code -h "https://github.com/$boxuser/$boxrepo" | grep "$boxbranch")
+ if [ "$existcheck" == "" ]; then
+ echo
+ read -p '💬 Exiting! Forked Version Does Not Exist! | Press [ENTER]: ' typed /tmp/output.info
+mainbanner
diff --git a/menu/pgbox/pgboxcore.yml b/menu/pgbox/pgboxcore.yml
new file mode 100644
index 00000000..b8ba3853
--- /dev/null
+++ b/menu/pgbox/pgboxcore.yml
@@ -0,0 +1,19 @@
+---
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: Check if Image Variable Exists
+ stat:
+ path: '/opt/coreapps'
+ register: pathcheck
+
+ - name: 'Transfer Image Variable'
+ shell: 'rm -rf /opt/coreapps'
+ when: pathcheck.stat.exists
+
+ - name: Cloning Core Apps
+ git:
+ repo: 'https://github.com/MrDoobPG/Apps-Core'
+ dest: /opt/coreapps
+ version: 'dev'
+ force: yes
diff --git a/menu/pgbox/pgboxselect.sh b/menu/pgbox/pgboxselect.sh
new file mode 100644
index 00000000..de8a36b9
--- /dev/null
+++ b/menu/pgbox/pgboxselect.sh
@@ -0,0 +1,42 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+
+mainstart() {
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+🚀 Box Apps Interface Selection
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+💬 PG Box installs a series of Core and Community applications!
+
+[1] PTS : Core
+[2] PGBlitzs Box: Community
+[3] PG Box: Removal
+[Z] Exit
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+
+ # Standby
+ read -p 'Type a Number | Press [ENTER]: ' typed /var/plexguide/pgcloner.rolename
+echo 'BlitzGCE' >/var/plexguide/pgcloner.roleproper
+echo 'BlitzGCE' >/var/plexguide/pgcloner.projectname
+echo 'v8.6' >/var/plexguide/pgcloner.projectversion
+echo 'blitzgce.sh' >/var/plexguide/pgcloner.startlink
+
+#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+echo "💬 Blitz GCE scripts are setup so that users can deploy any
+Google Cloud Edition container to act as as feeder between two to
+three months!" >/var/plexguide/pgcloner.info
+#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+### START PROCESS
+bash /opt/plexguide/menu/pgcloner/corev2/main.sh
diff --git a/menu/pgcloner/core/main.sh b/menu/pgcloner/core/main.sh
new file mode 100644
index 00000000..41241c20
--- /dev/null
+++ b/menu/pgcloner/core/main.sh
@@ -0,0 +1,150 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+
+# FUNCTIONS START ##############################################################
+source /opt/plexguide/menu/functions/functions.sh
+
+rolename=$(cat /var/plexguide/pgcloner.rolename)
+roleproper=$(cat /var/plexguide/pgcloner.roleproper)
+projectname=$(cat /var/plexguide/pgcloner.projectname)
+projectversion=$(cat /var/plexguide/pgcloner.projectversion)
+
+mkdir -p "/opt/$rolename"
+
+initial() {
+ ansible-playbook "/opt/plexguide/menu/pgcloner/core/primary.yml"
+ echo ""
+ echo "💬 Pulling Update Files - Please Wait"
+ file="/opt/$rolename/place.holder"
+ waitvar=0
+ while [ "$waitvar" == "0" ]; do
+ sleep .5
+ if [ -e "$file" ]; then waitvar=1; fi
+ done
+}
+
+custom() {
+ mkdir -p "/opt/$rolename"
+ ansible-playbook "/opt/plexguide/menu/pgcloner/core/personal.yml"
+
+ echo ""
+ echo "💬 Pulling Update Files - Please Wait"
+ file="/opt/$rolename/place.holder"
+ waitvar=0
+ while [ "$waitvar" == "0" ]; do
+ sleep .5
+ if [ -e "$file" ]; then waitvar=1; fi
+ done
+}
+
+mainbanner() {
+ clonerinfo=$(cat /var/plexguide/pgcloner.info)
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+🚀 $roleproper
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+$clonerinfo
+
+[1] Utilize $roleproper - PGBlitz's
+[2] Utilize $roleproper - Personal (Forked)
+
+[Z] Exit
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+
+ read -p 'Type a Selection | Press [ENTER]: ' typed /var/plexguide/$rolename.user
+ echo "$branch" >/var/plexguide/$rolename.branch
+ pinterface
+ ;;
+ 2)
+ existcheck=$(git ls-remote --exit-code -h "https://github.com/$user/$projectname" | grep "$branch")
+ if [ "$existcheck" == "" ]; then
+ echo
+ read -p '💬 Exiting! Forked Version Does Not Exist! | Press [ENTER]: ' typed /tmp/output.info
+mainbanner
diff --git a/menu/pgcloner/core/personal.yml b/menu/pgcloner/core/personal.yml
new file mode 100644
index 00000000..d447c4cc
--- /dev/null
+++ b/menu/pgcloner/core/personal.yml
@@ -0,0 +1,26 @@
+---
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: Register Role
+ shell: 'cat /var/plexguide/pgcloner.projectname'
+ register: pname
+
+ - name: Register Role
+ shell: 'cat /var/plexguide/pgcloner.rolename'
+ register: prole
+
+ - name: Register User - Personal
+ shell: 'cat /var/plexguide/{{prole.stdout}}.user'
+ register: user
+
+ - name: Register Branch - Personal
+ shell: 'cat /var/plexguide/{{prole.stdout}}.branch'
+ register: branch
+
+ - name: 'Cloning Personal Forked Role'
+ git:
+ repo: 'https://github.com/{{user.stdout}}/{{pname.stdout}}'
+ dest: '/opt/{{prole.stdout}}'
+ version: '{{branch.stdout}}'
+ force: yes
diff --git a/menu/pgcloner/core/primary.yml b/menu/pgcloner/core/primary.yml
new file mode 100644
index 00000000..e8d5e3d9
--- /dev/null
+++ b/menu/pgcloner/core/primary.yml
@@ -0,0 +1,31 @@
+---
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: Register Project Name
+ shell: 'cat /var/plexguide/pgcloner.projectname'
+ register: pname
+
+ - name: Register Role
+ shell: 'cat /var/plexguide/pgcloner.rolename'
+ register: prole
+
+ - name: Register Project Version
+ shell: 'cat /var/plexguide/pgcloner.projectversion'
+ register: pversion
+
+ - name: Check if Path Exists
+ stat:
+ path: '/opt/{{prole.stdout}}'
+ register: pathcheck
+
+ - name: 'Transfer Image Variable'
+ shell: 'rm -rf /opt/{{prole.stdout}}'
+ when: pathcheck.stat.exists
+
+ - name: Clone Role
+ git:
+ repo: 'https://github.com/MrDoobPG/{{pname.stdout}}'
+ dest: '/opt/{{prole.stdout}}'
+ version: '{{pversion.stdout}}'
+ force: yes
diff --git a/menu/pgcloner/corev2/main.sh b/menu/pgcloner/corev2/main.sh
new file mode 100644
index 00000000..c9e16dae
--- /dev/null
+++ b/menu/pgcloner/corev2/main.sh
@@ -0,0 +1,158 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+
+# FUNCTIONS START ##############################################################
+source /opt/plexguide/menu/functions/functions.sh
+
+rolename=$(cat /var/plexguide/pgcloner.rolename)
+roleproper=$(cat /var/plexguide/pgcloner.roleproper)
+projectname=$(cat /var/plexguide/pgcloner.projectname)
+projectversion=$(cat /var/plexguide/pgcloner.projectversion)
+startlink=$(cat /var/plexguide/pgcloner.startlink)
+
+mkdir -p "/opt/$rolename"
+
+initial() {
+ ansible-playbook "/opt/plexguide/menu/pgcloner/corev2/primary.yml"
+ echo ""
+ echo "💬 Pulling Update Files - Please Wait"
+ file="/opt/$rolename/place.holder"
+ waitvar=0
+ while [ "$waitvar" == "0" ]; do
+ sleep .5
+ if [ -e "$file" ]; then waitvar=1; fi
+ done
+ bash /opt/${rolename}/${startlink}
+}
+
+custom() {
+ mkdir -p "/opt/$rolename"
+ ansible-playbook "/opt/plexguide/menu/pgcloner/corev2/personal.yml"
+
+ echo ""
+ echo "💬 Pulling Update Files - Please Wait"
+ file="/opt/$rolename/place.holder"
+ waitvar=0
+ while [ "$waitvar" == "0" ]; do
+ sleep .5
+ if [ -e "$file" ]; then waitvar=1; fi
+ done
+ bash /opt/${rolename}/${startlink}
+}
+
+mainbanner() {
+ clonerinfo=$(cat /var/plexguide/pgcloner.info)
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+🚀 $roleproper
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+$clonerinfo
+
+[1] Utilize $roleproper - PGBlitz's
+[2] Utilize $roleproper - Personal (Forked)
+
+[Z] Exit
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+EOF
+
+ read -p 'Type a Selection | Press [ENTER]: ' typed /var/plexguide/$rolename.user
+ echo "$branch" >/var/plexguide/$rolename.branch
+ pinterface
+ ;;
+ 2)
+ existcheck=$(git ls-remote --exit-code -h "https://github.com/$user/$projectname" | grep "$branch")
+ if [ "$existcheck" == "" ]; then
+ echo
+ read -p '💬 Exiting! Forked Version Does Not Exist! | Press [ENTER]: ' typed /tmp/output.info
+mainbanner
diff --git a/menu/pgcloner/corev2/personal.yml b/menu/pgcloner/corev2/personal.yml
new file mode 100644
index 00000000..d447c4cc
--- /dev/null
+++ b/menu/pgcloner/corev2/personal.yml
@@ -0,0 +1,26 @@
+---
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: Register Role
+ shell: 'cat /var/plexguide/pgcloner.projectname'
+ register: pname
+
+ - name: Register Role
+ shell: 'cat /var/plexguide/pgcloner.rolename'
+ register: prole
+
+ - name: Register User - Personal
+ shell: 'cat /var/plexguide/{{prole.stdout}}.user'
+ register: user
+
+ - name: Register Branch - Personal
+ shell: 'cat /var/plexguide/{{prole.stdout}}.branch'
+ register: branch
+
+ - name: 'Cloning Personal Forked Role'
+ git:
+ repo: 'https://github.com/{{user.stdout}}/{{pname.stdout}}'
+ dest: '/opt/{{prole.stdout}}'
+ version: '{{branch.stdout}}'
+ force: yes
diff --git a/menu/pgcloner/corev2/primary.yml b/menu/pgcloner/corev2/primary.yml
new file mode 100644
index 00000000..e8d5e3d9
--- /dev/null
+++ b/menu/pgcloner/corev2/primary.yml
@@ -0,0 +1,31 @@
+---
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: Register Project Name
+ shell: 'cat /var/plexguide/pgcloner.projectname'
+ register: pname
+
+ - name: Register Role
+ shell: 'cat /var/plexguide/pgcloner.rolename'
+ register: prole
+
+ - name: Register Project Version
+ shell: 'cat /var/plexguide/pgcloner.projectversion'
+ register: pversion
+
+ - name: Check if Path Exists
+ stat:
+ path: '/opt/{{prole.stdout}}'
+ register: pathcheck
+
+ - name: 'Transfer Image Variable'
+ shell: 'rm -rf /opt/{{prole.stdout}}'
+ when: pathcheck.stat.exists
+
+ - name: Clone Role
+ git:
+ repo: 'https://github.com/MrDoobPG/{{pname.stdout}}'
+ dest: '/opt/{{prole.stdout}}'
+ version: '{{pversion.stdout}}'
+ force: yes
diff --git a/menu/pgcloner/hetzner.sh b/menu/pgcloner/hetzner.sh
new file mode 100644
index 00000000..bae78c95
--- /dev/null
+++ b/menu/pgcloner/hetzner.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+
+### FILL OUT THIS AREA ###
+echo 'hetzner' >/var/plexguide/pgcloner.rolename
+echo 'HCloud (Hetzner)' >/var/plexguide/pgcloner.roleproper
+echo 'Hetzner' >/var/plexguide/pgcloner.projectname
+echo 'v8.6' >/var/plexguide/pgcloner.projectversion
+echo 'hcloud.sh' >/var/plexguide/pgcloner.startlink
+
+#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+echo "💬 HCloud in conjuction with PGBlitz enables users to
+deploy Hetzner Cloud Instance (VMs) within seconds" >/var/plexguide/pgcloner.info
+#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+### START PROCESS
+bash /opt/plexguide/menu/pgcloner/corev2/main.sh
diff --git a/menu/pgcloner/multihd.sh b/menu/pgcloner/multihd.sh
new file mode 100644
index 00000000..e6355382
--- /dev/null
+++ b/menu/pgcloner/multihd.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+
+### FILL OUT THIS AREA ###
+echo 'multihd' >/var/plexguide/pgcloner.rolename
+echo 'MultiHD' >/var/plexguide/pgcloner.roleproper
+echo 'MultiHD' >/var/plexguide/pgcloner.projectname
+echo 'v8.6' >/var/plexguide/pgcloner.projectversion
+echo 'multihd.sh' >/var/plexguide/pgcloner.startlink
+
+#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+echo "💬 MultiHD enables to add multiple drives and mountpoints to MergerFS!" >/var/plexguide/pgcloner.info
+#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+### START PROCESS
+bash /opt/plexguide/menu/pgcloner/corev2/main.sh
diff --git a/menu/pgcloner/pgclone.sh b/menu/pgcloner/pgclone.sh
new file mode 100644
index 00000000..b7c1db27
--- /dev/null
+++ b/menu/pgcloner/pgclone.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+
+### FILL OUT THIS AREA ###
+echo 'pgclone' >/var/plexguide/pgcloner.rolename
+echo 'PG Clone' >/var/plexguide/pgcloner.roleproper
+echo 'PGClone' >/var/plexguide/pgcloner.projectname
+echo 'v8.6' >/var/plexguide/pgcloner.projectversion
+echo 'pgclone.sh' >/var/plexguide/pgcloner.startlink
+
+#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+echo "💬 PG Clone utilizes RClone's Mounts + MergerFS's Union" >/var/plexguide/pgcloner.info
+#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+### START PROCESS
+bash /opt/plexguide/menu/pgcloner/corev2/main.sh
diff --git a/menu/pgcloner/pgpatrol.sh b/menu/pgcloner/pgpatrol.sh
new file mode 100644
index 00000000..88a75ad1
--- /dev/null
+++ b/menu/pgcloner/pgpatrol.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+
+### FILL OUT THIS AREA ###
+echo 'pgpatrol' >/var/plexguide/pgcloner.rolename
+echo 'PGPatrol' >/var/plexguide/pgcloner.roleproper
+echo 'PGPatrol' >/var/plexguide/pgcloner.projectname
+echo 'v8.6' >/var/plexguide/pgcloner.projectversion
+
+#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+echo "💬 PG Patrol can boot idle plex users, users utilizing multiple
+ips (sharing the server), and much more!" >/var/plexguide/pgcloner.info
+#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+### START PROCESS
+bash /opt/plexguide/menu/pgcloner/corev2/main.sh
diff --git a/menu/pgcloner/pgpress.sh b/menu/pgcloner/pgpress.sh
new file mode 100644
index 00000000..b56a85b7
--- /dev/null
+++ b/menu/pgcloner/pgpress.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+
+### FILL OUT THIS AREA ###
+echo 'pgpress' >/var/plexguide/pgcloner.rolename
+echo 'PGPress' >/var/plexguide/pgcloner.roleproper
+echo 'PGPress' >/var/plexguide/pgcloner.projectname
+echo 'v8.6' >/var/plexguide/pgcloner.projectversion
+echo 'pressmain.sh' >/var/plexguide/pgcloner.startlink
+
+#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+echo "💬 PGPress is a combined group of services that enables the user to
+deploy their own wordpress websites; including the use of other multiple
+instances!" >/var/plexguide/pgcloner.info
+#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+### START PROCESS
+bash /opt/plexguide/menu/pgcloner/corev2/main.sh
diff --git a/menu/pgcloner/pgshield.sh b/menu/pgcloner/pgshield.sh
new file mode 100644
index 00000000..b71d87cd
--- /dev/null
+++ b/menu/pgcloner/pgshield.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+
+### FILL OUT THIS AREA ###
+echo 'pgshield' >/var/plexguide/pgcloner.rolename
+echo 'PGShield' >/var/plexguide/pgcloner.roleproper
+echo 'PGShield' >/var/plexguide/pgcloner.projectname
+echo 'v8.6' >/var/plexguide/pgcloner.projectversion
+echo 'pgshield.sh' >/var/plexguide/pgcloner.startlink
+
+#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+echo "💬 PG Shield protects users by deploying adding Google
+Authentication to all the containers for protection!" >/var/plexguide/pgcloner.info
+#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+### START PROCESS
+bash /opt/plexguide/menu/pgcloner/corev2/main.sh
diff --git a/menu/pgcloner/pgvault.sh b/menu/pgcloner/pgvault.sh
new file mode 100644
index 00000000..bacba343
--- /dev/null
+++ b/menu/pgcloner/pgvault.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+
+### FILL OUT THIS AREA ###
+echo 'pgvault' >/var/plexguide/pgcloner.rolename
+echo 'PG Vault' >/var/plexguide/pgcloner.roleproper
+echo 'PGVault' >/var/plexguide/pgcloner.projectname
+echo 'v8.6' >/var/plexguide/pgcloner.projectversion
+echo 'pgvault.sh' >/var/plexguide/pgcloner.startlink
+
+#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+echo "💬 PG Vault is a combined group of services that utilizes the backup
+and restore processes, which enables the safe storage and transport through
+the use of Google Drive in a hasty and efficient manner!" >/var/plexguide/pgcloner.info
+#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+### START PROCESS
+bash /opt/plexguide/menu/pgcloner/corev2/main.sh
diff --git a/menu/pgcloner/solo/pgui.sh b/menu/pgcloner/solo/pgui.sh
new file mode 100644
index 00000000..62a658a6
--- /dev/null
+++ b/menu/pgcloner/solo/pgui.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+
+### FILL OUT THIS AREA ###
+echo 'pgui' >/var/plexguide/pgcloner.rolename
+echo 'UI' >/var/plexguide/pgcloner.roleproper
+echo 'BlitzUI' >/var/plexguide/pgcloner.projectname
+echo 'v8.6' >/var/plexguide/pgcloner.projectversion
+
+### START PROCESS
+ansible-playbook /opt/plexguide/menu/pgcloner/core/primary.yml
diff --git a/menu/pgcloner/traefik.sh b/menu/pgcloner/traefik.sh
new file mode 100644
index 00000000..98bc9e54
--- /dev/null
+++ b/menu/pgcloner/traefik.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+
+### FILL OUT THIS AREA ###
+echo 'traefik' >/var/plexguide/pgcloner.rolename
+echo 'Traefik' >/var/plexguide/pgcloner.roleproper
+echo 'Traefik' >/var/plexguide/pgcloner.projectname
+echo 'v8.6' >/var/plexguide/pgcloner.projectversion
+echo 'traefik.sh' >/var/plexguide/pgcloner.startlink
+
+#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+echo "💬 Traefik is a modern HTTP reverse proxy and load balancer that makes
+deploying microservices easy. It serves as a reverse proxy that enables a
+user to mass obtain https (secure) certificates for all their containers" >/var/plexguide/pgcloner.info
+#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+### START PROCESS
+bash /opt/plexguide/menu/pgcloner/corev2/main.sh
diff --git a/menu/pgdnsswitcher/pgdnschanger.sh b/menu/pgdnsswitcher/pgdnschanger.sh
new file mode 100644
index 00000000..4544bd0a
--- /dev/null
+++ b/menu/pgdnsswitcher/pgdnschanger.sh
@@ -0,0 +1,88 @@
+#!/bin/bash
+#
+# Title: PGBlitz (PG DNS chnager)
+# Author(s): Admin9705 - Deiteq - Sub7Seven
+# Coder : MrDoob | Freelaancer Coder TechLead
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+
+tee <<-EOF
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+ ⌛ Verifiying PG DNS ( resolv.conf ) changer
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+
+echo "Updating packages"
+apt-get update -yqq >/dev/null
+echo "Upgrading packages"
+apt-get upgrade -yqq >/dev/null
+echo "Dist-Upgrading packages"
+apt-get dist-upgrade -yqq >/dev/null
+echo "Autoremove old Updates"
+apt-get autoremove -yqq >/dev/null
+echo "install complete"
+
+tee <<-EOF
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+ 🚀 PG DNS ( resolv.conf ) changer
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ [1] Google DNS IPv4
+ [2] Google DNS IPv4 + IPv6
+ [3] Cloudflare DNS IPv4
+ [4] Cloudflare DNS IPv4 + IPv6
+ [5] OpenDNS IPv4
+ [6] Comodo Secure DNS
+
+ [Z] Exit
+
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+EOF
+
+# Standby
+read -p 'Type a Number | Press [ENTER]: ' typed "0" and exit.user_input < "4"
+ retries: 99
+ delay: 1
+
+ - fail:
+ msg: 'User Opted NOT to CONTINUE! Aborting!'
+ when: exit.user_input == "1"
+ ############################## REGISTER PROJECT
+ - pause:
+ prompt: "\nWhat is Your GitHub UserName (Case Sensitive)?"
+ register: project
+ when: exit.user_input == "2"
+
+ - name: Store User Name
+ shell: "echo '{{project.user_input}}' > /var/plexguide/pgfork.project"
+ when: exit.user_input == "2"
+ ############################## REGISTER BRANCH
+ - pause:
+ prompt: "\nExample: Version-6 / mybranch / rclonefix\nPlease Update Your Branch-Release (Case Sensitive)"
+ register: version
+ when: exit.user_input == "2"
+
+ - name: Store Project Link
+ shell: "echo '{{version.user_input}}' > /var/plexguide/pgfork.version"
+ when: exit.user_input == "2"
+ ############################################# END
+ - name: Reregister Project Link
+ shell: 'cat /var/plexguide/pgfork.project'
+ register: projectfinal
+
+ - name: Reregister Project Link
+ shell: 'cat /var/plexguide/pgfork.version'
+ register: versionfinal
+
+ - name: Clone Fork
+ git:
+ repo: 'https://github.com/{{projectfinal.stdout}}/PGBlitz.com'
+ dest: /opt/plexguide
+ version: '{{versionfinal.stdout}}'
+ force: yes
+
+ - name: Note Fork Version
+ shell: "echo 'Personal Fork' > /var/plexguide/pg.number"
diff --git a/menu/pggce/gcechecker.sh b/menu/pggce/gcechecker.sh
new file mode 100644
index 00000000..672ca9dc
--- /dev/null
+++ b/menu/pggce/gcechecker.sh
@@ -0,0 +1,139 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705 - Deiteq
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+
+### NOTE THIS IS JUST A COPY - MAIN ONE SITE IN MAIN REPO - THIS IS JUST FOR INFO
+file1="/dev/nvme0n1"
+file2="/var/plexguide/gce.check"
+gcheck=$(dnsdomainname | tail -c 10)
+if [ -e "$file1" ] && [ ! -e "$file2" ] && [ "$gcheck" == ".internal" ]; then
+
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+📂 Google Cloud Feeder Edition SET!
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+⚡ Google Cloud Instance Detected!
+
+⚠️ NOTE: Setting Up the NVME Drive For You! Please Wait!
+⚠️ NOTE: Please don't close it !
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+
+ apt-get install mdadm --no-install-recommends -yqq 2>&1 >>/dev/null
+ export DEBIAN_FRONTEND=noninteractive
+ #Check for NVME
+ lsblk | grep nvme | awk '{print $1}' >/var/plexguide/nvme.log
+ lsblk | grep nvme | awk '{print $1}' >/var/plexguide/nvmeraid.log
+ sed -i 's/nvme0n//g' /var/plexguide/nvmeraid.log
+ #Check for NVME
+ nvme="$(tail -n1 /var/plexguide/nvmeraid.log)"
+
+ if [[ "$nvme" == "2" ]]; then
+ mdadm --create /dev/md0 --level=0 --raid-devices=2 /dev/nvme0n1 /dev/nvme0n2
+ mkfs.ext4 -F /dev/md0
+ mkdir -p /mnt
+ mount /dev/md0 /mnt
+ sed -i '$ a\/dev/md0 /mnt ext4 discard,defaults,nobarrier,nofail 0 0' /etc/fstab
+ chown -cR 1000:1000 /mnt
+ tune2fs -m 0 /dev/md0
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo "✅ PASSED ! PG NVME RAID0 Creator with 2 NVMEs - finish"
+ echo "✅ PASSED ! HDD Space now :" "$(df -h /mnt/ --total --local -x tmpfs | grep 'total' | awk '{print $2}')"
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ sleep 2
+ elif [[ "$nvme" == "3" ]]; then
+ mdadm --create /dev/md0 --level=0 --raid-devices=3 /dev/nvme0n1 /dev/nvme0n2 /dev/nvme0n3
+ mkfs.ext4 -F /dev/md0
+ mkdir -p /mnt
+ mount /dev/md0 /mnt
+ sed -i '$ a\/dev/md0 /mnt ext4 discard,defaults,nobarrier,nofail 0 0' /etc/fstab
+ chown -cR 1000:1000 /mnt
+ tune2fs -m 0 /dev/md0
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo "✅ PASSED ! PG NVME RAID0 Creator with 3 NVMEs - finish"
+ echo "✅ PASSED ! HDD Space now :" "$(df -h /mnt/ --total --local -x tmpfs | grep 'total' | awk '{print $2}')"
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ sleep 2
+ elif [[ "$nvme" == "4" ]]; then
+ mdadm --create /dev/md0 --level=0 --raid-devices=4 /dev/nvme0n1 /dev/nvme0n2 /dev/nvme0n3 /dev/nvme0n4
+ mkfs.ext4 -F /dev/md0
+ mkdir -p /mnt
+ mount /dev/md0 /mnt
+ sed -i '$ a\/dev/md0 /mnt ext4 discard,defaults,nobarrier,nofail 0 0' /etc/fstab
+ chown -cR 1000:1000 /mnt
+ tune2fs -m 0 /dev/md0
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo "✅ PASSED ! PG NVME RAID0 Creator with 4 NVMEs - finish"
+ echo "✅ PASSED ! HDD Space now :" "$(df -h /mnt/ --total --local -x tmpfs | grep 'total' | awk '{print $2}')"
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ sleep 2
+ elif [[ "$nvme" == "1" ]]; then
+ sleep 3
+ mkfs.ext4 -F /dev/nvme0n1 1>/dev/null 2>&1
+ mount -o discard,defaults,nobarrier /dev/nvme0n1 /mnt
+ chmod a+w /mnt 1>/dev/null 2>&1
+ echo UUID="$(blkid | grep nvme0n1 | cut -f2 -d'"')" /mnt ext4 discard,defaults,nobarrier,nofail 0 2 | tee -a /etc/fstab
+
+ mkdir -p /nvme1 1>/dev/null 2>&1
+ mkfs.ext4 -F /dev/nvme0n1
+ mount -o discard,defaults,nobarrier /dev/nvme0n1 /nvme1
+ chmod a+w /nvme1 1>/dev/null 2>&1
+ echo UUID="$(blkid | grep nvme0n1 | cut -f2 -d'"')" /nvme1 ext4 discard,defaults,nobarrier,nofail 0 2 | tee -a /etc/fstab
+ else
+ echo "nothing to do"
+ fi
+
+ touch /var/plexguide/gce.check
+ rm -rf /var/plexguide/gce.failed 1>/dev/null 2>&1
+ rm -rf /var/plexguide/gce.false 1>/dev/null 2>&1
+ rm -rf /var/plexguide/nvme.log 1>/dev/null 2>&1
+ rm -rf /var/plexguide/nvmeraid.log 1>/dev/null 2>&1
+
+ echo "feeder" >/var/plexguide/pg.server.deploy
+ cat /var/plexguide/pg.edition >/var/plexguide/pg.edition.stored
+
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+📂 GCE Harddrive Deployed!
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+⚡ Automatically Setting PG Google Feeder Edition (GCE)
+
+⚠️ Please Wait!
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+
+ sleep 6
+elif [ ! -e "$file1" ] && [ ! -e "$file2" ] && [ "$gcheck" == ".internal" ]; then
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+📂 Google Cloud Feeder Edition Failed!
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+⚡ Google Cloud Instance Detected, but you Failed to setup an NVME
+ drive per the wiki! This mistake only occurs on manual GCE
+ deployments. Most likely you setup an SSD instead! The install will
+ continue, but this will fail! Wipe the box and setup again with an
+ NVME Drive!
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+EOF
+ read -p 'Press [ENTER] to Continue! ' typed /dev/null 2>&1
+ rm -rf /var/plexguide/gce.false 1>/dev/null 2>&1
+ rm -rf /var/plexguide/nvme.log 1>/dev/null 2>&1
+ rm -rf /var/plexguide/nvmeraid.log 1>/dev/null 2>&1
+else
+ touch /var/plexguide/gce.false
+fi
diff --git a/menu/pgscan/LICENSE.md b/menu/pgscan/LICENSE.md
new file mode 100644
index 00000000..94a9ed02
--- /dev/null
+++ b/menu/pgscan/LICENSE.md
@@ -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/menu/pgscan/config.py b/menu/pgscan/config.py
new file mode 100644
index 00000000..7445e23e
--- /dev/null
+++ b/menu/pgscan/config.py
@@ -0,0 +1,373 @@
+import argparse
+import json
+import logging
+import os
+import sys
+import uuid
+from copy import copy
+
+logger = logging.getLogger("CONFIG")
+
+
+class Singleton(type):
+ _instances = {}
+
+ def __call__(cls, *args, **kwargs):
+ if cls not in cls._instances:
+ cls._instances[cls] = super(
+ Singleton, cls).__call__(*args, **kwargs)
+
+ return cls._instances[cls]
+
+
+class Config(object):
+ __metaclass__ = Singleton
+
+ base_config = {
+ 'PLEX_USER': 'plex',
+ 'PLEX_SECTION_PATH_MAPPINGS': {},
+ 'PLEX_SCANNER': '/usr/lib/plexmediaserver/Plex\\ Media\\ Scanner',
+ 'PLEX_SUPPORT_DIR': '/var/lib/plexmediaserver/Library/Application\ Support',
+ 'PLEX_LD_LIBRARY_PATH': '/usr/lib/plexmediaserver',
+ 'PLEX_DATABASE_PATH': '/var/lib/plexmediaserver/Library/Application Support/Plex Media Server'
+ '/Plug-in Support/Databases/com.plexapp.plugins.library.db',
+ 'PLEX_LOCAL_URL': 'http://localhost:32400',
+ 'PLEX_EMPTY_TRASH': False,
+ 'PLEX_EMPTY_TRASH_MAX_FILES': 100,
+ 'PLEX_EMPTY_TRASH_CONTROL_FILES': [],
+ 'PLEX_EMPTY_TRASH_ZERO_DELETED': False,
+ 'PLEX_WAIT_FOR_EXTERNAL_SCANNERS': True,
+ 'PLEX_ANALYZE_TYPE': 'basic',
+ 'PLEX_ANALYZE_DIRECTORY': True,
+ 'PLEX_TOKEN': '',
+ 'SERVER_IP': '0.0.0.0',
+ 'SERVER_PORT': 3467,
+ 'SERVER_PASS': uuid.uuid4().hex,
+ 'SERVER_PATH_MAPPINGS': {},
+ 'SERVER_SCAN_DELAY': 180,
+ 'SERVER_MAX_FILE_CHECKS': 10,
+ 'SERVER_FILE_CHECK_DELAY': 60,
+ 'SERVER_FILE_EXIST_PATH_MAPPINGS': {},
+ 'SERVER_ALLOW_MANUAL_SCAN': False,
+ 'SERVER_IGNORE_LIST': [],
+ 'SERVER_USE_SQLITE': False,
+ 'SERVER_SCAN_PRIORITIES': {},
+ 'SERVER_SCAN_FOLDER_ON_FILE_EXISTS_EXHAUSTION': False,
+ 'RCLONE_RC_CACHE_EXPIRE': {
+ 'ENABLED': False,
+ 'FILE_EXISTS_TO_REMOTE_MAPPINGS': {
+ },
+ 'RC_URL': 'http://localhost:5572'
+ },
+ 'DOCKER_NAME': 'plex',
+ 'RUN_COMMAND_BEFORE_SCAN': '',
+ 'RUN_COMMAND_AFTER_SCAN': '',
+ 'USE_DOCKER': False,
+ 'USE_SUDO': True,
+ 'GDRIVE': {
+ 'CLIENT_ID': '',
+ 'CLIENT_SECRET': '',
+ 'POLL_INTERVAL': 60,
+ 'ENABLED': False,
+ 'TEAMDRIVE': False,
+ 'SCAN_EXTENSIONS': [],
+ 'IGNORE_PATHS': []
+ }
+ }
+
+ base_settings = {
+ 'config': {
+ 'argv': '--config',
+ 'env': 'PLEX_AUTOSCAN_CONFIG',
+ 'default': os.path.join(os.path.dirname(sys.argv[0]), 'config', 'config.json')
+ },
+ 'logfile': {
+ 'argv': '--logfile',
+ 'env': 'PLEX_AUTOSCAN_LOGFILE',
+ 'default': os.path.join(os.path.dirname(sys.argv[0]), 'plex_autoscan.log')
+ },
+ 'loglevel': {
+ 'argv': '--loglevel',
+ 'env': 'PLEX_AUTOSCAN_LOGLEVEL',
+ 'default': 'INFO'
+ },
+ 'queuefile': {
+ 'argv': '--queuefile',
+ 'env': 'PLEX_AUTOSCAN_QUEUEFILE',
+ 'default': os.path.join(os.path.dirname(sys.argv[0]), 'queue.db')
+ },
+ 'tokenfile': {
+ 'argv': '--tokenfile',
+ 'env': 'PLEX_AUTOSCAN_TOKENFILE',
+ 'default': os.path.join(os.path.dirname(sys.argv[0]), 'token.json')
+ },
+ 'cachefile': {
+ 'argv': '--cachefile',
+ 'env': 'PLEX_AUTOSCAN_CACHEFILE',
+ 'default': os.path.join(os.path.dirname(sys.argv[0]), 'cache.db')
+ }
+ }
+
+ def __init__(self):
+ """Initializes config"""
+ # Args and settings
+ self.args = self.parse_args()
+ self.settings = self.get_settings()
+ # Configs
+ self.configs = None
+
+ @property
+ def default_config(self):
+ cfg = copy(self.base_config)
+
+ # add example scan priorities
+ cfg['SERVER_SCAN_PRIORITIES'] = {
+ "0": [
+ '/Movies/'
+ ],
+ "1": [
+ '/TV/'
+ ],
+ "2": [
+ '/Music/'
+ ]
+ }
+
+ # add example section path mappings
+ cfg['PLEX_SECTION_PATH_MAPPINGS'] = {
+ '1': [
+ '/Movies/'
+ ],
+ '2': [
+ '/TV/'
+ ]
+ }
+
+ # add example file trash control files
+ cfg['PLEX_EMPTY_TRASH_CONTROL_FILES'] = ['/mnt/unionfs/mounted.bin']
+
+ # add example server path mappings
+ cfg['SERVER_PATH_MAPPINGS'] = {
+ '/mnt/unionfs': [
+ '/home/seed/media/fused'
+ ]
+ }
+
+ # add example file exist path mappings
+ cfg['SERVER_FILE_EXIST_PATH_MAPPINGS'] = {
+ '/home/thompsons/plexdrive': [
+ '/data'
+ ]
+ }
+ # add example server ignore list
+ cfg['SERVER_IGNORE_LIST'] = ['/.grab/', '.DS_Store', 'Thumbs.db']
+
+ # add example scan extensions to gdrive
+ cfg['GDRIVE']['SCAN_EXTENSIONS'] = ['webm', 'mkv', 'flv', 'vob', 'ogv', 'ogg', 'drc', 'gif', 'gifv', 'mng',
+ 'avi', 'mov', 'qt', 'wmv', 'yuv', 'rm', 'rmvb', 'asf', 'amv', 'mp4', 'm4p',
+ 'm4v', 'mpg', 'mp2', 'mpeg', 'mpe', 'mpv', 'm2v', 'm4v', 'svi', '3gp',
+ '3g2', 'mxf', 'roq', 'nsv', 'f4v', 'f4p', 'f4a', 'f4b', 'mp3', 'flac', 'ts']
+
+ # add example rclone file exists to remote mappings
+ cfg['RCLONE_RC_CACHE_EXPIRE']['FILE_EXISTS_TO_REMOTE_MAPPINGS'] = {
+ 'Media/': [
+ '/home/thompsons/plexdrive/Media'
+ ]
+ }
+
+ return cfg
+
+ def __inner_upgrade(self, settings1, settings2, key=None, overwrite=False):
+ sub_upgraded = False
+ merged = copy(settings2)
+
+ if isinstance(settings1, dict):
+ for k, v in settings1.items():
+ # missing k
+ if k not in settings2:
+ merged[k] = v
+ sub_upgraded = True
+ if not key:
+ logger.info("Added %r config option: %s",
+ str(k), str(v))
+ else:
+ logger.info("Added %r to config option %r: %s",
+ str(k), str(key), str(v))
+ continue
+
+ # iterate children
+ if isinstance(v, dict) or isinstance(v, list):
+ merged[k], did_upgrade = self.__inner_upgrade(settings1[k], settings2[k], key=k,
+ overwrite=overwrite)
+ sub_upgraded = did_upgrade if did_upgrade else sub_upgraded
+ elif settings1[k] != settings2[k] and overwrite:
+ merged = settings1
+ sub_upgraded = True
+ elif isinstance(settings1, list) and key:
+ for v in settings1:
+ if v not in settings2:
+ merged.append(v)
+ sub_upgraded = True
+ logger.info("Added to config option %r: %s",
+ str(key), str(v))
+ continue
+
+ return merged, sub_upgraded
+
+ def upgrade_settings(self, currents):
+ fields_env = {}
+
+ # ENV gets priority: ENV > config.json
+ for name, data in self.base_config.items():
+ if name in os.environ:
+ # Use JSON decoder to get same behaviour as config file
+ fields_env[name] = json.JSONDecoder().decode(os.environ[name])
+ logger.info("Using ENV setting %s=%s", name, fields_env[name])
+
+ # Update in-memory config with environment settings
+ currents.update(fields_env)
+
+ # Do inner upgrade
+ upgraded_settings, upgraded = self.__inner_upgrade(
+ self.base_config, currents)
+ return upgraded_settings, upgraded
+
+ def load(self):
+ if not os.path.exists(self.settings['config']):
+ logger.warn("No config file found, creating default config.")
+ self.save(self.default_config)
+
+ cfg = {}
+ with open(self.settings['config'], 'r') as fp:
+ cfg, upgraded = self.upgrade_settings(json.load(fp))
+
+ # Save config if upgraded
+ if upgraded:
+ self.save(cfg)
+ exit(0)
+
+ self.configs = cfg
+
+ def save(self, cfg, exitOnSave=True):
+ with open(self.settings['config'], 'w') as fp:
+ json.dump(cfg, fp, indent=2, sort_keys=True)
+ if exitOnSave:
+ logger.warn(
+ "Please configure/review config before running again: %r",
+ self.settings['config']
+ )
+
+ if exitOnSave:
+ exit(0)
+
+ def get_settings(self):
+ setts = {}
+ for name, data in self.base_settings.items():
+ # Argrument priority: cmd < environment < default
+ try:
+ value = None
+ # Command line argument
+ if self.args[name]:
+ value = self.args[name]
+ logger.info("Using ARG setting %s=%s", name, value)
+
+ # Envirnoment variable
+ elif data['env'] in os.environ:
+ value = os.environ[data['env']]
+ logger.info("Using ENV setting %s=%s" % (
+ data['env'],
+ value
+ ))
+
+ # Default
+ else:
+ value = data['default']
+ logger.info("Using default setting %s=%s" % (
+ data['argv'],
+ value
+ ))
+
+ setts[name] = value
+
+ except Exception:
+ logger.exception(
+ "Exception retrieving setting value: %r" % name)
+
+ return setts
+
+ # Parse command line arguments
+ def parse_args(self):
+ parser = argparse.ArgumentParser(
+ description=(
+ 'Script to assist sonarr/radarr with plex imports. Will only scan the folder \n'
+ 'that has been imported, instead of the whole library section.'
+ ),
+ formatter_class=argparse.RawTextHelpFormatter
+ )
+
+ # Mode
+ parser.add_argument('cmd',
+ choices=('sections', 'server',
+ 'authorize', 'update_sections'),
+ help=(
+ '"sections": prints plex sections\n'
+ '"server": starts the application\n'
+ '"authorize": authorize against a google account\n'
+ '"update_sections": update section mappings in config\n'
+ )
+ )
+
+ # Config file
+ parser.add_argument(self.base_settings['config']['argv'],
+ nargs='?',
+ const=None,
+ help='Config file location (default: %s)' % self.base_settings[
+ 'config']['default']
+ )
+
+ # Log file
+ parser.add_argument(self.base_settings['logfile']['argv'],
+ nargs='?',
+ const=None,
+ help='Log file location (default: %s)' % self.base_settings[
+ 'logfile']['default']
+ )
+
+ # Queue file
+ parser.add_argument(self.base_settings['queuefile']['argv'],
+ nargs='?',
+ const=None,
+ help='Queue file location (default: %s)' % self.base_settings[
+ 'queuefile']['default']
+ )
+
+ # Token file
+ parser.add_argument(self.base_settings['tokenfile']['argv'],
+ nargs='?',
+ const=None,
+ help='Google token file location (default: %s)' % self.base_settings[
+ 'tokenfile']['default']
+ )
+
+ # Cache file
+ parser.add_argument(self.base_settings['cachefile']['argv'],
+ nargs='?',
+ const=None,
+ help='Google cache file location (default: %s)' % self.base_settings[
+ 'cachefile']['default']
+ )
+
+ # Logging level
+ parser.add_argument(self.base_settings['loglevel']['argv'],
+ choices=('WARN', 'INFO', 'DEBUG'),
+ help='Log level (default: %s)' % self.base_settings['loglevel']['default']
+ )
+
+ # Print help by default if no arguments
+ if len(sys.argv) == 1:
+ parser.print_help()
+
+ sys.exit(0)
+
+ else:
+ return vars(parser.parse_args())
diff --git a/menu/pgscan/config/default.config b/menu/pgscan/config/default.config
new file mode 100644
index 00000000..7a6ab83e
--- /dev/null
+++ b/menu/pgscan/config/default.config
@@ -0,0 +1,103 @@
+{
+ "DOCKER_NAME":"plex",
+ "GDRIVE":{
+ "CLIENT_ID":"",
+ "CLIENT_SECRET":"",
+ "ENABLED":false,
+ "POLL_INTERVAL":60,
+ "IGNORE_PATHS": [],
+ "SCAN_EXTENSIONS":[
+ "webm","mkv","flv","vob","ogv","ogg","drc","gif",
+ "gifv","mng","avi","mov","qt","wmv","yuv","rm",
+ "rmvb","asf","amv","mp4","m4p","m4v","mpg","mp2",
+ "mpeg","mpe","mpv","m2v","m4v","svi","3gp","3g2",
+ "mxf","roq","nsv","f4v","f4p","f4a","f4b","mp3",
+ "flac","ts"
+ ],
+ "TEAMDRIVE": false
+ },
+ "PLEX_ANALYZE_DIRECTORY":true,
+ "PLEX_ANALYZE_TYPE":"basic",
+ "PLEX_DATABASE_PATH":"/opt/appdata/plex/Library/Application Support/Plex Media Server/Plug-in Support/Databases/com.plexapp.plugins.library.db",
+ "PLEX_EMPTY_TRASH":true,
+ "PLEX_EMPTY_TRASH_CONTROL_FILES":[
+ "/mnt/unionfs/mounted.bin"
+ ],
+ "PLEX_EMPTY_TRASH_MAX_FILES":100,
+ "PLEX_EMPTY_TRASH_ZERO_DELETED":false,
+ "PLEX_LD_LIBRARY_PATH":"/usr/lib/plexmediaserver",
+ "PLEX_LOCAL_URL":"https://ipv4:32400",
+ "PLEX_SCANNER":"/usr/lib/plexmediaserver/Plex\\ Media\\ Scanner",
+ "PLEX_SECTION_PATH_MAPPINGS":{
+ "1":[
+ "/Movies/"
+ ],
+ "2":[
+ "/TV/"
+ ],
+ "3":[
+ "/Music/"
+ ]
+ },
+ "PLEX_SUPPORT_DIR":"/var/lib/plexmediaserver/Library/Application\\ Support",
+ "PLEX_TOKEN":"{{token}}",
+ "PLEX_USER":"plex",
+ "PLEX_WAIT_FOR_EXTERNAL_SCANNERS":true,
+ "RCLONE_RC_CACHE_EXPIRE":{
+ "ENABLED":false,
+ "FILE_EXISTS_TO_REMOTE_MAPPINGS": {},
+ "MOUNT_FOLDER":"/mnt/rclone",
+ "RC_URL":"http://localhost:5572"
+ },
+ "RUN_COMMAND_BEFORE_SCAN":"",
+ "RUN_COMMAND_AFTER_SCAN": "",
+ "SERVER_ALLOW_MANUAL_SCAN":false,
+ "SERVER_FILE_CHECK_DELAY":60,
+ "SERVER_MAX_FILE_CHECKS":10,
+ "SERVER_FILE_EXIST_PATH_MAPPINGS":{
+ "/mnt/unionfs/Media":[
+ "/data"
+ ]
+ },
+ "SERVER_IGNORE_LIST":[
+ "/.grab/",
+ ".DS_Store",
+ "Thumbs.db"
+ ],
+ "SERVER_IP":"0.0.0.0",
+ "SERVER_PASS":"{{token.stdout}}",
+ "SERVER_PATH_MAPPINGS":{
+ "/data/Movies/":[
+ "/movies/",
+ "/mnt/unionfs/Media/Movies/",
+ "My Drive/Media/Movies/"
+ ],
+ "/data/TV/":[
+ "/tv/",
+ "/mnt/unionfs/Media/TV/",
+ "My Drive/Media/TV/"
+ ],
+ "/data/Music/":[
+ "/music/",
+ "/mnt/unionfs/Media/Music/",
+ "My Drive/Media/Music/"
+ ]
+ },
+ "SERVER_PORT":3468,
+ "SERVER_SCAN_DELAY":180,
+ "SERVER_SCAN_FOLDER_ON_FILE_EXISTS_EXHAUSTION":true,
+ "SERVER_SCAN_PRIORITIES":{
+ "0":[
+ "/TV/"
+ ],
+ "1":[
+ "/Movies/"
+ ],
+ "2":[
+ "/Music/"
+ ]
+ },
+ "SERVER_USE_SQLITE":true,
+ "USE_DOCKER":true,
+ "USE_SUDO":false
+}
diff --git a/menu/pgscan/db.py b/menu/pgscan/db.py
new file mode 100644
index 00000000..50fd2664
--- /dev/null
+++ b/menu/pgscan/db.py
@@ -0,0 +1,124 @@
+import logging
+import os
+
+from peewee import DeleteQuery
+from peewee import Model, SqliteDatabase, CharField, IntegerField
+
+import config
+
+logger = logging.getLogger("DB")
+
+# Config
+conf = config.Config()
+
+db_path = conf.settings['queuefile']
+database = SqliteDatabase(db_path, threadlocals=True)
+
+
+class BaseQueueModel(Model):
+ class Meta:
+ database = database
+
+
+class QueueItemModel(BaseQueueModel):
+ scan_path = CharField(max_length=256, unique=True, null=False)
+ scan_for = CharField(max_length=64, null=False)
+ scan_section = IntegerField(null=False)
+ scan_type = CharField(max_length=64, null=False)
+
+
+def create_database(db, db_path):
+ if not os.path.exists(db_path):
+ db.create_tables([QueueItemModel])
+ logger.info("Created database tables")
+
+
+def connect(db):
+ if not db.is_closed():
+ return False
+ return db.connect()
+
+
+def init(db, db_path):
+ if not os.path.exists(db_path):
+ create_database(db, db_path)
+ connect(db)
+
+
+def get_next_item():
+ item = None
+ try:
+ item = QueueItemModel.get()
+ except Exception:
+ # logger.exception("Exception getting first item to scan: ")
+ pass
+ return item
+
+
+def exists_file_root_path(file_path):
+ items = get_all_items()
+ if '.' in file_path:
+ dir_path = os.path.dirname(file_path)
+ else:
+ dir_path = file_path
+
+ for item in items:
+ if dir_path.lower() in item['scan_path'].lower():
+ return True, item['scan_path']
+ return False, None
+
+
+def get_all_items():
+ items = []
+ try:
+ for item in QueueItemModel.select():
+ items.append({'scan_path': item.scan_path,
+ 'scan_for': item.scan_for,
+ 'scan_type': item.scan_type,
+ 'scan_section': item.scan_section})
+ except Exception:
+ logger.exception("Exception getting all items from database: ")
+ return None
+ return items
+
+
+def get_queue_count():
+ count = 0
+ try:
+ count = QueueItemModel.select().count()
+ except Exception:
+ logger.exception("Exception getting queued item count from database: ")
+ return count
+
+
+def remove_item(scan_path):
+ try:
+ return DeleteQuery(QueueItemModel).where(QueueItemModel.scan_path == scan_path).execute()
+ except Exception:
+ logger.exception("Exception deleting %r from database: ", scan_path)
+ return False
+
+
+def add_item(scan_path, scan_for, scan_section, scan_type):
+ item = None
+ try:
+ return QueueItemModel.create(scan_path=scan_path, scan_for=scan_for, scan_section=scan_section,
+ scan_type=scan_type)
+ except AttributeError as ex:
+ return item
+ except Exception:
+ pass
+ # logger.exception("Exception adding %r to database: ", scan_path)
+ return item
+
+
+def queued_count():
+ try:
+ return QueueItemModel.select().count()
+ except Exception:
+ logger.exception("Exception retrieving queued count: ")
+ return 0
+
+
+# Init
+init(database, db_path)
diff --git a/menu/pgscan/gdrive.py b/menu/pgscan/gdrive.py
new file mode 100644
index 00000000..6194a217
--- /dev/null
+++ b/menu/pgscan/gdrive.py
@@ -0,0 +1,303 @@
+import json
+import logging
+import os
+from urllib import urlencode
+
+import backoff
+import requests
+from sqlitedict import SqliteDict
+
+import utils
+
+logger = logging.getLogger("GDRIVE")
+
+
+class Gdrive:
+ def __init__(self, config, token_path, cache_path):
+ self.cfg = config
+ self.token_path = token_path
+ self.cache_path = cache_path
+ self.token = None
+ self.cache = None
+
+ def first_run(self):
+ # token file
+ if not os.path.exists(self.token_path):
+ # token.json does not exist, lets do the first run auth process
+ print("Visit %s and authorize against the account you wish to use" %
+ self.authorize_url())
+ auth_code = raw_input('Enter authorization code: ')
+ if self.first_access_token(auth_code) and self.token is not None:
+ self.dump_token()
+ else:
+ logger.error(
+ "Failed to authorize with the supplied client_id/client_secret/auth_code...")
+ return False
+ else:
+ self.token = utils.load_json(self.token_path)
+
+ # cache file
+ self.cache = SqliteDict(self.cache_path, tablename='cache', encode=json.dumps, decode=json.loads,
+ autocommit=False)
+ return True
+
+ def authorize_url(self):
+ payload = {
+ 'client_id': self.cfg['GDRIVE']['CLIENT_ID'],
+ 'redirect_uri': 'urn:ietf:wg:oauth:2.0:oob',
+ 'response_type': 'code',
+ 'access_type': 'offline',
+ 'scope': 'https://www.googleapis.com/auth/drive'
+ }
+ url = 'https://accounts.google.com/o/oauth2/v2/auth?' + \
+ urlencode(payload)
+ return url
+
+ def first_access_token(self, auth_code):
+ logger.info("Requesting access token for auth code %r", auth_code)
+ payload = {
+ 'code': auth_code,
+ 'client_id': self.cfg['GDRIVE']['CLIENT_ID'],
+ 'client_secret': self.cfg['GDRIVE']['CLIENT_SECRET'],
+ 'grant_type': 'authorization_code',
+ 'redirect_uri': 'urn:ietf:wg:oauth:2.0:oob',
+ }
+ success, resp, data = self._make_request('https://www.googleapis.com/oauth2/v4/token', data=payload,
+ headers={}, request_type='post')
+ if success and resp.status_code == 200:
+ logger.info("Retrieved first access token!")
+ self.token = data
+ self.token['page_token'] = ''
+ return True
+ else:
+ logger.error("Error retrieving first access_token:\n%s", data)
+ return False
+
+ def refresh_access_token(self):
+ logger.debug("Renewing access token...")
+ payload = {
+ 'refresh_token': self.token['refresh_token'],
+ 'client_id': self.cfg['GDRIVE']['CLIENT_ID'],
+ 'client_secret': self.cfg['GDRIVE']['CLIENT_SECRET'],
+ 'grant_type': 'refresh_token',
+ }
+ success, resp, data = self._make_request('https://www.googleapis.com/oauth2/v4/token', data=payload,
+ headers={}, request_type='post')
+ if success and resp.status_code == 200 and 'access_token' in data:
+ logger.info("Renewed access token!")
+
+ refresh_token = self.token['refresh_token']
+ page_token = self.token['page_token']
+ self.token = data
+ if 'refresh_token' not in self.token or not self.token['refresh_token']:
+ self.token['refresh_token'] = refresh_token
+ self.token['page_token'] = page_token
+ self.dump_token()
+ return True
+ else:
+ logger.error("Error renewing access token:\n%s", data)
+ return False
+
+ def get_changes_first_page_token(self):
+ success, resp, data = self._make_request('https://www.googleapis.com/drive/v3/changes/startPageToken',
+ params={'supportsTeamDrives': self.cfg['GDRIVE']['TEAMDRIVE']})
+ if success and resp.status_code == 200:
+ if 'startPageToken' not in data:
+ logger.error(
+ "Failed to retrieve startPageToken from returned startPageToken:\n%s", data)
+ return False
+ self.token['page_token'] = data['startPageToken']
+ self.dump_token()
+ return True
+ else:
+ logger.error("Error retrieving first page token:\n%s", data)
+ return False
+
+ def get_changes(self):
+ success, resp, data = self._make_request('https://www.googleapis.com/drive/v3/changes',
+ params={'pageToken': self.token['page_token'], 'pageSize': 1000,
+ 'includeRemoved': True,
+ 'includeTeamDriveItems': self.cfg['GDRIVE'][
+ 'TEAMDRIVE'],
+ 'supportsTeamDrives': self.cfg['GDRIVE']['TEAMDRIVE'],
+ 'fields': 'changes(file(md5Checksum,mimeType,modifiedTime,'
+ 'name,parents,teamDriveId,trashed),'
+ 'fileId,removed,teamDrive(id,name),'
+ 'teamDriveId),newStartPageToken,nextPageToken'})
+ if success and resp.status_code == 200:
+ # page token logic
+ if data is not None and 'nextPageToken' in data:
+ self.token['page_token'] = data['nextPageToken']
+ self.dump_token()
+ elif data is not None and 'newStartPageToken' in data:
+ self.token['page_token'] = data['newStartPageToken']
+ self.dump_token()
+ else:
+ logger.error("Unexpected response while polling for changes from page %s:\n%s",
+ str(self.token['page_token']), data)
+ return False, data
+ return True, data
+ else:
+ logger.error("Error getting page changes for page_token %r:\n%s",
+ self.token['page_token'], data)
+ return False, data
+
+ def get_id_metadata(self, item_id, teamdrive_id=None):
+ # return cache from metadata if available
+ cached_metadata = self._get_cached_metdata(item_id)
+ if cached_metadata:
+ return True, cached_metadata
+
+ # does item_id match teamdrive_id?
+ if teamdrive_id is not None and item_id == teamdrive_id:
+ success, resp, data = self._make_request(
+ 'https://www.googleapis.com/drive/v3/teamdrives/%s' % str(item_id))
+ if success and resp.status_code == 200 and 'name' in data:
+ # we successfully retrieved this teamdrive info, lets place a mimeType key in the result
+ # so we know it needs to be cached
+ data['mimeType'] = 'application/vnd.google-apps.folder'
+ else:
+ # retrieve file metadata
+ success, resp, data = self._make_request('https://www.googleapis.com/drive/v3/files/%s' % str(item_id),
+ params={
+ 'supportsTeamDrives': self.cfg['GDRIVE']['TEAMDRIVE'],
+ 'fields': 'id,md5Checksum,mimeType,modifiedTime,name,parents,'
+ 'trashed,teamDriveId'})
+ if success and resp.status_code == 200:
+ return True, data
+ else:
+ logger.error(
+ "Error retrieving metadata for item %r:\n%s", item_id, data)
+ return False, data
+
+ def get_id_file_paths(self, item_id, teamdrive_id=None):
+ file_paths = []
+ added_to_cache = 0
+
+ try:
+ def get_item_paths(obj_id, path, paths, new_cache_entries, teamdrive_id=None):
+ success, obj = self.get_id_metadata(obj_id, teamdrive_id)
+ if not success:
+ return new_cache_entries
+
+ teamdrive_id = teamdrive_id if 'teamDriveId' not in obj else obj['teamDriveId']
+
+ # add item object to cache if we know its not from cache
+ if 'mimeType' in obj:
+ # we know this is a new item fetched from the api, because the cache does not store this field
+ self.add_item_to_cache(
+ obj['id'], obj['name'], [] if 'parents' not in obj else obj['parents'])
+ new_cache_entries += 1
+
+ if path.strip() == '':
+ path = obj['name']
+ else:
+ path = os.path.join(obj['name'], path)
+
+ if 'parents' in obj and obj['parents']:
+ for parent in obj['parents']:
+ new_cache_entries += get_item_paths(
+ parent, path, paths, new_cache_entries, teamdrive_id)
+
+ if (not obj or 'parents' not in obj or not obj['parents']) and len(path):
+ paths.append(path)
+ return new_cache_entries
+ return new_cache_entries
+
+ added_to_cache += get_item_paths(item_id, '',
+ file_paths, added_to_cache, teamdrive_id)
+ if added_to_cache:
+ logger.debug("Dumping cache due to new entries!")
+ self.dump_cache()
+
+ if len(file_paths):
+ return True, file_paths
+ else:
+ return False, file_paths
+
+ except Exception:
+ logger.exception(
+ "Exception retrieving filepaths for '%s': ", item_id)
+
+ return False, []
+
+ # cache
+ def add_item_to_cache(self, item_id, item_name, item_parents):
+ if item_id not in self.cache:
+ logger.info("Added '%s' to cache: %s", item_id, item_name)
+ self.cache[item_id] = {'name': item_name, 'parents': item_parents}
+ return
+
+ def remove_item_from_cache(self, item_id):
+ if self.cache.pop(item_id, None):
+ return True
+ return False
+
+ # dump jsons
+ def dump_token(self):
+ utils.dump_json(self.token_path, self.token)
+ return
+
+ def dump_cache(self):
+ self.cache.commit()
+ return
+
+ ############################################################
+ # INTERNALS
+ ############################################################
+
+ # cache
+ def _get_cached_metdata(self, item_id):
+ if item_id in self.cache:
+ return self.cache[item_id]
+ return None
+
+ # requests
+ @backoff.on_predicate(backoff.expo, lambda x: not x[0] and (
+ 'error' in x[2] and 'code' in x[2]['error'] and x[2]['error']['code'] != 401), max_tries=8)
+ def _make_request(self, url, headers=None, data=None, params=None, request_type='get'):
+ refreshed_token = False
+
+ while True:
+ if headers is None and self.token:
+ auth_headers = {
+ 'Authorization': 'Bearer %s' % self.token['access_token'],
+ }
+ else:
+ auth_headers = {}
+
+ resp = None
+ if request_type == 'get':
+ resp = requests.get(url, params=params, headers=headers if headers is not None else auth_headers,
+ timeout=30)
+ elif request_type == 'post':
+ resp = requests.post(url, data=data, headers=headers if headers is not None else auth_headers,
+ timeout=30)
+ else:
+ return False, resp, {
+ 'error': {'code': 401, 'message': 'Invalid request_type was supplied to _make_request'}}
+
+ # response logic
+ try:
+ data = resp.json()
+ except ValueError:
+ logger.exception("Exception while decoding response from Google Drive for data:\n%s\nTraceback: ",
+ resp.text)
+ return False, resp, {
+ 'error': {'code': resp.status_code, 'message': 'Failed to json decode Google Drive response'}}
+
+ if 'error' in data and 'code' in data['error'] and (
+ 'message' in data['error'] and 'Invalid Credentials' in data['error']['message']):
+ # the token has expired.
+ if not refreshed_token:
+ refreshed_token = True
+ self.refresh_access_token()
+ continue
+ else:
+ # attempt was already made to refresh token
+ return False, resp, data
+
+ if resp.status_code == 200:
+ return True, resp, data
+ else:
+ return False, resp, data
diff --git a/menu/pgscan/gpl3.tracking b/menu/pgscan/gpl3.tracking
new file mode 100644
index 00000000..924dc607
--- /dev/null
+++ b/menu/pgscan/gpl3.tracking
@@ -0,0 +1,5 @@
+PG Project Modifications:
+Adopting Configuration Files
+
+Project Info:
+Original Project Info: https://github.com/l3uddz/plexautoscan
diff --git a/menu/pgscan/pgscan.sh b/menu/pgscan/pgscan.sh
new file mode 100644
index 00000000..f71c1b7a
--- /dev/null
+++ b/menu/pgscan/pgscan.sh
@@ -0,0 +1,106 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+
+# KEY VARIABLE RECALL & EXECUTION
+mkdir -p /var/plexguide/pgscan
+
+# FUNCTIONS START ##############################################################
+
+# FIRST FUNCTION
+variable() {
+ file="$1"
+ if [ ! -e "$file" ]; then echo "$2" >$1; fi
+}
+
+deploycheck() {
+ dcheck=$(systemctl status pgscan | grep "\(running\)\>" | grep "\")
+ if [ "$dcheck" != "" ]; then
+ dstatus="✅ DEPLOYED"
+ else dstatus="⚠️ NOT DEPLOYED"; fi
+}
+
+plexcheck() {
+ pcheck=$(docker ps | grep "\")
+ if [ "$pcheck" == "" ]; then
+
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+⛔️ WARNING! - Plex is Not Installed or Running! Exiting!
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+EOF
+ read -p 'Confirm Info | PRESS [ENTER] ' typed = config['SERVER_MAX_FILE_CHECKS']:
+ logger.warning(
+ "File '%s' exhausted all available checks, aborting scan request.", check_path)
+ # remove item from database if sqlite is enabled
+ if config['SERVER_USE_SQLITE']:
+ if db.remove_item(path):
+ logger.info("Removed '%s' from database", path)
+ time.sleep(1)
+ else:
+ logger.error("Failed removing '%s' from database", path)
+ return
+
+ else:
+ logger.info("File '%s' did not exist on check %d of %d, checking again in %s seconds.", check_path,
+ checks,
+ config['SERVER_MAX_FILE_CHECKS'],
+ config['SERVER_FILE_CHECK_DELAY'])
+ time.sleep(config['SERVER_FILE_CHECK_DELAY'])
+ # send rclone cache clear if enabled
+ if config['RCLONE_RC_CACHE_EXPIRE']['ENABLED']:
+ utils.rclone_rc_clear_cache(config, check_path)
+
+ # build plex scanner command
+ if os.name == 'nt':
+ final_cmd = '"%s" --scan --refresh --section %s --directory "%s"' \
+ % (config['PLEX_SCANNER'], str(section), scan_path)
+ else:
+ cmd = 'export LD_LIBRARY_PATH=' + config['PLEX_LD_LIBRARY_PATH'] + ';'
+ if not config['USE_DOCKER']:
+ cmd += 'export PLEX_MEDIA_SERVER_APPLICATION_SUPPORT_DIR=' + \
+ config['PLEX_SUPPORT_DIR'] + ';'
+ cmd += config['PLEX_SCANNER'] + ' --scan --refresh --section ' + str(section) + ' --directory ' + cmd_quote(
+ scan_path)
+
+ if config['USE_DOCKER']:
+ final_cmd = 'docker exec -u %s -i %s bash -c %s' % \
+ (cmd_quote(config['PLEX_USER']), cmd_quote(
+ config['DOCKER_NAME']), cmd_quote(cmd))
+ elif config['USE_SUDO']:
+ final_cmd = 'sudo -u %s bash -c %s' % (
+ config['PLEX_USER'], cmd_quote(cmd))
+ else:
+ final_cmd = cmd
+
+ # invoke plex scanner
+ priority = utils.get_priority(config, scan_path)
+ logger.debug(
+ "Waiting for turn in the scan request backlog with priority: %d", priority)
+
+ lock.acquire(priority)
+ try:
+ logger.info("Scan request is now being processed")
+ # wait for existing scanners being ran by plex
+ if config['PLEX_WAIT_FOR_EXTERNAL_SCANNERS']:
+ scanner_name = os.path.basename(
+ config['PLEX_SCANNER']).replace('\\', '')
+ if not utils.wait_running_process(scanner_name):
+ logger.warning(
+ "There was a problem waiting for existing '%s' process(s) to finish, aborting scan.", scanner_name)
+ # remove item from database if sqlite is enabled
+ if config['SERVER_USE_SQLITE']:
+ if db.remove_item(path):
+ logger.info("Removed '%s' from database", path)
+ time.sleep(1)
+ else:
+ logger.error(
+ "Failed removing '%s' from database", path)
+ return
+ else:
+ logger.info("No '%s' processes were found.", scanner_name)
+
+ # run external command before scan if supplied
+ if len(config['RUN_COMMAND_BEFORE_SCAN']) > 2:
+ logger.info("Running external command: %r",
+ config['RUN_COMMAND_BEFORE_SCAN'])
+ utils.run_command(config['RUN_COMMAND_BEFORE_SCAN'])
+ logger.info("Finished running external command")
+
+ # begin scan
+ logger.info("Starting Plex Scanner")
+ logger.debug(final_cmd)
+ utils.run_command(final_cmd.encode("utf-8"))
+ logger.info("Finished scan!")
+
+ # remove item from database if sqlite is enabled
+ if config['SERVER_USE_SQLITE']:
+ if db.remove_item(path):
+ logger.info("Removed '%s' from database", path)
+ time.sleep(1)
+ logger.info("There is %d queued items remaining...",
+ db.queued_count())
+ else:
+ logger.error("Failed removing '%s' from database", path)
+
+ # empty trash if configured
+ if config['PLEX_EMPTY_TRASH'] and config['PLEX_TOKEN'] and config['PLEX_EMPTY_TRASH_MAX_FILES']:
+ logger.info("Checking deleted item count in 10 seconds...")
+ time.sleep(10)
+
+ # check deleted item count, don't proceed if more than this value
+ deleted_items = get_deleted_count(config)
+ if deleted_items > config['PLEX_EMPTY_TRASH_MAX_FILES']:
+ logger.warning("There were %d deleted files, skipping emptying trash for section %s", deleted_items,
+ section)
+ elif deleted_items == -1:
+ logger.error(
+ "Could not determine deleted item count, aborting emptying trash")
+ elif not config['PLEX_EMPTY_TRASH_ZERO_DELETED'] and not deleted_items and scan_type != 'Upgrade':
+ logger.info(
+ "Skipping emptying trash as there were no deleted items")
+ else:
+ logger.info(
+ "Emptying trash to clear %d deleted items", deleted_items)
+ empty_trash(config, str(section))
+
+ # analyze movie/episode
+ if config['PLEX_ANALYZE_TYPE'].lower() != 'off' and not scan_path_is_directory:
+ logger.debug("Sleeping 10 seconds before sending analyze request")
+ time.sleep(10)
+ analyze_item(config, path)
+
+ # run external command after scan if supplied
+ if len(config['RUN_COMMAND_AFTER_SCAN']) > 2:
+ logger.info("Running external command: %r",
+ config['RUN_COMMAND_AFTER_SCAN'])
+ utils.run_command(config['RUN_COMMAND_AFTER_SCAN'])
+ logger.info("Finished running external command")
+
+ except Exception:
+ logger.exception(
+ "Unexpected exception occurred while processing: '%s'", scan_path)
+ finally:
+ lock.release()
+ return
+
+
+def show_sections(config):
+ if os.name == 'nt':
+ final_cmd = '""%s" --list"' % config['PLEX_SCANNER']
+ else:
+ cmd = 'export LD_LIBRARY_PATH=' + config['PLEX_LD_LIBRARY_PATH'] + ';'
+ if not config['USE_DOCKER']:
+ cmd += 'export PLEX_MEDIA_SERVER_APPLICATION_SUPPORT_DIR=' + \
+ config['PLEX_SUPPORT_DIR'] + ';'
+ cmd += config['PLEX_SCANNER'] + ' --list'
+
+ if config['USE_DOCKER']:
+ final_cmd = 'docker exec -u %s -it %s bash -c %s' % (
+ cmd_quote(config['PLEX_USER']), cmd_quote(config['DOCKER_NAME']), cmd_quote(cmd))
+ elif config['USE_SUDO']:
+ final_cmd = 'sudo -u %s bash -c "%s"' % (config['PLEX_USER'], cmd)
+ else:
+ final_cmd = cmd
+ logger.info("Using Plex Scanner")
+ logger.debug(final_cmd)
+ os.system(final_cmd)
+
+
+def analyze_item(config, scan_path):
+ if not os.path.exists(config['PLEX_DATABASE_PATH']):
+ logger.info(
+ "Could not analyze '%s' because plex database could not be found?", scan_path)
+ return
+ # get files metadata_item_id
+ metadata_item_ids = get_file_metadata_ids(config, scan_path)
+ if metadata_item_ids is None or not len(metadata_item_ids):
+ logger.info(
+ "Aborting analyze of '%s' because could not find any metadata_item_id for it", scan_path)
+ return
+ metadata_item_id = ','.join(str(x) for x in metadata_item_ids)
+
+ # build plex analyze command
+ analyze_type = 'analyze-deeply' if config['PLEX_ANALYZE_TYPE'].lower(
+ ) == 'deep' else 'analyze'
+ if os.name == 'nt':
+ final_cmd = '"%s" --%s --item %s' % (
+ config['PLEX_SCANNER'], analyze_type, metadata_item_id)
+ else:
+ cmd = 'export LD_LIBRARY_PATH=' + config['PLEX_LD_LIBRARY_PATH'] + ';'
+ if not config['USE_DOCKER']:
+ cmd += 'export PLEX_MEDIA_SERVER_APPLICATION_SUPPORT_DIR=' + \
+ config['PLEX_SUPPORT_DIR'] + ';'
+ cmd += config['PLEX_SCANNER'] + ' --' + \
+ analyze_type + ' --item ' + metadata_item_id
+
+ if config['USE_DOCKER']:
+ final_cmd = 'docker exec -u %s -i %s bash -c %s' % \
+ (cmd_quote(config['PLEX_USER']), cmd_quote(
+ config['DOCKER_NAME']), cmd_quote(cmd))
+ elif config['USE_SUDO']:
+ final_cmd = 'sudo -u %s bash -c %s' % (
+ config['PLEX_USER'], cmd_quote(cmd))
+ else:
+ final_cmd = cmd
+
+ # begin analysis
+ logger.info("Starting %s analysis of metadata_item(s): %s",
+ 'deep' if config['PLEX_ANALYZE_TYPE'].lower() == 'deep' else 'basic', metadata_item_id)
+ logger.debug(final_cmd)
+ utils.run_command(final_cmd.encode("utf-8"))
+ logger.info("Finished %s analysis of metadata_item(s): %s!",
+ 'deep' if config['PLEX_ANALYZE_TYPE'].lower() == 'deep' else 'basic', metadata_item_id)
+
+
+def get_file_metadata_ids(config, file_path):
+ results = []
+ media_item_row = None
+
+ try:
+ with sqlite3.connect(config['PLEX_DATABASE_PATH']) as conn:
+ conn.row_factory = sqlite3.Row
+ with closing(conn.cursor()) as c:
+ # query media_parts to retrieve media_item_row for this file
+ for x in range(5):
+ media_item_row = c.execute(
+ "SELECT * FROM media_parts WHERE file=?", (file_path,)).fetchone()
+ if media_item_row:
+ logger.info(
+ "Found row in media_parts where file = '%s' after %d/5 tries!", file_path, x + 1)
+ break
+ else:
+ logger.error("Could not locate record in media_parts where file = '%s', %d/5 attempts...",
+ file_path, x + 1)
+ time.sleep(10)
+
+ if not media_item_row:
+ logger.error(
+ "Could not locate record in media_parts where file = '%s' after 5 tries...", file_path)
+ return None
+
+ media_item_id = media_item_row['media_item_id']
+ if media_item_id and int(media_item_id):
+ # query db to find metadata_item_id
+ metadata_item_id = \
+ c.execute("SELECT * FROM media_items WHERE id=?", (int(media_item_id),)).fetchone()[
+ 'metadata_item_id']
+ if metadata_item_id and int(metadata_item_id):
+ logger.debug("Found metadata_item_id for '%s': %d",
+ file_path, int(metadata_item_id))
+
+ # query db to find parent_id of metadata_item_id
+ if config['PLEX_ANALYZE_DIRECTORY']:
+ parent_id = \
+ c.execute("SELECT * FROM metadata_items WHERE id=?",
+ (int(metadata_item_id),)).fetchone()['parent_id']
+ if not parent_id or not int(parent_id):
+ # could not find parent_id of this item, likely its a movie...
+ # lets just return the metadata_item_id
+ return [int(metadata_item_id)]
+ logger.debug(
+ "Found parent_id for '%s': %d", file_path, int(parent_id))
+
+ # if mode is basic, single parent_id is enough
+ if config['PLEX_ANALYZE_TYPE'].lower() == 'basic':
+ return [int(parent_id)]
+
+ # lets find all metadata_item_id's with this parent_id for use with deep analyze
+ metadata_items = c.execute("SELECT * FROM metadata_items WHERE parent_id=?",
+ (int(parent_id),)).fetchall()
+ if not metadata_items:
+ # could not find any results, lets just return metadata_item_id
+ return [int(metadata_item_id)]
+
+ for row in metadata_items:
+ if row['id'] and int(row['id']) and int(row['id']) not in results:
+ results.append(int(row['id']))
+
+ logger.debug(
+ "Found media_item_id's for '%s': %s", file_path, results)
+ logger.info("Found %d media_item_id's to deep analyze for: '%s'", len(
+ results), file_path)
+ else:
+ # user had PLEX_ANALYZE_DIRECTORY as False - lets just scan the single metadata_item_id
+ results.append(int(metadata_item_id))
+
+ except Exception as ex:
+ logger.exception(
+ "Exception finding metadata_item_id for '%s': ", file_path)
+ return results
+
+
+def empty_trash(config, section):
+ for control in config['PLEX_EMPTY_TRASH_CONTROL_FILES']:
+ if not os.path.exists(control):
+ logger.info(
+ "Skipping emptying trash as control file does not exist: '%s'", control)
+ return
+
+ if len(config['PLEX_EMPTY_TRASH_CONTROL_FILES']):
+ logger.info("Control file(s) exist!")
+
+ for x in range(5):
+ try:
+ resp = requests.put('%s/library/sections/%s/emptyTrash?X-Plex-Token=%s' % (
+ config['PLEX_LOCAL_URL'], section, config['PLEX_TOKEN']), data=None, timeout=30)
+ if resp.status_code == 200:
+ logger.info(
+ "Trash cleared for section %s after %d/5 tries!", section, x + 1)
+ break
+ else:
+ logger.error("Unexpected response status_code for empty trash request: %d, %d/5 attempts...",
+ resp.status_code, x + 1)
+ time.sleep(10)
+ except Exception as ex:
+ logger.exception(
+ "Exception sending empty trash for section %s, %d/5 attempts: ", section, x + 1)
+ time.sleep(10)
+ return
+
+
+def get_deleted_count(config):
+ try:
+ with sqlite3.connect(config['PLEX_DATABASE_PATH']) as conn:
+ with closing(conn.cursor()) as c:
+ deleted_metadata = \
+ c.execute(
+ 'SELECT count(*) FROM metadata_items WHERE deleted_at IS NOT NULL').fetchone()[0]
+ deleted_media_parts = \
+ c.execute(
+ 'SELECT count(*) FROM media_parts WHERE deleted_at IS NOT NULL').fetchone()[0]
+
+ return int(deleted_metadata) + int(deleted_media_parts)
+
+ except Exception as ex:
+ logger.exception(
+ "Exception retrieving deleted item count from database: ")
+ return -1
diff --git a/menu/pgscan/requirements.txt b/menu/pgscan/requirements.txt
new file mode 100644
index 00000000..4b9c9b2f
--- /dev/null
+++ b/menu/pgscan/requirements.txt
@@ -0,0 +1,6 @@
+flask>=1.0.0
+psutil ~= 5.4.0
+requests ~= 2.20.0
+peewee ~= 2.10.1
+backoff ~= 1.5.0
+sqlitedict ~= 1.5.0
diff --git a/menu/pgscan/scan.py b/menu/pgscan/scan.py
new file mode 100644
index 00000000..b4696f81
--- /dev/null
+++ b/menu/pgscan/scan.py
@@ -0,0 +1,591 @@
+#!/usr/bin/env python2.7
+from gdrive import Gdrive
+import utils
+import plex
+import db
+import json
+import logging
+import os
+import sys
+import time
+from copy import copy
+from logging.handlers import RotatingFileHandler
+
+from flask import Flask
+from flask import abort
+from flask import jsonify
+from flask import request
+
+# Get config
+import config
+import threads
+
+############################################################
+# INIT
+############################################################
+
+# Logging
+logFormatter = logging.Formatter(
+ '%(asctime)24s - %(levelname)8s - %(name)9s [%(thread)5d]: %(message)s')
+rootLogger = logging.getLogger()
+rootLogger.setLevel(logging.INFO)
+
+# Decrease modules logging
+logging.getLogger('requests').setLevel(logging.ERROR)
+logging.getLogger('werkzeug').setLevel(logging.ERROR)
+logging.getLogger('peewee').setLevel(logging.ERROR)
+logging.getLogger('urllib3.connectionpool').setLevel(logging.ERROR)
+logging.getLogger('sqlitedict').setLevel(logging.ERROR)
+
+# Console logger, log to stdout instead of stderr
+consoleHandler = logging.StreamHandler(sys.stdout)
+consoleHandler.setFormatter(logFormatter)
+rootLogger.addHandler(consoleHandler)
+
+# Load initial config
+conf = config.Config()
+
+# File logger
+fileHandler = RotatingFileHandler(
+ conf.settings['logfile'],
+ maxBytes=1024 * 1024 * 5,
+ backupCount=5,
+ encoding='utf-8'
+)
+fileHandler.setFormatter(logFormatter)
+rootLogger.addHandler(fileHandler)
+
+# Set configured log level
+rootLogger.setLevel(conf.settings['loglevel'])
+# Load config file
+conf.load()
+
+# Scan logger
+logger = rootLogger.getChild("AUTOSCAN")
+
+# Multiprocessing
+thread = threads.Thread()
+scan_lock = threads.PriorityLock()
+resleep_paths = []
+
+# local imports
+
+google = None
+
+
+############################################################
+# QUEUE PROCESSOR
+############################################################
+
+
+def queue_processor():
+ logger.info("Starting queue processor in 10 seconds")
+ try:
+ time.sleep(10)
+ logger.info("Queue processor started")
+ db_scan_requests = db.get_all_items()
+ items = 0
+ for db_item in db_scan_requests:
+ thread.start(plex.scan, args=[conf.configs, scan_lock, db_item['scan_path'], db_item['scan_for'],
+ db_item['scan_section'],
+ db_item['scan_type'], resleep_paths])
+ items += 1
+ time.sleep(2)
+ logger.info("Restored %d scan requests from database", items)
+ except Exception:
+ logger.exception(
+ "Exception while processing scan requests from database: ")
+ return
+
+
+############################################################
+# FUNCS
+############################################################
+
+
+def start_scan(path, scan_for, scan_type):
+ section = utils.get_plex_section(conf.configs, path)
+ if section <= 0:
+ return False
+ else:
+ logger.debug("Using section id: %d for '%s'", section, path)
+
+ if conf.configs['SERVER_USE_SQLITE']:
+ db_exists, db_file = db.exists_file_root_path(path)
+ if not db_exists and db.add_item(path, scan_for, section, scan_type):
+ logger.info("Added '%s' to database, proceeding with scan", path)
+ else:
+ logger.info(
+ "Already processing '%s' from same folder, aborting adding an extra scan request to the queue", db_file)
+ resleep_paths.append(db_file)
+ return False
+
+ thread.start(plex.scan, args=[
+ conf.configs, scan_lock, path, scan_for, section, scan_type, resleep_paths])
+ return True
+
+
+def start_queue_reloader():
+ thread.start(queue_processor)
+ return True
+
+
+def start_google_monitor():
+ thread.start(thread_google_monitor)
+ return True
+
+
+############################################################
+# GOOGLE DRIVE
+############################################################
+
+def process_google_changes(changes):
+ global google
+ file_paths = []
+
+ # convert changes to file paths list
+ for change in changes:
+ logger.debug("Processing Google change: %s", change)
+ if 'file' in change and 'fileId' in change:
+ # dont consider trashed/removed events for processing
+ if ('trashed' in change['file'] and change['file']['trashed']) or (
+ 'removed' in change and change['removed']):
+ # remove item from cache
+ if google.remove_item_from_cache(change['fileId']):
+ logger.info("Removed '%s' from cache: %s",
+ change['fileId'], change['file']['name'])
+ continue
+
+ # we always want to add changes to the cache so renames etc can be reflected inside the cache
+ google.add_item_to_cache(change['fileId'], change['file']['name'],
+ [] if 'parents' not in change['file'] else change['file']['parents'])
+
+ # dont process folder events
+ if 'mimeType' in change['file'] and 'vnd.google-apps.folder' in change['file']['mimeType']:
+ # ignore this change as we dont want to scan folders
+ continue
+
+ # get this files paths
+ success, item_paths = google.get_id_file_paths(change['fileId'],
+ change['file']['teamDriveId'] if 'teamDriveId' in change[
+ 'file'] else None)
+ if success and len(item_paths):
+ file_paths.extend(item_paths)
+ elif 'teamDrive' in change and 'teamDriveId' in change:
+ # this is a teamdrive change
+ # dont consider trashed/removed events for processing
+ if 'removed' in change and change['removed']:
+ # remove item from cache
+ if google.remove_item_from_cache(change['teamDriveId']):
+ logger.info("Removed teamDrive '%s' from cache: %s", change['teamDriveId'],
+ change['teamDrive']['name'] if 'name' in change['teamDrive'] else 'Unknown teamDrive')
+ continue
+
+ if 'id' in change['teamDrive'] and 'name' in change['teamDrive']:
+ # we always want to add changes to the cache so renames etc can be reflected inside the cache
+ google.add_item_to_cache(
+ change['teamDrive']['id'], change['teamDrive']['name'], [])
+ continue
+
+ # always dump the cache after running changes
+ google.dump_cache()
+
+ # remove files that are not of an allowed extension type
+ removed_rejected_extensions = 0
+ for file_path in copy(file_paths):
+ if not utils.allowed_scan_extension(file_path, conf.configs['GDRIVE']['SCAN_EXTENSIONS']):
+ # this file did not have an allowed extension, remove it
+ file_paths.remove(file_path)
+ removed_rejected_extensions += 1
+
+ if removed_rejected_extensions:
+ logger.info("Ignored %d file(s) from Google Drive changes for disallowed file extensions",
+ removed_rejected_extensions)
+
+ # remove files that are in the ignore paths list
+ removed_rejected_paths = 0
+ for file_path in copy(file_paths):
+ for ignore_path in conf.configs['GDRIVE']['IGNORE_PATHS']:
+ if file_path.lower().startswith(ignore_path.lower()):
+ # this file was from an ignored path, remove it
+ file_paths.remove(file_path)
+ removed_rejected_paths += 1
+
+ if removed_rejected_paths:
+ logger.info("Ignored %d file(s) from Google Drive changes for disallowed file paths",
+ removed_rejected_paths)
+
+ # remove files that already exist in the plex database
+ removed_rejected_exists = utils.remove_files_exist_in_plex_database(
+ file_paths, conf.configs['PLEX_DATABASE_PATH'])
+
+ if removed_rejected_exists:
+ logger.info("Ignored %d file(s) from Google Drive changes for already being in Plex!",
+ removed_rejected_exists)
+
+ # process the file_paths list
+ if len(file_paths):
+ logger.info("Proceeding with scan of %d file(s) from Google Drive changes: %s", len(
+ file_paths), file_paths)
+
+ # loop each file, remapping and starting a scan thread
+ for file_path in file_paths:
+ final_path = utils.map_pushed_path(conf.configs, file_path)
+ start_scan(final_path, 'Google Drive', 'Download')
+
+ return True
+
+
+def thread_google_monitor():
+ global google
+
+ logger.info("Starting Google Drive changes monitor in 30 seconds...")
+ time.sleep(30)
+
+ # load access tokens
+ google = Gdrive(
+ conf.configs, conf.settings['tokenfile'], conf.settings['cachefile'])
+ if not google.first_run():
+ logger.error("Failed to retrieve Google Drive access tokens...")
+ exit(1)
+ else:
+ logger.info("Google Drive access tokens were successfully loaded")
+
+ try:
+
+ logger.info("Google Drive changes monitor started")
+ while True:
+ if not google.token['page_token']:
+ # we have no page_token, likely this is first run, lets retrieve a starting page token
+ if not google.get_changes_first_page_token():
+ logger.error(
+ "Failed to retrieve starting Google Drive changes page token...")
+ return
+ else:
+ logger.info(
+ "Retrieved starting Google Drive changes page token: %s", google.token['page_token'])
+ time.sleep(conf.configs['GDRIVE']['POLL_INTERVAL'])
+
+ # get page changes
+ changes = []
+ changes_attempts = 0
+
+ while True:
+ try:
+ success, page = google.get_changes()
+ changes_attempts = 0
+ except Exception:
+ changes_attempts += 1
+ logger.exception(
+ "Exception occurred while polling Google Drive for changes on page %s on attempt %d/12: ",
+ str(google.token['page_token']), changes_attempts)
+
+ if changes_attempts < 12:
+ logger.warning(
+ "Sleeping for 5 minutes before trying to poll Google Drive for changes again...")
+ time.sleep(60 * 5)
+ continue
+ else:
+ logger.error("Failed to poll Google Drive changes after 12 consecutive attempts, "
+ "aborting...")
+ return
+
+ if not success:
+ logger.error("Failed to retrieve Google Drive changes for page: %s, aborting...",
+ str(google.token['page_token']))
+ return
+ else:
+ # successfully retrieved some changes
+ if 'changes' in page:
+ changes.extend(page['changes'])
+
+ # page logic
+ if page is not None and 'nextPageToken' in page:
+ # there are more pages to retrieve
+ logger.debug(
+ "There are more Google Drive changes pages to retrieve, retrieving next page...")
+ continue
+ elif page is not None and 'newStartPageToken' in page:
+ # there are no more pages to retrieve
+ break
+ else:
+ logger.error("There was an unexpected outcome when polling Google Drive for changes, "
+ "aborting future polls...")
+ return
+
+ # process changes
+ if len(changes):
+ logger.info(
+ "There's %d Google Drive change(s) to process", len(changes))
+ process_google_changes(changes)
+
+ # sleep before polling for changes again
+ time.sleep(conf.configs['GDRIVE']['POLL_INTERVAL'])
+
+ except Exception:
+ logger.exception("Fatal Exception occurred while monitoring Google Drive for changes, page = %s: ",
+ google.token['page_token'])
+
+
+############################################################
+# SERVER
+############################################################
+
+app = Flask(__name__)
+app.config['JSON_AS_ASCII'] = False
+
+
+@app.route("/api/%s" % conf.configs['SERVER_PASS'], methods=['GET', 'POST'])
+def api_call():
+ data = {}
+ try:
+ if request.content_type == 'application/json':
+ data = request.get_json(silent=True)
+ elif request.method == 'POST':
+ data = request.form.to_dict()
+ else:
+ data = request.args.to_dict()
+
+ # verify cmd was supplied
+ if 'cmd' not in data:
+ logger.error("Unknown %s API call from %r",
+ request.method, request.remote_addr)
+ return jsonify({'error': 'No cmd parameter was supplied'})
+ else:
+ logger.info("Client %s API call from %r, type: %s",
+ request.method, request.remote_addr, data['cmd'])
+
+ # process cmds
+ cmd = data['cmd'].lower()
+ if cmd == 'queue_count':
+ # queue count
+ if not conf.configs['SERVER_USE_SQLITE']:
+ # return error if SQLITE db is not enabled
+ return jsonify({'error': 'SERVER_USE_SQLITE must be enabled'})
+ return jsonify({'queue_count': db.get_queue_count()})
+
+ else:
+ # unknown cmd
+ return jsonify({'error': 'Unknown cmd: %s' % cmd})
+
+ except Exception:
+ logger.exception("Exception parsing %s API call from %r: ",
+ request.method, request.remote_addr)
+
+ return jsonify({'error': 'Unexpected error occurred, check logs...'})
+
+
+@app.route("/%s" % conf.configs['SERVER_PASS'], methods=['GET'])
+def manual_scan():
+ if not conf.configs['SERVER_ALLOW_MANUAL_SCAN']:
+ return abort(401)
+ page = """
+
+
+ Plex Autoscan
+
+
+
+
+
+
+
+
Plex Autoscan
+
+
Path to scan
+
+
Clicking Submit will add the path to the scan queue.
+
+
+
+
+ """
+ return page, 200
+
+
+@app.route("/%s" % conf.configs['SERVER_PASS'], methods=['POST'])
+def client_pushed():
+ if request.content_type == 'application/json':
+ data = request.get_json(silent=True)
+ else:
+ data = request.form.to_dict()
+
+ if not data:
+ logger.error("Invalid scan request from: %r", request.remote_addr)
+ abort(400)
+ logger.debug("Client %r request dump:\n%s", request.remote_addr,
+ json.dumps(data, indent=4, sort_keys=True))
+
+ if ('eventType' in data and data['eventType'] == 'Test') or ('EventType' in data and data['EventType'] == 'Test'):
+ logger.info("Client %r made a test request, event: '%s'",
+ request.remote_addr, 'Test')
+ elif 'eventType' in data and data['eventType'] == 'Manual':
+ logger.info("Client %r made a manual scan request for: '%s'",
+ request.remote_addr, data['filepath'])
+ final_path = utils.map_pushed_path(conf.configs, data['filepath'])
+ # ignore this request?
+ ignore, ignore_match = utils.should_ignore(final_path, conf.configs)
+ if ignore:
+ logger.info("Ignored scan request for '%s' because '%s' was matched from SERVER_IGNORE_LIST", final_path,
+ ignore_match)
+ return "Ignoring scan request because %s was matched from your SERVER_IGNORE_LIST" % ignore_match
+ if start_scan(final_path, 'Manual', 'Manual'):
+ return """
+
+
+ Plex Autoscan
+
+
+
+
+
+ '{0}' has already been added to the scan queue.
+
+
+
+
+
+ """.format(data['filepath'])
+
+ elif 'series' in data and 'eventType' in data and data['eventType'] == 'Rename' and 'path' in data['series']:
+ # sonarr Rename webhook
+ logger.info("Client %r scan request for series: '%s', event: '%s'", request.remote_addr, data['series']['path'],
+ "Upgrade" if ('isUpgrade' in data and data['isUpgrade']) else data['eventType'])
+ final_path = utils.map_pushed_path(
+ conf.configs, data['series']['path'])
+ start_scan(final_path, 'Sonarr',
+ "Upgrade" if ('isUpgrade' in data and data['isUpgrade']) else data['eventType'])
+
+ elif 'movie' in data and 'eventType' in data and data['eventType'] == 'Rename' and 'folderPath' in data['movie']:
+ # radarr Rename webhook
+ logger.info("Client %r scan request for movie: '%s', event: '%s'", request.remote_addr,
+ data['movie']['folderPath'],
+ "Upgrade" if ('isUpgrade' in data and data['isUpgrade']) else data['eventType'])
+ final_path = utils.map_pushed_path(
+ conf.configs, data['movie']['folderPath'])
+ start_scan(final_path, 'Radarr',
+ "Upgrade" if ('isUpgrade' in data and data['isUpgrade']) else data['eventType'])
+
+ elif 'movie' in data and 'movieFile' in data and 'folderPath' in data['movie'] and \
+ 'relativePath' in data['movieFile'] and 'eventType' in data:
+ # radarr download/upgrade webhook
+ path = os.path.join(data['movie']['folderPath'],
+ data['movieFile']['relativePath'])
+ logger.info("Client %r scan request for movie: '%s', event: '%s'", request.remote_addr, path,
+ "Upgrade" if ('isUpgrade' in data and data['isUpgrade']) else data['eventType'])
+ final_path = utils.map_pushed_path(conf.configs, path)
+ start_scan(final_path, 'Radarr',
+ "Upgrade" if ('isUpgrade' in data and data['isUpgrade']) else data['eventType'])
+
+ elif 'series' in data and 'episodeFile' in data and 'eventType' in data:
+ # sonarr download/upgrade webhook
+ path = os.path.join(data['series']['path'],
+ data['episodeFile']['relativePath'])
+ logger.info("Client %r scan request for series: '%s', event: '%s'", request.remote_addr, path,
+ "Upgrade" if ('isUpgrade' in data and data['isUpgrade']) else data['eventType'])
+ final_path = utils.map_pushed_path(conf.configs, path)
+ start_scan(final_path, 'Sonarr',
+ "Upgrade" if ('isUpgrade' in data and data['isUpgrade']) else data['eventType'])
+
+ elif 'artist' in data and 'trackFile' in data and 'eventType' in data:
+ # lidarr download/upgrade webhook
+ path = os.path.join(data['artist']['path'],
+ data['trackFile']['relativePath'])
+ logger.info("Client %r scan request for album track: '%s', event: '%s'", request.remote_addr, path,
+ "Upgrade" if ('isUpgrade' in data and data['isUpgrade']) else data['eventType'])
+ final_path = utils.map_pushed_path(conf.configs, path)
+ start_scan(final_path, 'Lidarr',
+ "Upgrade" if ('isUpgrade' in data and data['isUpgrade']) else data['eventType'])
+
+ else:
+ logger.error("Unknown scan request from: %r", request.remote_addr)
+ abort(400)
+
+ return "OK"
+
+
+############################################################
+# MAIN
+############################################################
+
+if __name__ == "__main__":
+ logger.info("""
+PG Scan Started!
+""")
+ if conf.args['cmd'] == 'sections':
+ plex.show_sections(conf.configs)
+
+ exit(0)
+ elif conf.args['cmd'] == 'update_sections':
+ plex.updateSectionMappings(conf)
+ elif conf.args['cmd'] == 'authorize':
+ if not conf.configs['GDRIVE']['ENABLED']:
+ logger.error(
+ "You must enable the ENABLED setting in the GDRIVE config section...")
+ exit(1)
+ else:
+ google = Gdrive(
+ conf.configs, conf.settings['tokenfile'], conf.settings['cachefile'])
+ if not google.first_run():
+ logger.error("Failed to retrieve access tokens...")
+ exit(1)
+ else:
+ logger.info("Access tokens were successfully retrieved!")
+ exit(0)
+
+ elif conf.args['cmd'] == 'server':
+ if conf.configs['SERVER_USE_SQLITE']:
+ start_queue_reloader()
+
+ if conf.configs['GDRIVE']['ENABLED']:
+ if not os.path.exists(conf.settings['tokenfile']):
+ logger.error(
+ "You must authorize your Google Drive account with the authorize option...")
+ exit(1)
+ start_google_monitor()
+
+ logger.info("Starting server: http://%s:%d/%s",
+ conf.configs['SERVER_IP'],
+ conf.configs['SERVER_PORT'],
+ conf.configs['SERVER_PASS']
+ )
+ app.run(host=conf.configs['SERVER_IP'],
+ port=conf.configs['SERVER_PORT'], debug=False, use_reloader=False)
+ logger.info("Server stopped")
+ exit(0)
+ else:
+ logger.error("Unknown command...")
+ exit(1)
diff --git a/menu/pgscan/system/pgscan.service b/menu/pgscan/system/pgscan.service
new file mode 100644
index 00000000..0cf07582
--- /dev/null
+++ b/menu/pgscan/system/pgscan.service
@@ -0,0 +1,17 @@
+# /etc/systemd/system/pgscan.service
+
+[Unit]
+Description=Plex Autoscan
+After=network-online.target unionfs.service
+
+[Service]
+User=1000
+Group=1000
+Type=simple
+WorkingDirectory=/opt/appdata/pgscan/
+ExecStart=/opt/appdata/pgscan/scan.py server --loglevel=INFO
+Restart=always
+RestartSec=10
+
+[Install]
+WantedBy=default.target
diff --git a/menu/pgscan/threads.py b/menu/pgscan/threads.py
new file mode 100644
index 00000000..0310f096
--- /dev/null
+++ b/menu/pgscan/threads.py
@@ -0,0 +1,61 @@
+import Queue
+import copy
+import threading
+
+
+class PriorityLock:
+ def __init__(self):
+ self._is_available = True
+ self._mutex = threading.Lock()
+ self._waiter_queue = Queue.PriorityQueue()
+
+ def acquire(self, priority=0):
+ self._mutex.acquire()
+ # First, just check the lock.
+ if self._is_available:
+ self._is_available = False
+ self._mutex.release()
+ return True
+ event = threading.Event()
+ self._waiter_queue.put((priority, event))
+ self._mutex.release()
+ event.wait()
+ # When the event is triggered, we have the lock.
+ return True
+
+ def release(self):
+ self._mutex.acquire()
+ # Notify the next thread in line, if any.
+ try:
+ _, event = self._waiter_queue.get_nowait()
+ except Queue.Empty:
+ self._is_available = True
+ else:
+ event.set()
+ self._mutex.release()
+
+
+class Thread:
+ def __init__(self):
+ self.threads = []
+
+ def start(self, target, name=None, args=None, track=False):
+ thread = threading.Thread(
+ target=target, name=name, args=args if args else [])
+ thread.daemon = True
+ thread.start()
+ if track:
+ self.threads.append(thread)
+ return thread
+
+ def join(self):
+ for thread in copy.copy(self.threads):
+ thread.join()
+ self.threads.remove(thread)
+ return
+
+ def kill(self):
+ for thread in copy.copy(self.threads):
+ thread.kill()
+ self.threads.remove(thread)
+ return
diff --git a/menu/pgscan/utils.py b/menu/pgscan/utils.py
new file mode 100644
index 00000000..b75d228f
--- /dev/null
+++ b/menu/pgscan/utils.py
@@ -0,0 +1,243 @@
+import json
+import logging
+import os
+import sqlite3
+import subprocess
+import sys
+import time
+from contextlib import closing
+from copy import copy
+
+import requests
+
+try:
+ from urlparse import urljoin
+except ImportError:
+ from urllib.parse import urljoin
+
+import psutil
+
+logger = logging.getLogger("UTILS")
+
+
+def get_plex_section(config, path):
+ for section, mappings in config['PLEX_SECTION_PATH_MAPPINGS'].items():
+ for mapping in mappings:
+ if mapping.lower() in path.lower():
+ return int(section)
+ logger.error("Unable to map '%s' to a section id....", path)
+ return -1
+
+
+def map_pushed_path(config, path):
+ for mapped_path, mappings in config['SERVER_PATH_MAPPINGS'].items():
+ for mapping in mappings:
+ if mapping in path:
+ logger.debug("Mapping '%s' to '%s'", mapping, mapped_path)
+ return path.replace(mapping, mapped_path)
+ return path
+
+
+def map_pushed_path_file_exists(config, path):
+ for mapped_path, mappings in config['SERVER_FILE_EXIST_PATH_MAPPINGS'].items():
+ for mapping in mappings:
+ if mapping in path:
+ logger.debug("Mapping file check path '%s' to '%s'",
+ mapping, mapped_path)
+ return path.replace(mapping, mapped_path)
+ return path
+
+
+def map_file_exists_path_for_rclone(config, path):
+ for mapped_path, mappings in config['RCLONE_RC_CACHE_EXPIRE']['FILE_EXISTS_TO_REMOTE_MAPPINGS'].items():
+ for mapping in mappings:
+ if mapping in path:
+ logger.debug(
+ "Mapping file check path '%s' to '%s' for rclone cache clear", mapping, mapped_path)
+ return path.replace(mapping, mapped_path)
+ return path
+
+
+def is_process_running(process_name):
+ try:
+ for process in psutil.process_iter():
+ if process.name().lower() == process_name.lower():
+ return True, process
+
+ return False, None
+ except psutil.ZombieProcess:
+ return False, None
+ except Exception:
+ logger.exception(
+ "Exception checking for process: '%s': ", process_name)
+ return False, None
+
+
+def wait_running_process(process_name):
+ try:
+ running, process = is_process_running(process_name)
+ while running and process:
+ logger.info("'%s' is running, pid: %d, cmdline: %r. Checking again in 60 seconds...", process.name(),
+ process.pid, process.cmdline())
+ time.sleep(60)
+ running, process = is_process_running(process_name)
+
+ return True
+
+ except Exception:
+ logger.exception("Exception waiting for process: '%s'", process_name())
+
+ return False
+
+
+def run_command(command):
+ process = subprocess.Popen(
+ command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+ while True:
+ output = str(process.stdout.readline()).lstrip(
+ 'b').replace('\\n', '').strip()
+ if process.poll() is not None:
+ break
+ if output and len(output) >= 8:
+ logger.info(output)
+
+ rc = process.poll()
+ return rc
+
+
+def should_ignore(file_path, config):
+ for item in config['SERVER_IGNORE_LIST']:
+ if item.lower() in file_path.lower():
+ return True, item
+
+ return False, None
+
+
+def remove_item_from_list(item, from_list):
+ while item in from_list:
+ from_list.pop(from_list.index(item))
+ return
+
+
+def get_priority(config, scan_path):
+ try:
+ for priority, paths in config['SERVER_SCAN_PRIORITIES'].items():
+ for path in paths:
+ if path.lower() in scan_path.lower():
+ logger.debug("Using priority %d for path '%s'",
+ int(priority), scan_path)
+ return int(priority)
+ logger.debug("Using default priority 0 for path '%s'", scan_path)
+ except Exception:
+ logger.exception(
+ "Exception determining priority to use for '%s': ", scan_path)
+ return 0
+
+
+def rclone_rc_clear_cache(config, scan_path):
+ try:
+ rclone_rc_url = urljoin(
+ config['RCLONE_RC_CACHE_EXPIRE']['RC_URL'], 'cache/expire')
+
+ cache_clear_path = map_file_exists_path_for_rclone(
+ config, scan_path).lstrip(os.path.sep)
+ logger.debug("Top level cache_clear_path: '%s'", cache_clear_path)
+
+ while True:
+ last_clear_path = cache_clear_path
+ cache_clear_path = os.path.dirname(cache_clear_path)
+ if cache_clear_path == last_clear_path or not len(cache_clear_path):
+ # is the last path we tried to clear, the same as this path, if so, abort
+ logger.error("Aborting rclone cache clear for '%s' due to directory level exhaustion, last level: '%s'",
+ scan_path, last_clear_path)
+ return False
+ else:
+ last_clear_path = cache_clear_path
+
+ # send cache clear request
+ logger.info("Sending rclone cache clear for: '%s'",
+ cache_clear_path)
+ try:
+ resp = requests.post(rclone_rc_url, json={
+ 'remote': cache_clear_path}, timeout=120)
+ if '{' in resp.text and '}' in resp.text:
+ data = resp.json()
+ if 'error' in data:
+ logger.info(
+ "Failed to clear rclone cache for '%s': %s", cache_clear_path, data['error'])
+ continue
+ elif ('status' in data and 'message' in data) and data['status'] == 'ok':
+ logger.info(
+ "Successfully cleared rclone cache for '%s'", cache_clear_path)
+ return True
+
+ # abort on unexpected response (no json response, no error/status & message in returned json
+ logger.error("Unexpected rclone cache clear response from %s while trying to clear '%s': %s",
+ rclone_rc_url, cache_clear_path, resp.text)
+ break
+
+ except Exception:
+ logger.exception("Exception sending rclone cache clear to %s for '%s': ", rclone_rc_url,
+ cache_clear_path)
+ break
+
+ except Exception:
+ logger.exception(
+ "Exception clearing rclone directory cache for '%s': ", scan_path)
+ return False
+
+
+def load_json(file_path):
+ if os.path.sep not in file_path:
+ file_path = os.path.join(os.path.dirname(sys.argv[0]), file_path)
+
+ with open(file_path, 'r') as fp:
+ return json.load(fp)
+
+
+def dump_json(file_path, obj, processing=True):
+ if os.path.sep not in file_path:
+ file_path = os.path.join(os.path.dirname(sys.argv[0]), file_path)
+
+ with open(file_path, 'w') as fp:
+ if processing:
+ json.dump(obj, fp, indent=2, sort_keys=True)
+ else:
+ json.dump(obj, fp)
+ return
+
+
+def remove_files_exist_in_plex_database(file_paths, plex_db_path):
+ removed_items = 0
+ try:
+ if plex_db_path and os.path.exists(plex_db_path):
+ with sqlite3.connect(plex_db_path) as conn:
+ conn.row_factory = sqlite3.Row
+ with closing(conn.cursor()) as c:
+ for file_path in copy(file_paths):
+ # check if file exists in plex
+ file_name = os.path.basename(file_path)
+ logger.debug(
+ "Checking if '%s' exists in the plex database at '%s'", file_name, plex_db_path)
+ found_item = c.execute("SELECT * FROM media_parts WHERE file LIKE ?", ('%' + file_name,)) \
+ .fetchone()
+ if found_item:
+ logger.debug(
+ "'%s' was found in the plex media_parts table", file_name)
+ file_paths.remove(file_path)
+ removed_items += 1
+
+ except Exception:
+ logger.exception(
+ "Exception checking if %s exists in the plex database: ", file_paths)
+ return removed_items
+
+
+def allowed_scan_extension(file_path, extensions):
+ check_path = file_path.lower()
+ for ext in extensions:
+ if check_path.endswith(ext.lower()):
+ logger.debug("'%s' had allowed extension: %s", file_path, ext)
+ return True
+ logger.debug("'%s' did not have an allowed extension", file_path)
+ return False
diff --git a/menu/pgstage/pgstage.yml b/menu/pgstage/pgstage.yml
new file mode 100644
index 00000000..c88dd504
--- /dev/null
+++ b/menu/pgstage/pgstage.yml
@@ -0,0 +1,9 @@
+---
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ - name: 'Cloning PTS'
+ git:
+ repo: 'https://github.com/MrDoobPG/Install'
+ dest: '/opt/pgstage'
+ force: yes
diff --git a/menu/pgtrakt/LICENSE b/menu/pgtrakt/LICENSE
new file mode 100644
index 00000000..94a9ed02
--- /dev/null
+++ b/menu/pgtrakt/LICENSE
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/menu/pgtrakt/config.json.backup b/menu/pgtrakt/config.json.backup
new file mode 100644
index 00000000..f070f383
--- /dev/null
+++ b/menu/pgtrakt/config.json.backup
@@ -0,0 +1,114 @@
+{
+ "core": {
+ "debug": false
+ },
+ "automatic": {
+ "movies": {
+ "anticipated": 3,
+ "boxoffice": 10,
+ "interval": 24,
+ "popular": 3,
+ "trending": 2
+ },
+ "shows": {
+ "anticipated": 10,
+ "interval": 48,
+ "popular": 1,
+ "trending": 2
+ }
+ },
+ "notifications": {
+ "verbose": false
+ },
+ "filters": {
+ "shows": {
+ "allowed_countries": [
+ "us",
+ "gb",
+ "de",
+ "ca"
+ ],
+ "allowed_languages": [],
+ "blacklisted_min_runtime": 15,
+ "blacklisted_min_year": 2010,
+ "blacklisted_max_year": 2019,
+ "blacklisted_genres": [
+ "animation",
+ "game-show",
+ "talk-show",
+ "home-and-garden",
+ "children",
+ "reality",
+ "anime",
+ "news",
+ "documentary",
+ "special-interest"
+ ],
+ "blacklisted_networks": [
+ "twitch",
+ "youtube",
+ "nickelodeon",
+ "hallmark",
+ "reelzchannel",
+ "disney",
+ "cnn",
+ "cbbc",
+ "the movie network",
+ "teletoon",
+ "cartoon network",
+ "espn",
+ "fox sports",
+ "yahoo!"
+ ],
+ "disabled_for": [],
+ },
+ "movies": {
+ "allowed_countries": [
+ "us",
+ "gb",
+ "de",
+ "ca"
+ ],
+ "allowed_languages": [],
+ "blacklisted_genres": [
+ "documentary",
+ "music",
+ "short",
+ "sporting-event",
+ "film-noir",
+ "fan-film"
+ ],
+ "blacklist_title_keywords": [
+ "untitled",
+ "ufc"
+ ],
+ "blacklisted_min_runtime": 35,
+ "blacklisted_min_year": 2000,
+ "blacklisted_max_year": 2019,
+ "allowed_countries": [
+ "us",
+ "gb",
+ "de",
+ "ca"
+ ],
+ "disabled_for": [],
+ }
+ },
+ "radarr": {
+ "api_key": "",
+ "profile": "HD-1080p",
+ "url": "http://localhost:7878/",
+ "root_folder": "/movies/"
+ },
+ "sonarr": {
+ "api_key": "",
+ "profile": "HD-1080p",
+ "url": "http://localhost:8989/",
+ "root_folder": "/tv/",
+ "tags": {}
+ },
+ "trakt": {
+ "client_id": "",
+ "client_secret": ""
+ }
+}
diff --git a/menu/pgtrakt/config.json.sample b/menu/pgtrakt/config.json.sample
new file mode 100644
index 00000000..c6a9277e
--- /dev/null
+++ b/menu/pgtrakt/config.json.sample
@@ -0,0 +1,105 @@
+{
+ "core": {
+ "debug": false
+ },
+ "automatic": {
+ "movies": {
+ "anticipated": 3,
+ "boxoffice": 10,
+ "interval": 24,
+ "popular": 3,
+ "trending": 2
+ },
+ "shows": {
+ "anticipated": 10,
+ "interval": 48,
+ "popular": 1,
+ "trending": 2
+ }
+ },
+ "notifications": {
+ "verbose": false
+ },
+ "filters": {
+ "shows": {
+ "allowed_countries": [
+ "us",
+ "gb",
+ "ca",
+ "de"
+ ],
+ "allowed_languages": [],
+ "blacklisted_min_runtime": 15,
+ "blacklisted_min_year": 1980,
+ "blacklisted_max_year": 2050,
+ "blacklisted_genres": [
+ "animation",
+ "game-show",
+ "news",
+ "special-interest"
+ ],
+ "blacklisted_networks": [
+ "twitch",
+ "youtube",
+ "reelzchannel",
+ "cbbc",
+ "teletoon",
+ "espn",
+ "fox sports",
+ "yahoo!"
+ ],
+ "blacklisted_tvdb_ids": [],
+ "disabled_for": []
+ },
+ "movies": {
+ "allowed_countries": [
+ "us",
+ "gb",
+ "ca",
+ "de"
+ ],
+ "allowed_languages": [],
+ "blacklisted_genres": [
+ "short",
+ "sporting-event",
+ "film-noir",
+ "fan-film"
+ ],
+ "blacklist_title_keywords": [
+ "untitled",
+ "ufc"
+ ],
+ "blacklisted_min_runtime": 30,
+ "blacklisted_min_year": 1970,
+ "blacklisted_max_year": 2050,
+ "blacklisted_tmdb_ids": [],
+ "allowed_countries": [
+ "us",
+ "gb",
+ "ca",
+ "de"
+ ],
+ "disabled_for": []
+ }
+ },
+ "core": {
+ "debug": false
+ },
+ "radarr": {
+ "api_key": "{{rapi.stdout}}",
+ "profile": "{{rprofile.stdout}}",
+ "url": "http://0.0.0.0:7878",
+ "root_folder": "{{rpath.stdout}}"
+ },
+ "sonarr": {
+ "api_key": "{{sapi.stdout}}",
+ "profile": "{{sprofile.stdout}}",
+ "url": "http://0.0.0.0:8989",
+ "root_folder": "{{spath.stdout}}",
+ "tags": {}
+ },
+ "trakt": {
+ "client_id": "{{client.stdout}}",
+ "client_secret": "{{secret.stdout}}"
+ }
+ }
diff --git a/menu/pgtrakt/gpl3.tracking b/menu/pgtrakt/gpl3.tracking
new file mode 100644
index 00000000..118f630e
--- /dev/null
+++ b/menu/pgtrakt/gpl3.tracking
@@ -0,0 +1,5 @@
+PG Project Modifications:
+Adopting Configuration Files
+
+Project Info:
+Original Project Info: https://github.com/l3uddz/traktarr
diff --git a/menu/pgtrakt/helpers/misc.py b/menu/pgtrakt/helpers/misc.py
new file mode 100644
index 00000000..6543d68d
--- /dev/null
+++ b/menu/pgtrakt/helpers/misc.py
@@ -0,0 +1,86 @@
+from copy import copy
+
+from misc.log import logger
+
+log = logger.get_logger(__name__)
+
+
+def get_response_dict(response, key_field=None, key_value=None):
+ found_response = None
+ try:
+ if isinstance(response, list):
+ if not key_field or not key_value:
+ found_response = response[0]
+ else:
+ for result in response:
+ if isinstance(result, dict) and key_field in result and result[key_field] == key_value:
+ found_response = result
+ break
+
+ if not found_response:
+ log.error(
+ "Unable to find a result with key %s where the value is %s", key_field, key_value)
+
+ elif isinstance(response, dict):
+ found_response = response
+ else:
+ log.error("Unexpected response instance type of %s for %s",
+ type(response).__name__, response)
+
+ except Exception:
+ log.exception("Exception determining response for %s: ", response)
+ return found_response
+
+
+def backoff_handler(details):
+ log.warning("Backing off {wait:0.1f} seconds afters {tries} tries "
+ "calling function {target} with args {args} and kwargs "
+ "{kwargs}".format(**details))
+
+
+def dict_merge(dct, merge_dct):
+ for k, v in merge_dct.items():
+ import collections
+
+ if k in dct and isinstance(dct[k], dict) and isinstance(merge_dct[k], collections.Mapping):
+ dict_merge(dct[k], merge_dct[k])
+ else:
+ dct[k] = merge_dct[k]
+
+ return dct
+
+
+def unblacklist_genres(genre, blacklisted_genres):
+ genres = genre.split(',')
+ for allow_genre in genres:
+ if allow_genre in blacklisted_genres:
+ blacklisted_genres.remove(allow_genre)
+ return
+
+
+def allowed_genres(genre, object_type, trakt_object):
+ allowed_object = False
+ genres = genre.split(',')
+
+ for item in genres:
+ if item.lower() in trakt_object[object_type]['genres']:
+ allowed_object = True
+ break
+ return allowed_object
+
+
+def sorted_list(original_list, list_type, sort_key, reverse=True):
+ prepared_list = copy(original_list)
+ for item in prepared_list:
+ if not item[list_type][sort_key]:
+ if sort_key == 'released' or sort_key == 'first_aired':
+ item[list_type][sort_key] = ""
+ else:
+ item[list_type][sort_key] = 0
+
+ return sorted(prepared_list, key=lambda k: k[list_type][sort_key], reverse=reverse)
+
+
+# reference: https://stackoverflow.com/a/16712886
+def substring_after(s, delim):
+ return s.partition(delim)[2]
diff --git a/menu/pgtrakt/helpers/radarr.py b/menu/pgtrakt/helpers/radarr.py
new file mode 100644
index 00000000..32b6a917
--- /dev/null
+++ b/menu/pgtrakt/helpers/radarr.py
@@ -0,0 +1,55 @@
+from misc.log import logger
+
+log = logger.get_logger(__name__)
+
+
+def movies_to_tmdb_dict(radarr_movies):
+ movies = {}
+ try:
+ for tmp in radarr_movies:
+ if 'tmdbId' not in tmp:
+ log.debug("Could not handle movie: %s", tmp['title'])
+ continue
+ movies[tmp['tmdbId']] = tmp
+ return movies
+ except Exception:
+ log.exception("Exception processing Radarr movies to TMDB dict: ")
+ return None
+
+
+def remove_existing_movies(radarr_movies, trakt_movies, callback=None):
+ new_movies_list = []
+
+ if not radarr_movies or not trakt_movies:
+ log.error("Inappropriate parameters were supplied")
+ return None
+
+ try:
+ # turn radarr movies result into a dict with tmdb id as keys
+ processed_movies = movies_to_tmdb_dict(radarr_movies)
+ if not processed_movies:
+ return None
+
+ # loop list adding to movies that do not already exist
+ for tmp in trakt_movies:
+ if 'movie' not in tmp or 'ids' not in tmp['movie'] or 'tmdb' not in tmp['movie']['ids']:
+ log.debug(
+ "Skipping movie because it did not have required fields: %s", tmp)
+ if callback:
+ callback('movie', tmp)
+ continue
+ # check if movie exists in processed_movies
+ if tmp['movie']['ids']['tmdb'] in processed_movies:
+ log.debug("Removing existing movie: %s", tmp['movie']['title'])
+ if callback:
+ callback('movie', tmp)
+ continue
+
+ new_movies_list.append(tmp)
+
+ log.debug("Filtered %d Trakt movies to %d movies that weren't already in Radarr", len(trakt_movies),
+ len(new_movies_list))
+ return new_movies_list
+ except Exception:
+ log.exception("Exception removing existing movies from Trakt list: ")
+ return None
diff --git a/menu/pgtrakt/helpers/rating.py b/menu/pgtrakt/helpers/rating.py
new file mode 100644
index 00000000..53c6be2e
--- /dev/null
+++ b/menu/pgtrakt/helpers/rating.py
@@ -0,0 +1,33 @@
+from misc.log import logger
+import json
+import requests
+
+log = logger.get_logger(__name__)
+
+
+def get_rating(apikey, movie):
+ imdbID = movie['movie']['ids']['imdb']
+ if(imdbID):
+ log.debug("Requesting ratings from OMDB for %s (%d) | Genres: %s | Country: %s | imdbID: %s", movie['movie']['title'], movie['movie']['year'],
+ ', '.join(movie['movie']['genres']), movie['movie']['country'].upper(), imdbID)
+ r = requests.get('http://www.omdbapi.com/?i=' +
+ imdbID + '&apikey=' + apikey)
+ if(r.status_code == 200):
+ log.debug("Successfully requested ratings from OMDB for %s (%d) | Genres: %s | Country: %s | imdbID: %s",
+ movie['movie']['title'], movie['movie']['year'],
+ ', '.join(movie['movie']['genres']), movie['movie']['country'].upper(), imdbID)
+ for source in json.loads(r.text)["Ratings"]:
+ if(source['Source'] == 'Rotten Tomatoes'):
+ log.debug("Rotten Tomatoes shows rating: %s for %s (%d) | Genres: %s | Country: %s | imdbID: %s ", source['Value'], movie['movie']['title'], movie['movie']['year'],
+ ', '.join(movie['movie']['genres']), movie['movie']['country'].upper(), imdbID)
+ return int(source['Value'].split('%')[0])
+ else:
+ log.debug("Error encountered when requesting ratings from OMDB for %s (%d) | Genres: %s | Country: %s | imdbID: %s",
+ movie['movie']['title'], movie['movie']['year'],
+ ', '.join(movie['movie']['genres']), movie['movie']['country'].upper(), imdbID)
+ else:
+ log.debug("Skipping %s (%d) | Genres: %s | Country: %s as it does not have an imdbID",
+ movie['movie']['title'], movie['movie']['year'],
+ ', '.join(movie['movie']['genres']), movie['movie']['country'].upper())
+
+ return -1
diff --git a/menu/pgtrakt/helpers/sonarr.py b/menu/pgtrakt/helpers/sonarr.py
new file mode 100644
index 00000000..60a03b1d
--- /dev/null
+++ b/menu/pgtrakt/helpers/sonarr.py
@@ -0,0 +1,89 @@
+from misc.log import logger
+
+log = logger.get_logger(__name__)
+
+
+def series_tag_id_from_network(profile_tags, network_tags, network):
+ try:
+ tags = []
+ for tag_name, tag_networks in network_tags.items():
+ for tag_network in tag_networks:
+ if tag_network.lower() in network.lower() and tag_name.lower() in profile_tags:
+ log.debug("Using %s tag for network: %s",
+ tag_name, network)
+ tags.append(profile_tags[tag_name.lower()])
+ if tags:
+ return tags
+ except Exception:
+ log.exception(
+ "Exception determining tag to use for network %s: ", network)
+ return None
+
+
+def readable_tag_from_ids(profile_tag_ids, chosen_tag_ids):
+ try:
+ if not chosen_tag_ids:
+ return None
+
+ tags = []
+ for tag_name, tag_id in profile_tag_ids.items():
+ if tag_id in chosen_tag_ids:
+ tags.append(tag_name)
+ if tags:
+ return tags
+ except Exception:
+ log.exception(
+ "Exception building readable tag name list from ids %s: ", chosen_tag_ids)
+ return None
+
+
+def series_to_tvdb_dict(sonarr_series):
+ series = {}
+ try:
+ for tmp in sonarr_series:
+ if 'tvdbId' not in tmp:
+ log.debug("Could not handle show: %s", tmp['title'])
+ continue
+ series[tmp['tvdbId']] = tmp
+ return series
+ except Exception:
+ log.exception("Exception processing Sonarr shows to TVDB dict: ")
+ return None
+
+
+def remove_existing_series(sonarr_series, trakt_series, callback=None):
+ new_series_list = []
+
+ if not sonarr_series or not trakt_series:
+ log.error("Inappropriate parameters were supplied")
+ return None
+
+ try:
+ # turn sonarr series result into a dict with tvdb id as keys
+ processed_series = series_to_tvdb_dict(sonarr_series)
+ if not processed_series:
+ return None
+
+ # loop list adding to series that do not already exist
+ for tmp in trakt_series:
+ if 'show' not in tmp or 'ids' not in tmp['show'] or 'tvdb' not in tmp['show']['ids']:
+ log.debug(
+ "Skipping show because it did not have required fields: %s", tmp)
+ if callback:
+ callback('show', tmp)
+ continue
+ # check if show exists in processed_series
+ if tmp['show']['ids']['tvdb'] in processed_series:
+ log.debug("Removing existing show: %s", tmp['show']['title'])
+ if callback:
+ callback('show', tmp)
+ continue
+
+ new_series_list.append(tmp)
+
+ log.debug("Filtered %d Trakt shows to %d shows that weren't already in Sonarr", len(trakt_series),
+ len(new_series_list))
+ return new_series_list
+ except Exception:
+ log.exception("Exception removing existing shows from Trakt list: ")
+ return None
diff --git a/menu/pgtrakt/helpers/str.py b/menu/pgtrakt/helpers/str.py
new file mode 100644
index 00000000..e2784092
--- /dev/null
+++ b/menu/pgtrakt/helpers/str.py
@@ -0,0 +1,35 @@
+from misc.log import logger
+
+log = logger.get_logger(__name__)
+
+
+def get_year_from_timestamp(timestamp):
+ year = 0
+ try:
+ if not timestamp:
+ return 0
+
+ year = timestamp[:timestamp.index('-')]
+ except Exception:
+ log.exception("Exception parsing year from %s: ", timestamp)
+ return int(year) if str(year).isdigit() else 0
+
+
+def is_ascii(string):
+ try:
+ string.encode('ascii')
+ except UnicodeEncodeError:
+ return False
+ except UnicodeDecodeError:
+ return False
+ except Exception:
+ log.exception(u"Exception checking if %r was ascii: ", string)
+ return False
+ return True
+
+
+def ensure_endswith(data, endswith_key):
+ if not data.strip().endswith(endswith_key):
+ return "%s%s" % (data.strip(), endswith_key)
+ else:
+ return data
diff --git a/menu/pgtrakt/helpers/trakt.py b/menu/pgtrakt/helpers/trakt.py
new file mode 100644
index 00000000..d6f7aead
--- /dev/null
+++ b/menu/pgtrakt/helpers/trakt.py
@@ -0,0 +1,305 @@
+from helpers import str as misc_str
+from misc.log import logger
+
+log = logger.get_logger(__name__)
+
+
+def blacklisted_show_genre(show, genres):
+ blacklisted = False
+ try:
+ if not show['show']['genres']:
+ log.debug("%s was blacklisted because it had no genres",
+ show['show']['title'])
+ blacklisted = True
+ else:
+ for genre in genres:
+ if genre.lower() in show['show']['genres']:
+ log.debug("%s was blacklisted because it has genre: %s",
+ show['show']['title'], genre)
+ blacklisted = True
+ break
+
+ except Exception:
+ log.exception(
+ "Exception determining if show has a blacklisted genre %s: ", show)
+ return blacklisted
+
+
+def blacklisted_show_year(show, earliest_year, latest_year):
+ blacklisted = False
+ try:
+ year = misc_str.get_year_from_timestamp(show['show']['first_aired'])
+ if not year:
+ log.debug(
+ "%s was blacklisted due to having an unknown first_aired date", show['show']['title'])
+ blacklisted = True
+ else:
+ if year < earliest_year or year > latest_year:
+ log.debug("%s was blacklisted because it first aired in: %d",
+ show['show']['title'], year)
+ blacklisted = True
+ except Exception:
+ log.exception(
+ "Exception determining if show is within min_year and max_year range %s:", show)
+ return blacklisted
+
+
+def blacklisted_show_country(show, allowed_countries):
+ blacklisted = False
+ try:
+ if not show['show']['country']:
+ log.debug("%s was blacklisted because it had no country",
+ show['show']['title'])
+ blacklisted = True
+ else:
+ if show['show']['country'].lower() not in allowed_countries:
+ log.debug("%s was blacklisted because it's from country: %s", show['show']['title'],
+ show['show']['country'])
+ blacklisted = True
+
+ except Exception:
+ log.exception(
+ "Exception determining if show was from an allowed country %s: ", show)
+ return blacklisted
+
+
+def blacklisted_show_network(show, networks):
+ blacklisted = False
+ try:
+ if not show['show']['network']:
+ log.debug("%s was blacklisted because it had no network",
+ show['show']['title'])
+ blacklisted = True
+ else:
+ for network in networks:
+ if network.lower() in show['show']['network'].lower():
+ log.debug("%s was blacklisted because it's from network: %s", show['show']['title'],
+ show['show']['network'])
+ blacklisted = True
+ break
+
+ except Exception:
+ log.exception(
+ "Exception determining if show is from a blacklisted network %s: ", show)
+ return blacklisted
+
+
+def blacklisted_show_runtime(show, lowest_runtime):
+ blacklisted = False
+ try:
+ if not show['show']['runtime'] or not isinstance(show['show']['runtime'], int):
+ log.debug("%s was blacklisted because it had no runtime",
+ show['show']['title'])
+ blacklisted = True
+ elif int(show['show']['runtime']) < lowest_runtime:
+ log.debug("%s was blacklisted because it had a runtime of: %d", show['show']['title'],
+ show['show']['runtime'])
+ blacklisted = True
+
+ except Exception:
+ log.exception(
+ "Exception determining if show had sufficient runtime %s: ", show)
+ return blacklisted
+
+
+def blacklisted_show_id(show, blacklisted_ids):
+ blacklisted = False
+ try:
+ if not show['show']['ids']['tvdb'] or not isinstance(show['show']['ids']['tvdb'], int):
+ log.debug(
+ "%s was blacklisted because it had an invalid tvdb id", show['show']['title'])
+ blacklisted = True
+ elif show['show']['ids']['tvdb'] in blacklisted_ids:
+ log.debug("%s was blacklisted because it had a blacklisted tvdb id of: %d", show['show']['title'],
+ show['show']['ids']['tvdb'])
+ blacklisted = True
+
+ except Exception:
+ log.exception(
+ "Exception determining if show had a blacklisted tvdb id %s: ", show)
+ return blacklisted
+
+
+def is_show_blacklisted(show, blacklist_settings, ignore_blacklist, callback=None):
+ if ignore_blacklist:
+ return False
+
+ blacklisted = False
+ try:
+ if blacklisted_show_year(show, blacklist_settings.blacklisted_min_year,
+ blacklist_settings.blacklisted_max_year):
+ blacklisted = True
+ if blacklisted_show_country(show, blacklist_settings.allowed_countries):
+ blacklisted = True
+ if blacklisted_show_genre(show, blacklist_settings.blacklisted_genres):
+ blacklisted = True
+ if blacklisted_show_network(show, blacklist_settings.blacklisted_networks):
+ blacklisted = True
+ if blacklisted_show_runtime(show, blacklist_settings.blacklisted_min_runtime):
+ blacklisted = True
+ if blacklisted_show_id(show, blacklist_settings.blacklisted_tvdb_ids):
+ blacklisted = True
+
+ if blacklisted and callback:
+ callback('show', show)
+
+ except Exception:
+ log.exception(
+ "Exception determining if show was blacklisted %s: ", show)
+ return blacklisted
+
+
+def blacklisted_movie_genre(movie, genres):
+ blacklisted = False
+ try:
+ if not movie['movie']['genres']:
+ log.debug("%s was blacklisted because it had no genres",
+ movie['movie']['title'])
+ blacklisted = True
+ else:
+ for genre in genres:
+ if genre.lower() in movie['movie']['genres']:
+ log.debug("%s was blacklisted because it has genre: %s",
+ movie['movie']['title'], genre)
+ blacklisted = True
+ break
+
+ except Exception:
+ log.exception(
+ "Exception determining if movie has a blacklisted genre %s: ", movie)
+ return blacklisted
+
+
+def blacklisted_movie_year(movie, earliest_year, latest_year):
+ blacklisted = False
+ try:
+ year = movie['movie']['year']
+ if year is None or not isinstance(year, int):
+ log.debug("%s was blacklisted due to having an unknown year",
+ movie['movie']['title'])
+ blacklisted = True
+ else:
+ if int(year) < earliest_year or int(year) > latest_year:
+ log.debug("%s was blacklisted because it's year is: %d",
+ movie['movie']['title'], int(year))
+ blacklisted = True
+ except Exception:
+ log.exception(
+ "Exception determining if movie is within min_year and max_year ranger %s:", movie)
+ return blacklisted
+
+
+def blacklisted_movie_country(movie, allowed_countries):
+ blacklisted = False
+ try:
+ if not movie['movie']['country']:
+ log.debug("%s was blacklisted because it had no country",
+ movie['movie']['title'])
+ blacklisted = True
+ else:
+ if movie['movie']['country'].lower() not in allowed_countries:
+ log.debug("%s was blacklisted because it's from country: %s", movie['movie']['title'],
+ movie['movie']['country'])
+ blacklisted = True
+
+ except Exception:
+ log.exception(
+ "Exception determining if movie was from an allowed country %s: ", movie)
+ return blacklisted
+
+
+def blacklisted_movie_title(movie, blacklisted_keywords):
+ blacklisted = False
+ try:
+ if not movie['movie']['title']:
+ log.debug("Blacklisted movie because it had no title: %s", movie)
+ blacklisted = True
+ else:
+ for keyword in blacklisted_keywords:
+ if keyword.lower() in movie['movie']['title'].lower():
+ log.debug("%s was blacklisted because it had title keyword: %s",
+ movie['movie']['title'], keyword)
+ blacklisted = True
+ break
+
+ except Exception:
+ log.exception(
+ "Exception determining if movie had a blacklisted title %s: ", movie)
+ return blacklisted
+
+
+def blacklisted_movie_runtime(movie, lowest_runtime):
+ blacklisted = False
+ try:
+ if not movie['movie']['runtime'] or not isinstance(movie['movie']['runtime'], int):
+ log.debug("%s was blacklisted because it had no runtime",
+ movie['movie']['title'])
+ blacklisted = True
+ elif int(movie['movie']['runtime']) < lowest_runtime:
+ log.debug("%s was blacklisted because it had a runtime of: %d", movie['movie']['title'],
+ movie['movie']['runtime'])
+ blacklisted = True
+
+ except Exception:
+ log.exception(
+ "Exception determining if movie had sufficient runtime %s: ", movie)
+ return blacklisted
+
+
+def blacklisted_movie_id(movie, blacklisted_ids):
+ blacklisted = False
+ try:
+ if not movie['movie']['ids']['tmdb'] or not isinstance(movie['movie']['ids']['tmdb'], int):
+ log.debug(
+ "%s was blacklisted because it had an invalid tmdb id", movie['movie']['title'])
+ blacklisted = True
+ elif movie['movie']['ids']['tmdb'] in blacklisted_ids:
+ log.debug("%s was blacklisted because it had a blacklisted tmdb id of: %d", movie['movie']['title'],
+ movie['movie']['ids']['tmdb'])
+ blacklisted = True
+
+ except Exception:
+ log.exception(
+ "Exception determining if show had a blacklisted tmdb id %s: ", movie)
+ return blacklisted
+
+
+def is_movie_blacklisted(movie, blacklist_settings, ignore_blacklist, callback=None):
+ if ignore_blacklist:
+ return False
+
+ blacklisted = False
+ try:
+ if blacklisted_movie_title(movie, blacklist_settings.blacklist_title_keywords):
+ blacklisted = True
+ if blacklisted_movie_year(movie, blacklist_settings.blacklisted_min_year,
+ blacklist_settings.blacklisted_max_year):
+ blacklisted = True
+ if blacklisted_movie_country(movie, blacklist_settings.allowed_countries):
+ blacklisted = True
+ if blacklisted_movie_genre(movie, blacklist_settings.blacklisted_genres):
+ blacklisted = True
+ if blacklisted_movie_runtime(movie, blacklist_settings.blacklisted_min_runtime):
+ blacklisted = True
+ if blacklisted_movie_id(movie, blacklist_settings.blacklisted_tmdb_ids):
+ blacklisted = True
+
+ if blacklisted and callback:
+ callback('movie', movie)
+
+ except Exception:
+ log.exception(
+ "Exception determining if movie was blacklisted %s: ", movie)
+ return blacklisted
+
+
+def extract_list_user_and_key_from_url(list_url):
+ try:
+ import re
+ list_user = re.search('\/users\/([^/]*)', list_url).group(1)
+ list_key = re.search('\/lists\/([^/]*)', list_url).group(1)
+
+ return list_user, list_key
+ except:
+ log.error('The URL "%s" is not in the correct format', list_url)
+ exit()
diff --git a/menu/pgtrakt/list_of_country_codes.md b/menu/pgtrakt/list_of_country_codes.md
new file mode 100644
index 00000000..c57a9a66
--- /dev/null
+++ b/menu/pgtrakt/list_of_country_codes.md
@@ -0,0 +1,79 @@
+- `au` (Australia)
+- `at` (Austria)
+- `be` (Belgium)
+- `bo` (Bolivia, Plurinational State of)
+- `ba` (Bosnia and Herzegovina)
+- `br` (Brazil)
+- `io` (British Indian Ocean Territory)
+- `bg` (Bulgaria)
+- `ca` (Canada)
+- `cl` (Chile)
+- `cn` (China)
+- `co` (Colombia)
+- `hr` (Croatia)
+- `cu` (Cuba)
+- `cz` (Czech Republic)
+- `dk` (Denmark)
+- `eg` (Egypt)
+- `ee` (Estonia)
+- `fi` (Finland)
+- `fr` (France)
+- `pf` (French Polynesia)
+- `ge` (Georgia)
+- `de` (Germany)
+- `gr` (Greece)
+- `hk` (Hong Kong)
+- `hu` (Hungary)
+- `is` (Iceland)
+- `in` (India)
+- `id` (Indonesia)
+- `ir` (Iran, Islamic Republic of)
+- `ie` (Ireland)
+- `il` (Israel)
+- `it` (Italy)
+- `jp` (Japan)
+- `ke` (Kenya)
+- `kp` (Korea, Democratic People's Republic of)
+- `kr` (Korea, Republic of)
+- `kw` (Kuwait)
+- `lv` (Latvia)
+- `lb` (Lebanon)
+- `lt` (Lithuania)
+- `mk` (Macedonia, Republic of)
+- `my` (Malaysia)
+- `mv` (Maldives)
+- `mx` (Mexico)
+- `mn` (Mongolia)
+- `me` (Montenegro)
+- `ma` (Morocco)
+- `nl` (Netherlands)
+- `nz` (New Zealand)
+- `ng` (Nigeria)
+- `no` (Norway)
+- `pk` (Pakistan)
+- `py` (Paraguay)
+- `ph` (Philippines)
+- `pl` (Poland)
+- `pt` (Portugal)
+- `ro` (Romania)
+- `ru` (Russian Federation)
+- `sa` (Saudi Arabia)
+- `rs` (Serbia)
+- `sg` (Singapore)
+- `sk` (Slovakia)
+- `si` (Slovenia)
+- `za` (South Africa)
+- `es` (Spain)
+- `sz` (Swaziland)
+- `se` (Sweden)
+- `ch` (Switzerland)
+- `tw` (Taiwan)
+- `th` (Thailand)
+- `tr` (Turkey)
+- `ua` (Ukraine)
+- `ae` (United Arab Emirates)
+- `gb` (United Kingdom)
+- `us` (United States)
+- `um` (United States Minor Outlying Islands)
+- `ve` (Venezuela, Bolivarian Republic of)
+- `vn` (Vietnam)
diff --git a/menu/pgtrakt/list_of_language_codes.md b/menu/pgtrakt/list_of_language_codes.md
new file mode 100644
index 00000000..45c74d39
--- /dev/null
+++ b/menu/pgtrakt/list_of_language_codes.md
@@ -0,0 +1,131 @@
+- `af` (Afrikaans)
+- `ak` (Akan)
+- `sq` (Albanian)
+- `am` (Amharic)
+- `ar` (Arabic)
+- `hy` (Armenian)
+- `as` (Assamese)
+- `ay` (Aymara)
+- `az` (Azerbaijani)
+- `bm` (Bambara)
+- `eu` (Basque)
+- `be` (Belarusian)
+- `bn` (Bengali)
+- `nb` (Bokmål, Norwegian; Norwegian Bokmål)
+- `bs` (Bosnian)
+- `br` (Breton)
+- `bg` (Bulgarian)
+- `my` (Burmese)
+- `ca` (Catalan; Valencian)
+- `km` (Central Khmer)
+- `ch` (Chamorro)
+- `ce` (Chechen)
+- `zh` (Chinese)
+- `kw` (Cornish)
+- `co` (Corsican)
+- `cr` (Cree)
+- `hr` (Croatian)
+- `cs` (Czech)
+- `da` (Danish)
+- `dv` (Divehi; Dhivehi; Maldivian)
+- `nl` (Dutch; Flemish)
+- `dz` (Dzongkha)
+- `en` (English)
+- `eo` (Esperanto)
+- `et` (Estonian)
+- `ee` (Ewe)
+- `fo` (Faroese)
+- `fi` (Finnish)
+- `fr` (French)
+- `gd` (Gaelic; Scottish Gaelic)
+- `gl` (Galician)
+- `ka` (Georgian)
+- `de` (German)
+- `el` (Greek, Modern (1453-))
+- `gn` (Guarani)
+- `gu` (Gujarati)
+- `ht` (Haitian; Haitian Creole)
+- `ha` (Hausa)
+- `he` (Hebrew)
+- `hi` (Hindi)
+- `hu` (Hungarian)
+- `is` (Icelandic)
+- `ig` (Igbo)
+- `id` (Indonesian)
+- `iu` (Inuktitut)
+- `ga` (Irish)
+- `it` (Italian)
+- `ja` (Japanese)
+- `jv` (Javanese)
+- `kl` (Kalaallisut; Greenlandic)
+- `kn` (Kannada)
+- `kk` (Kazakh)
+- `rw` (Kinyarwanda)
+- `ky` (Kirghiz; Kyrgyz)
+- `ko` (Korean)
+- `ku` (Kurdish)
+- `lo` (Lao)
+- `la` (Latin)
+- `lv` (Latvian)
+- `lt` (Lithuanian)
+- `lb` (Luxembourgish; Letzeburgesch)
+- `mk` (Macedonian)
+- `mg` (Malagasy)
+- `ms` (Malay)
+- `ml` (Malayalam)
+- `mt` (Maltese)
+- `mi` (Maori)
+- `mr` (Marathi)
+- `mh` (Marshallese)
+- `mn` (Mongolian)
+- `nv` (Navajo; Navaho)
+- `nr` (Ndebele, South; South Ndebele)
+- `ne` (Nepali)
+- `no` (Norwegian)
+- `nn` (Norwegian Nynorsk; Nynorsk, Norwegian)
+- `oc` (Occitan (post 1500); Provençal)
+- `pa` (Panjabi; Punjabi)
+- `fa` (Persian)
+- `pl` (Polish)
+- `pt` (Portuguese)
+- `ps` (Pushto; Pashto)
+- `qu` (Quechua)
+- `ro` (Romanian; Moldavian; Moldovan)
+- `rn` (Rundi)
+- `ru` (Russian)
+- `sm` (Samoan)
+- `sg` (Sango)
+- `sa` (Sanskrit)
+- `sr` (Serbian)
+- `sn` (Shona)
+- `si` (Sinhala; Sinhalese)
+- `sk` (Slovak)
+- `sl` (Slovenian)
+- `so` (Somali)
+- `es` (Spanish; Castilian)
+- `sw` (Swahili)
+- `sv` (Swedish)
+- `tl` (Tagalog)
+- `ty` (Tahitian)
+- `tg` (Tajik)
+- `ta` (Tamil)
+- `tt` (Tatar)
+- `te` (Telugu)
+- `th` (Thai)
+- `bo` (Tibetan)
+- `ti` (Tigrinya)
+- `to` (Tonga (Tonga Islands))
+- `tr` (Turkish)
+- `tk` (Turkmen)
+- `uk` (Ukrainian)
+- `ur` (Urdu)
+- `uz` (Uzbek)
+- `ve` (Venda)
+- `vi` (Vietnamese)
+- `cy` (Welsh)
+- `fy` (Western Frisian)
+- `wo` (Wolof)
+- `xh` (Xhosa)
+- `yi` (Yiddish)
+- `za` (Zhuang; Chuang)
+- `zu` (Zulu)
diff --git a/menu/pgtrakt/list_of_movie_genres.md b/menu/pgtrakt/list_of_movie_genres.md
new file mode 100644
index 00000000..feee12d1
--- /dev/null
+++ b/menu/pgtrakt/list_of_movie_genres.md
@@ -0,0 +1,34 @@
+- `action`
+- `adventure`
+- `animation`
+- `anime`
+- `comedy`
+- `crime`
+- `disaster`
+- `documentary`
+- `drama`
+- `eastern`
+- `family`
+- `fan-film`
+- `fantasy`
+- `film-noir`
+- `history`
+- `holiday`
+- `horror`
+- `indie`
+- `music`
+- `musical`
+- `mystery`
+- `none`
+- `road`
+- `romance`
+- `science-fiction`
+- `short`
+- `sporting-event`
+- `sports`
+- `superhero`
+- `suspense`
+- `thriller`
+- `tv-movie`
+- `war`
+- `western`
diff --git a/menu/pgtrakt/list_of_show_genres.md b/menu/pgtrakt/list_of_show_genres.md
new file mode 100644
index 00000000..b7dd63ef
--- /dev/null
+++ b/menu/pgtrakt/list_of_show_genres.md
@@ -0,0 +1,39 @@
+- `action`
+- `adventure`
+- `animation`
+- `anime`
+- `biography`
+- `children`
+- `comedy`
+- `crime`
+- `disaster`
+- `documentary`
+- `drama`
+- `eastern`
+- `family`
+- `fantasy`
+- `game-show`
+- `history`
+- `holiday`
+- `home-and-garden`
+- `horror`
+- `mini-series`
+- `music`
+- `musical`
+- `mystery`
+- `news`
+- `none`
+- `reality`
+- `romance`
+- `science-fiction`
+- `short`
+- `soap`
+- `special-interest`
+- `sporting-event`
+- `sports`
+- `superhero`
+- `suspense`
+- `talk-show`
+- `thriller`
+- `war`
+- `western`
diff --git a/menu/pgtrakt/media/__init__.py b/menu/pgtrakt/media/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/menu/pgtrakt/media/pvr.py b/menu/pgtrakt/media/pvr.py
new file mode 100644
index 00000000..380e9c64
--- /dev/null
+++ b/menu/pgtrakt/media/pvr.py
@@ -0,0 +1,158 @@
+import os.path
+from abc import ABC, abstractmethod
+
+import backoff
+import requests
+
+from helpers.misc import backoff_handler
+from helpers import str as misc_str
+from helpers import misc
+from misc.log import logger
+
+log = logger.get_logger(__name__)
+
+
+class PVR(ABC):
+ def __init__(self, server_url, api_key):
+ self.server_url = server_url
+ self.api_key = api_key
+ self.headers = {
+ 'Content-Type': 'application/json',
+ 'X-Api-Key': self.api_key,
+ }
+
+ def validate_api_key(self):
+ try:
+ # request system status to validate api_key
+ req = requests.get(
+ os.path.join(misc_str.ensure_endswith(
+ self.server_url, "/"), 'api/system/status'),
+ headers=self.headers,
+ timeout=60,
+ allow_redirects=False
+ )
+ log.debug("Request Response: %d", req.status_code)
+
+ if req.status_code == 200 and 'version' in req.json():
+ return True
+ return False
+ except Exception:
+ log.exception("Exception validating api_key: ")
+ return False
+
+ @abstractmethod
+ def get_objects(self):
+ pass
+
+ @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler)
+ def _get_objects(self, endpoint):
+ try:
+ # make request
+ req = requests.get(
+ os.path.join(misc_str.ensure_endswith(
+ self.server_url, "/"), endpoint),
+ headers=self.headers,
+ timeout=60,
+ allow_redirects=False
+ )
+ log.debug("Request URL: %s", req.url)
+ log.debug("Request Response: %d", req.status_code)
+
+ if req.status_code == 200:
+ resp_json = req.json()
+ log.debug("Found %d objects", len(resp_json))
+ return resp_json
+ else:
+ log.error(
+ "Failed to retrieve all objects, request response: %d", req.status_code)
+ except Exception:
+ log.exception("Exception retrieving objects: ")
+ return None
+
+ @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler)
+ def get_profile_id(self, profile_name):
+ try:
+ # make request
+ req = requests.get(
+ os.path.join(misc_str.ensure_endswith(
+ self.server_url, "/"), 'api/profile'),
+ headers=self.headers,
+ timeout=60,
+ allow_redirects=False
+ )
+ log.debug("Request URL: %s", req.url)
+ log.debug("Request Response: %d", req.status_code)
+
+ if req.status_code == 200:
+ resp_json = req.json()
+ for profile in resp_json:
+ if profile['name'].lower() == profile_name.lower():
+ log.debug("Found id of %s profile: %d",
+ profile_name, profile['id'])
+ return profile['id']
+ log.debug("Profile %s with id %d did not match %s",
+ profile['name'], profile['id'], profile_name)
+ else:
+ log.error(
+ "Failed to retrieve all quality profiles, request response: %d", req.status_code)
+ except Exception:
+ log.exception(
+ "Exception retrieving id of profile %s: ", profile_name)
+ return None
+
+ def _prepare_add_object_payload(self, title, title_slug, profile_id, root_folder):
+ return {
+ 'title': title,
+ 'titleSlug': title_slug,
+ 'qualityProfileId': profile_id,
+ 'images': [],
+ 'monitored': True,
+ 'rootFolderPath': root_folder,
+ 'addOptions': {
+ 'ignoreEpisodesWithFiles': False,
+ 'ignoreEpisodesWithoutFiles': False,
+ }
+ }
+
+ @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler)
+ def _add_object(self, endpoint, payload, identifier_field, identifier):
+ try:
+ # make request
+ req = requests.post(
+ os.path.join(misc_str.ensure_endswith(
+ self.server_url, "/"), endpoint),
+ headers=self.headers,
+ json=payload,
+ timeout=60,
+ allow_redirects=False
+ )
+ log.debug("Request URL: %s", req.url)
+ log.debug("Request Payload: %s", payload)
+ log.debug("Request Response Code: %d", req.status_code)
+ log.debug("Request Response Text:\n%s", req.text)
+
+ response_json = None
+ if 'json' in req.headers['Content-Type'].lower():
+ response_json = misc.get_response_dict(
+ req.json(), identifier_field, identifier)
+
+ if (req.status_code == 201 or req.status_code == 200) \
+ and (response_json and identifier_field in response_json) \
+ and response_json[identifier_field] == identifier:
+ log.debug("Successfully added %s (%d)",
+ payload['title'], identifier)
+ return True
+ elif response_json and ('errorMessage' in response_json or 'message' in response_json):
+ message = response_json['errorMessage'] if 'errorMessage' in response_json else response_json['message']
+
+ log.error("Failed to add %s (%d) - status_code: %d, reason: %s", payload['title'], identifier,
+ req.status_code, message)
+ return False
+ else:
+ log.error("Failed to add %s (%d), unexpected response:\n%s",
+ payload['title'], identifier, req.text)
+ return False
+ except Exception:
+ log.exception("Exception adding %s (%d): ",
+ payload['title'], identifier)
+ return None
diff --git a/menu/pgtrakt/media/radarr.py b/menu/pgtrakt/media/radarr.py
new file mode 100644
index 00000000..c60b5bc6
--- /dev/null
+++ b/menu/pgtrakt/media/radarr.py
@@ -0,0 +1,29 @@
+import backoff
+
+from helpers.misc import backoff_handler, dict_merge
+from media.pvr import PVR
+from misc.log import logger
+
+log = logger.get_logger(__name__)
+
+
+class Radarr(PVR):
+ def get_objects(self):
+ return self._get_objects('api/movie')
+
+ @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler)
+ def add_movie(self, movie_tmdbid, movie_title, movie_year, movie_title_slug, profile_id, root_folder,
+ search_missing=False):
+ payload = self._prepare_add_object_payload(
+ movie_title, movie_title_slug, profile_id, root_folder)
+
+ payload = dict_merge(payload, {
+ 'tmdbId': movie_tmdbid,
+ 'year': movie_year,
+ 'minimumAvailability': 'released',
+ 'addOptions': {
+ 'searchForMovie': search_missing
+ }
+ })
+
+ return self._add_object('api/movie', payload, identifier_field='tmdbId', identifier=movie_tmdbid)
diff --git a/menu/pgtrakt/media/sonarr.py b/menu/pgtrakt/media/sonarr.py
new file mode 100644
index 00000000..e52a51b5
--- /dev/null
+++ b/menu/pgtrakt/media/sonarr.py
@@ -0,0 +1,62 @@
+import os.path
+
+import backoff
+import requests
+from helpers.misc import backoff_handler, dict_merge
+
+from helpers import str as misc_str
+from media.pvr import PVR
+from misc.log import logger
+
+log = logger.get_logger(__name__)
+
+
+class Sonarr(PVR):
+ def get_objects(self):
+ return self._get_objects('api/series')
+
+ @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler)
+ def get_tags(self):
+ tags = {}
+ try:
+ # make request
+ req = requests.get(
+ os.path.join(misc_str.ensure_endswith(
+ self.server_url, "/"), 'api/tag'),
+ headers=self.headers,
+ timeout=60,
+ allow_redirects=False
+ )
+ log.debug("Request URL: %s", req.url)
+ log.debug("Request Response: %d", req.status_code)
+
+ if req.status_code == 200:
+ resp_json = req.json()
+ log.debug("Found %d tags", len(resp_json))
+ for tag in resp_json:
+ tags[tag['label']] = tag['id']
+ return tags
+ else:
+ log.error(
+ "Failed to retrieve all tags, request response: %d", req.status_code)
+ except Exception:
+ log.exception("Exception retrieving tags: ")
+ return None
+
+ @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler)
+ def add_series(self, series_tvdbid, series_title, series_title_slug, profile_id, root_folder, tag_ids=None,
+ search_missing=False):
+ payload = self._prepare_add_object_payload(
+ series_title, series_title_slug, profile_id, root_folder)
+
+ payload = dict_merge(payload, {
+ 'tvdbId': series_tvdbid,
+ 'tags': [] if not tag_ids or not isinstance(tag_ids, list) else tag_ids,
+ 'seasons': [],
+ 'seasonFolder': True,
+ 'addOptions': {
+ 'searchForMissingEpisodes': search_missing
+ }
+ })
+
+ return self._add_object('api/series', payload, identifier_field='tvdbId', identifier=series_tvdbid)
diff --git a/menu/pgtrakt/media/trakt.py b/menu/pgtrakt/media/trakt.py
new file mode 100644
index 00000000..65b80051
--- /dev/null
+++ b/menu/pgtrakt/media/trakt.py
@@ -0,0 +1,575 @@
+import time
+
+import backoff
+import requests
+
+from helpers.misc import backoff_handler, dict_merge
+from helpers.trakt import extract_list_user_and_key_from_url
+from misc.log import logger
+
+log = logger.get_logger(__name__)
+
+
+class Trakt:
+ non_user_lists = ['anticipated', 'trending',
+ 'popular', 'boxoffice', 'watched', 'played']
+
+ def __init__(self, cfg):
+ self.cfg = cfg
+
+ ############################################################
+ # Requests
+ ############################################################
+
+ def _make_request(self, url, payload={}, authenticate_user=None, request_type='get'):
+ headers, authenticate_user = self._headers(authenticate_user)
+
+ if authenticate_user:
+ url = url.replace('{authenticate_user}', authenticate_user)
+
+ # make request
+ if request_type == 'delete':
+ req = requests.delete(url, headers=headers,
+ params=payload, timeout=30)
+ else:
+ req = requests.get(url, headers=headers,
+ params=payload, timeout=30)
+ log.debug("Request URL: %s", req.url)
+ log.debug("Request Payload: %s", payload)
+ log.debug("Request User: %s", authenticate_user)
+ log.debug("Response Code: %d", req.status_code)
+
+ return req
+
+ @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler)
+ def _make_item_request(self, url, object_name, payload={}):
+ payload = dict_merge(payload, {'extended': 'full'})
+
+ try:
+ req = self._make_request(url, payload)
+
+ if req.status_code == 200:
+ resp_json = req.json()
+ return resp_json
+ elif req.status_code == 401:
+ log.error(
+ "The authentication to Trakt is revoked. Please re-authenticate.")
+ exit()
+ else:
+ log.error("Failed to retrieve %s, request response: %d",
+ object_name, req.status_code)
+ return None
+ except Exception:
+ log.exception("Exception retrieving %s: ", object_name)
+ return None
+
+ @backoff.on_predicate(backoff.expo, lambda x: x is None, max_tries=4, on_backoff=backoff_handler)
+ def _make_items_request(self, url, limit, languages, type_name, object_name, authenticate_user=None, payload={},
+ sleep_between=5, genres=None):
+ if not languages:
+ languages = ['en']
+
+ payload = dict_merge(payload, {
+ 'extended': 'full', 'limit': limit, 'page': 1, 'languages': ','.join(languages)})
+ if genres:
+ payload['genres'] = genres
+
+ processed = []
+
+ if authenticate_user:
+ type_name = type_name.replace(
+ '{authenticate_user}', self._user_used_for_authentication(authenticate_user))
+
+ try:
+ while True:
+ req = self._make_request(url, payload, authenticate_user)
+
+ current_page = payload['page']
+ total_pages = 0 if 'X-Pagination-Page-Count' not in req.headers else int(
+ req.headers['X-Pagination-Page-Count'])
+
+ log.debug("Response Page: %d of %d", current_page, total_pages)
+
+ if req.status_code == 200:
+ resp_json = req.json()
+ if type_name == 'person' and 'cast' in resp_json:
+ # handle person results
+ for item in resp_json['cast']:
+ if item not in processed:
+ if object_name.rstrip('s') not in item and 'title' in item:
+ processed.append(
+ {object_name.rstrip('s'): item})
+ else:
+ processed.append(item)
+ else:
+ for item in resp_json:
+ if item not in processed:
+ if object_name.rstrip('s') not in item and 'title' in item:
+ processed.append(
+ {object_name.rstrip('s'): item})
+ else:
+ processed.append(item)
+
+ # check if we have fetched the last page, break if so
+ if total_pages == 0:
+ log.debug("There were no more pages to retrieve")
+ break
+ elif current_page >= total_pages:
+ log.debug(
+ "There are no more pages to retrieve results from")
+ break
+ else:
+ log.info(
+ "There are %d pages left to retrieve results from", total_pages - current_page)
+ payload['page'] += 1
+ time.sleep(sleep_between)
+ elif req.status_code == 401:
+ log.error(
+ "The authentication to Trakt is revoked. Please re-authenticate.")
+ exit()
+ else:
+ log.error("Failed to retrieve %s %s, request response: %d",
+ type_name, object_name, req.status_code)
+ break
+
+ if len(processed):
+ log.debug("Found %d %s %s", len(
+ processed), type_name, object_name)
+ return processed
+ return None
+ except Exception:
+ log.exception("Exception retrieving %s %s: ",
+ type_name, object_name)
+ return None
+
+ def validate_client_id(self):
+ try:
+ # request anticipated shows to validate client_id
+ req = self._make_request(
+ url='https://api.trakt.tv/shows/anticipated',
+ )
+
+ if req.status_code == 200:
+ return True
+ return False
+ except Exception:
+ log.exception("Exception validating client_id: ")
+ return False
+
+ def remove_recommended_item(self, item_type, trakt_id, authenticate_user=None):
+ ret = self._make_request(
+ url='https://api.trakt.tv/recommendations/%ss/%s' % (
+ item_type, str(trakt_id)),
+ authenticate_user=authenticate_user,
+ request_type='delete'
+ )
+ if ret.status_code == 204:
+ return True
+ return False
+
+ ############################################################
+ # OAuth Authentication
+ ############################################################
+
+ def __oauth_request_device_code(self):
+ log.info(
+ "We're talking to Trakt to get your verification code. Please wait a moment...")
+
+ payload = {'client_id': self.cfg.trakt.client_id}
+
+ print(self._headers_without_authentication())
+
+ # Request device code
+ req = requests.post('https://api.trakt.tv/oauth/device/code', params=payload,
+ headers=self._headers_without_authentication())
+ device_code_response = req.json()
+
+ # Display needed information to the user
+ log.info('Go to: %s on any device and enter %s. We\'ll be polling Trakt every %s seconds for a reply',
+ device_code_response['verification_url'], device_code_response['user_code'],
+ device_code_response['interval'])
+
+ return device_code_response
+
+ def __oauth_process_token_request(self, req):
+ success = False
+
+ if req.status_code == 200:
+ # Success; saving the access token
+ access_token_response = req.json()
+ access_token = access_token_response['access_token']
+
+ # But first we need to find out what user this token belongs to
+ temp_headers = self._headers_without_authentication()
+ temp_headers['Authorization'] = 'Bearer ' + access_token
+
+ req = requests.get(
+ 'https://api.trakt.tv/users/me', headers=temp_headers)
+
+ from misc.config import Config
+ new_config = Config()
+
+ new_config.merge_settings({
+ "trakt": {
+ req.json()['username']: access_token_response
+ }
+ })
+
+ success = True
+ elif req.status_code == 404:
+ log.debug('The device code was wrong')
+ log.error(
+ 'Whoops, something went wrong; aborting the authentication process')
+ elif req.status_code == 409:
+ log.error(
+ 'You\'ve already authenticated this application; aborting the authentication process')
+ elif req.status_code == 410:
+ log.error(
+ 'The authentication process has expired; please start again')
+ elif req.status_code == 418:
+ log.error(
+ 'You\'ve denied the authentication; are you sure? Please try again')
+ elif req.status_code == 429:
+ log.debug('We\'re polling too quickly.')
+
+ return success, req.status_code
+
+ def __oauth_poll_for_access_token(self, device_code, polling_interval=5, polling_expire=600):
+ polling_start = time.time()
+ time.sleep(polling_interval)
+ tries = 0
+
+ while time.time() - polling_start < polling_expire:
+ tries += 1
+
+ log.debug('Polling Trakt for the %sth time; %s seconds left', tries,
+ polling_expire - round(time.time() - polling_start))
+
+ payload = {'code': device_code, 'client_id': self.cfg.trakt.client_id,
+ 'client_secret': self.cfg.trakt.client_secret, 'grant_type': 'authorization_code'}
+
+ # Poll Trakt for access token
+ req = requests.post('https://api.trakt.tv/oauth/device/token', params=payload,
+ headers=self._headers_without_authentication())
+
+ success, status_code = self.__oauth_process_token_request(req)
+
+ if success:
+ break
+ elif status_code == 426:
+ log.debug('Increasing the interval by one second')
+ polling_interval += 1
+
+ time.sleep(polling_interval)
+ return False
+
+ def __oauth_refresh_access_token(self, refresh_token):
+ payload = {'refresh_token': refresh_token, 'client_id': self.cfg.trakt.client_id,
+ 'client_secret': self.cfg.trakt.client_secret, 'grant_type': 'refresh_token'}
+
+ req = requests.post('https://api.trakt.tv/oauth/token', params=payload,
+ headers=self._headers_without_authentication())
+
+ success, status_code = self.__oauth_process_token_request(req)
+
+ return success
+
+ def oauth_authentication(self):
+ try:
+ device_code_response = self.__oauth_request_device_code()
+
+ if self.__oauth_poll_for_access_token(device_code_response['device_code'],
+ device_code_response['interval'],
+ device_code_response['expires_in']):
+ return True
+ except Exception:
+ log.exception("Exception occurred when authenticating user")
+ return False
+
+ def _get_first_authenticated_user(self):
+ import copy
+
+ users = copy.copy(self.cfg.trakt)
+
+ if 'client_id' in users.keys():
+ users.pop('client_id')
+
+ if 'client_secret' in users.keys():
+ users.pop('client_secret')
+
+ if len(users) > 0:
+ return list(users.keys())[0]
+
+ def _user_is_authenticated(self, user):
+ return user in self.cfg['trakt'].keys()
+
+ def _renew_oauth_token_if_expired(self, user):
+ token_information = self.cfg['trakt'][user]
+
+ # Check if the acces_token for the user is expired
+ expires_at = token_information['created_at'] + \
+ token_information['expires_in']
+ if expires_at < round(time.time()):
+ log.info("The access token for the user %s has expired. We're requesting a new one; please wait a moment.",
+ user)
+
+ if self.__oauth_refresh_access_token(token_information["refresh_token"]):
+ log.info(
+ "The access token for the user %s has been refreshed. Please restart the application.", user)
+
+ def _user_used_for_authentication(self, user=None):
+ if user is None:
+ user = self._get_first_authenticated_user()
+ elif not self._user_is_authenticated(user):
+ log.error('The user %s you specified to use for authentication is not authenticated yet. ' +
+ 'Authenticate the user first, before you use it to retrieve lists.', user)
+
+ exit()
+
+ return user
+
+ def _headers_without_authentication(self):
+ return {
+ 'Content-Type': 'application/json',
+ 'trakt-api-version': '2',
+ 'trakt-api-key': self.cfg.trakt.client_id
+ }
+
+ def _headers(self, user=None):
+ headers = self._headers_without_authentication()
+
+ user = self._user_used_for_authentication(user)
+
+ if user is not None:
+ self._renew_oauth_token_if_expired(user)
+ headers['Authorization'] = 'Bearer ' + \
+ self.cfg['trakt'][user]['access_token']
+ else:
+ log.info('No user')
+
+ return headers, user
+
+ ############################################################
+ # Shows
+ ############################################################
+
+ def get_show(self, show_id):
+ return self._make_item_request(
+ url='https://api.trakt.tv/shows/%s' % str(show_id),
+ object_name='show',
+ )
+
+ def get_trending_shows(self, limit=1000, languages=None, genres=None):
+ return self._make_items_request(
+ url='https://api.trakt.tv/shows/trending',
+ limit=limit,
+ languages=languages,
+ object_name='shows',
+ type_name='trending',
+ genres=genres
+ )
+
+ def get_popular_shows(self, limit=1000, languages=None, genres=None):
+ return self._make_items_request(
+ url='https://api.trakt.tv/shows/popular',
+ limit=limit,
+ languages=languages,
+ object_name='shows',
+ type_name='popular',
+ genres=genres
+ )
+
+ def get_anticipated_shows(self, limit=1000, languages=None, genres=None):
+ return self._make_items_request(
+ url='https://api.trakt.tv/shows/anticipated',
+ limit=limit,
+ languages=languages,
+ object_name='shows',
+ type_name='anticipated',
+ genres=genres
+ )
+
+ def get_person_shows(self, person, limit=1000, languages=None, genres=None):
+ return self._make_items_request(
+ url='https://api.trakt.tv/people/%s/shows' % person,
+ limit=limit,
+ languages=languages,
+ object_name='shows',
+ type_name='person',
+ genres=genres
+ )
+
+ def get_most_played_shows(self, limit=1000, languages=None, genres=None, most_type=None):
+ return self._make_items_request(
+ url='https://api.trakt.tv/shows/played/%s' % (
+ 'weekly' if not most_type else most_type),
+ limit=limit,
+ languages=languages,
+ object_name='shows',
+ type_name='played',
+ genres=genres
+ )
+
+ def get_most_watched_shows(self, limit=1000, languages=None, genres=None, most_type=None):
+ return self._make_items_request(
+ url='https://api.trakt.tv/shows/watched/%s' % (
+ 'weekly' if not most_type else most_type),
+ limit=limit,
+ languages=languages,
+ object_name='shows',
+ type_name='watched',
+ genres=genres
+ )
+
+ def get_recommended_shows(self, authenticate_user=None, limit=1000, languages=None, genres=None):
+ return self._make_items_request(
+ url='https://api.trakt.tv/recommendations/shows',
+ authenticate_user=authenticate_user,
+ limit=limit,
+ languages=languages,
+ object_name='shows',
+ type_name='recommended from {authenticate_user}',
+ genres=genres
+ )
+
+ def get_watchlist_shows(self, authenticate_user=None, limit=1000, languages=None):
+ return self._make_items_request(
+ url='https://api.trakt.tv/users/{authenticate_user}/watchlist/shows',
+ authenticate_user=authenticate_user,
+ limit=limit,
+ languages=languages,
+ object_name='shows',
+ type_name='watchlist from {authenticate_user}',
+ )
+
+ def get_user_list_shows(self, list_url, authenticate_user=None, limit=1000, languages=None):
+ list_user, list_key = extract_list_user_and_key_from_url(list_url)
+
+ log.debug('Fetching %s from %s', list_key, list_user)
+
+ return self._make_items_request(
+ url='https://api.trakt.tv/users/' + list_user +
+ '/lists/' + list_key + '/items/shows',
+ authenticate_user=authenticate_user,
+ limit=limit,
+ languages=languages,
+ object_name='shows',
+ type_name=(list_key + ' from ' + list_user),
+ )
+
+ ############################################################
+ # Movies
+ ############################################################
+
+ def get_movie(self, movie_id):
+ return self._make_item_request(
+ url='https://api.trakt.tv/movies/%s' % str(movie_id),
+ object_name='movie',
+ )
+
+ def get_trending_movies(self, limit=1000, languages=None, genres=None):
+ return self._make_items_request(
+ url='https://api.trakt.tv/movies/trending',
+ limit=limit,
+ languages=languages,
+ object_name='movies',
+ type_name='trending',
+ genres=genres
+ )
+
+ def get_popular_movies(self, limit=1000, languages=None, genres=None):
+ return self._make_items_request(
+ url='https://api.trakt.tv/movies/popular',
+ limit=limit,
+ languages=languages,
+ object_name='movies',
+ type_name='popular',
+ genres=genres
+ )
+
+ def get_anticipated_movies(self, limit=1000, languages=None, genres=None):
+ return self._make_items_request(
+ url='https://api.trakt.tv/movies/anticipated',
+ limit=limit,
+ languages=languages,
+ object_name='movies',
+ type_name='anticipated',
+ genres=genres
+ )
+
+ def get_person_movies(self, person, limit=1000, languages=None, genres=None):
+ return self._make_items_request(
+ url='https://api.trakt.tv/people/%s/movies' % person,
+ limit=limit,
+ languages=languages,
+ object_name='movies',
+ type_name='person',
+ genres=genres
+ )
+
+ def get_most_played_movies(self, limit=1000, languages=None, genres=None, most_type=None):
+ return self._make_items_request(
+ url='https://api.trakt.tv/movies/played/%s' % (
+ 'weekly' if not most_type else most_type),
+ limit=limit,
+ languages=languages,
+ object_name='movies',
+ type_name='played',
+ genres=genres
+ )
+
+ def get_most_watched_movies(self, limit=1000, languages=None, genres=None, most_type=None):
+ return self._make_items_request(
+ url='https://api.trakt.tv/movies/watched/%s' % (
+ 'weekly' if not most_type else most_type),
+ limit=limit,
+ languages=languages,
+ object_name='movies',
+ type_name='watched',
+ genres=genres
+ )
+
+ def get_boxoffice_movies(self, limit=1000, languages=None):
+ return self._make_items_request(
+ url='https://api.trakt.tv/movies/boxoffice',
+ limit=limit,
+ languages=languages,
+ object_name='movies',
+ type_name='anticipated',
+ )
+
+ def get_recommended_movies(self, authenticate_user=None, limit=1000, languages=None, genres=None):
+ return self._make_items_request(
+ url='https://api.trakt.tv/recommendations/movies',
+ authenticate_user=authenticate_user,
+ limit=limit,
+ languages=languages,
+ object_name='movies',
+ type_name='recommended from {authenticate_user}',
+ genres=genres
+ )
+
+ def get_watchlist_movies(self, authenticate_user=None, limit=1000, languages=None):
+ return self._make_items_request(
+ url='https://api.trakt.tv/users/{authenticate_user}/watchlist/movies',
+ authenticate_user=authenticate_user,
+ limit=limit,
+ languages=languages,
+ object_name='movies',
+ type_name='watchlist from {authenticate_user}',
+ )
+
+ def get_user_list_movies(self, list_url, authenticate_user=None, limit=1000, languages=None):
+ list_user, list_key = extract_list_user_and_key_from_url(list_url)
+
+ log.debug('Fetching %s from %s', list_key, list_user)
+
+ return self._make_items_request(
+ url='https://api.trakt.tv/users/' + list_user +
+ '/lists/' + list_key + '/items/movies',
+ authenticate_user=authenticate_user,
+ limit=limit,
+ languages=languages,
+ object_name='movies',
+ type_name=(list_key + ' from ' + list_user),
+ )
diff --git a/menu/pgtrakt/misc/__init__.py b/menu/pgtrakt/misc/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/menu/pgtrakt/misc/config.py b/menu/pgtrakt/misc/config.py
new file mode 100644
index 00000000..cccd244d
--- /dev/null
+++ b/menu/pgtrakt/misc/config.py
@@ -0,0 +1,211 @@
+import json
+import os
+import sys
+
+from attrdict import AttrDict
+
+
+class Singleton(type):
+ _instances = {}
+
+ def __call__(cls, *args, **kwargs):
+ if cls not in cls._instances:
+ cls._instances[cls] = super(
+ Singleton, cls).__call__(*args, **kwargs)
+
+ return cls._instances[cls]
+
+
+class AttrConfig(AttrDict):
+ """
+ Simple AttrDict subclass to return None when requested attribute does not exist
+ """
+
+ def __init__(self, config):
+ super().__init__(config)
+
+ def __getattr__(self, item):
+ try:
+ return super().__getattr__(item)
+ except AttributeError:
+ pass
+ # Default behaviour
+ return None
+
+
+class Config(object, metaclass=Singleton):
+ base_config = {
+ 'core': {
+ 'debug': False
+ },
+ 'trakt': {
+ 'client_id': '',
+ 'client_secret': ''
+ },
+ 'sonarr': {
+ 'url': 'http://localhost:8989/',
+ 'api_key': '',
+ 'profile': 'HD-1080p',
+ 'root_folder': '/tv/',
+ 'tags': {
+ }
+ },
+ 'radarr': {
+ 'url': 'http://localhost:7878/',
+ 'api_key': '',
+ 'profile': 'HD-1080p',
+ 'root_folder': '/movies/'
+ },
+ 'omdb': {
+ 'api_key': ''
+ },
+ 'filters': {
+ 'shows': {
+ 'disabled_for': [],
+ 'blacklisted_genres': [],
+ 'blacklisted_networks': [],
+ 'allowed_countries': [],
+ 'allowed_languages': [],
+ 'blacklisted_min_runtime': 15,
+ 'blacklisted_min_year': 2000,
+ 'blacklisted_max_year': 2019,
+ 'blacklisted_tvdb_ids': [],
+ },
+ 'movies': {
+ 'disabled_for': [],
+ 'blacklisted_genres': [],
+ 'blacklisted_min_runtime': 60,
+ 'blacklisted_min_year': 2000,
+ 'blacklisted_max_year': 2019,
+ 'blacklist_title_keywords': [],
+ 'blacklisted_tmdb_ids': [],
+ 'allowed_countries': [],
+ 'allowed_languages': [],
+ 'rating_limit': ""
+ }
+ },
+ 'automatic': {
+ 'movies': {
+ 'interval': 20,
+ 'anticipated': 3,
+ 'trending': 3,
+ 'popular': 3,
+ 'boxoffice': 10
+ },
+ 'shows': {
+ 'interval': 48,
+ 'anticipated': 10,
+ 'trending': 1,
+ 'popular': 1
+ }
+ },
+ 'notifications': {
+ 'verbose': True
+ }
+ }
+
+ def __init__(self, config_path, logfile):
+ """Initializes config"""
+ self.conf = None
+
+ self.config_path = config_path
+ self.log_path = logfile
+
+ @property
+ def cfg(self):
+ # Return existing loaded config
+ if self.conf:
+ return self.conf
+
+ # Built initial config if it doesn't exist
+ if self.build_config():
+ print("Please edit the default configuration before running again!")
+ sys.exit(0)
+ # Load config, upgrade if necessary
+ else:
+ tmp = self.load_config()
+ self.conf, upgraded = self.upgrade_settings(tmp)
+
+ # Save config if upgraded
+ if upgraded:
+ self.dump_config()
+ print("New config options were added, adjust and restart!")
+ sys.exit(0)
+
+ return self.conf
+
+ @property
+ def logfile(self):
+ return self.log_path
+
+ def build_config(self):
+ if not os.path.exists(self.config_path):
+ print("Dumping default config to: %s" % self.config_path)
+ with open(self.config_path, 'w') as fp:
+ json.dump(self.base_config, fp, sort_keys=True, indent=2)
+ return True
+ else:
+ return False
+
+ def dump_config(self):
+ if os.path.exists(self.config_path):
+ with open(self.config_path, 'w') as fp:
+ json.dump(self.conf, fp, sort_keys=True, indent=2)
+ return True
+ else:
+ return False
+
+ def load_config(self):
+ with open(self.config_path, 'r') as fp:
+ return AttrConfig(json.load(fp))
+
+ def __inner_upgrade(self, settings1, settings2, key=None, overwrite=False):
+ sub_upgraded = False
+ merged = settings2.copy()
+
+ if isinstance(settings1, dict):
+ for k, v in settings1.items():
+ # missing k
+ if k not in settings2:
+ merged[k] = v
+ sub_upgraded = True
+ if not key:
+ print("Added %r config option: %s" % (str(k), str(v)))
+ else:
+ print("Added %r to config option %r: %s" %
+ (str(k), str(key), str(v)))
+ continue
+
+ # iterate children
+ if isinstance(v, dict) or isinstance(v, list):
+ merged[k], did_upgrade = self.__inner_upgrade(settings1[k], settings2[k], key=k,
+ overwrite=overwrite)
+ sub_upgraded = did_upgrade if did_upgrade else sub_upgraded
+ elif settings1[k] != settings2[k] and overwrite:
+ merged = settings1
+ sub_upgraded = True
+ elif isinstance(settings1, list) and key:
+ for v in settings1:
+ if v not in settings2:
+ merged.append(v)
+ sub_upgraded = True
+ print("Added to config option %r: %s" % (str(key), str(v)))
+ continue
+
+ return merged, sub_upgraded
+
+ def upgrade_settings(self, currents):
+ upgraded_settings, upgraded = self.__inner_upgrade(
+ self.base_config, currents)
+ return AttrConfig(upgraded_settings), upgraded
+
+ def merge_settings(self, settings_to_merge):
+ upgraded_settings, upgraded = self.__inner_upgrade(
+ settings_to_merge, self.conf, overwrite=True)
+
+ self.conf = upgraded_settings
+
+ if upgraded:
+ self.dump_config()
+
+ return AttrConfig(upgraded_settings), upgraded
diff --git a/menu/pgtrakt/misc/log.py b/menu/pgtrakt/misc/log.py
new file mode 100644
index 00000000..46eea9e1
--- /dev/null
+++ b/menu/pgtrakt/misc/log.py
@@ -0,0 +1,55 @@
+import logging
+import os
+import sys
+from logging.handlers import RotatingFileHandler
+
+from misc.config import Config
+
+
+class Logger:
+ def __init__(self, file_name=None, log_level=logging.DEBUG,
+ log_format='%(asctime)s - %(levelname)-10s - %(name)-35s - %(funcName)-35s - %(message)s'):
+ self.log_format = log_format
+
+ # init root_logger
+ self.log_formatter = logging.Formatter(log_format)
+ self.root_logger = logging.getLogger()
+ self.root_logger.setLevel(log_level)
+
+ # disable bloat loggers
+ logging.getLogger("requests").setLevel(logging.WARNING)
+ logging.getLogger('urllib3').setLevel(logging.ERROR)
+ logging.getLogger('schedule').setLevel(logging.ERROR)
+
+ # init console_logger
+ self.console_handler = logging.StreamHandler(sys.stdout)
+ self.console_handler.setFormatter(self.log_formatter)
+ self.root_logger.addHandler(self.console_handler)
+
+ # init file_logger
+ if file_name:
+ if os.path.sep not in file_name:
+ # file_name was a filename, lets build a full file_path
+ self.log_file_path = os.path.join(os.path.dirname(
+ os.path.realpath(sys.argv[0])), file_name)
+ else:
+ self.log_file_path = file_name
+
+ self.file_handler = RotatingFileHandler(
+ self.log_file_path,
+ maxBytes=1024 * 1024 * 5,
+ backupCount=5
+ )
+ self.file_handler.setFormatter(self.log_formatter)
+ self.root_logger.addHandler(self.file_handler)
+
+ # Set chosen logging level
+ self.root_logger.setLevel(log_level)
+
+ def get_logger(self, name):
+ return self.root_logger.getChild(name)
+
+
+# Default logger
+logger = Logger(Config().logfile,
+ logging.DEBUG if Config().cfg.core.debug else logging.INFO)
diff --git a/menu/pgtrakt/notifications/__init__.py b/menu/pgtrakt/notifications/__init__.py
new file mode 100644
index 00000000..79a1e5d0
--- /dev/null
+++ b/menu/pgtrakt/notifications/__init__.py
@@ -0,0 +1,58 @@
+from misc.log import logger
+
+from .pushover import Pushover
+from .slack import Slack
+
+log = logger.get_logger(__name__)
+
+SERVICES = {
+ 'pushover': Pushover,
+ 'slack': Slack
+}
+
+
+class Notifications:
+ def __init__(self):
+ self.services = []
+
+ def load(self, **kwargs):
+ if 'service' not in kwargs:
+ log.error(
+ "You must specify a service to load with the service parameter")
+ return False
+ elif kwargs['service'] not in SERVICES:
+ log.error("You specified an invalid service to load: %s",
+ kwargs['service'])
+ return False
+
+ try:
+ chosen_service = SERVICES[kwargs['service']]
+ del kwargs['service']
+
+ # load service
+ service = chosen_service(**kwargs)
+ self.services.append(service)
+
+ except Exception:
+ log.exception(
+ "Exception while loading service, kwargs=%r: ", kwargs)
+
+ def send(self, **kwargs):
+ try:
+ # remove service keyword if supplied
+ if 'service' in kwargs:
+ # send notification to specified service
+ chosen_service = kwargs['service'].lower()
+ del kwargs['service']
+ else:
+ chosen_service = None
+
+ # send notification(s)
+ for service in self.services:
+ if chosen_service and service.NAME.lower() != chosen_service:
+ continue
+ elif service.send(**kwargs):
+ log.debug("Sent notification with %s", service.NAME)
+ except Exception:
+ log.exception(
+ "Exception sending notification, kwargs=%r: ", kwargs)
diff --git a/menu/pgtrakt/notifications/pushover.py b/menu/pgtrakt/notifications/pushover.py
new file mode 100644
index 00000000..ce6230f1
--- /dev/null
+++ b/menu/pgtrakt/notifications/pushover.py
@@ -0,0 +1,37 @@
+import requests
+
+from misc.log import logger
+
+log = logger.get_logger(__name__)
+
+
+class Pushover:
+ NAME = "Pushover"
+
+ def __init__(self, app_token, user_token, priority=0):
+ self.app_token = app_token
+ self.user_token = user_token
+ self.priority = priority
+ log.debug("Initialized Pushover notification agent")
+
+ def send(self, **kwargs):
+ if not self.app_token or not self.user_token:
+ log.error(
+ "You must specify an app_token and user_token when initializing this class")
+ return False
+
+ # send notification
+ try:
+ payload = {
+ 'token': self.app_token,
+ 'user': self.user_token,
+ 'message': kwargs['message'],
+ 'priority': self.priority,
+ }
+ resp = requests.post(
+ 'https://api.pushover.net/1/messages.json', data=payload, timeout=30)
+ return True if resp.status_code == 200 else False
+
+ except Exception:
+ log.exception("Error sending notification to %r", self.user_token)
+ return False
diff --git a/menu/pgtrakt/notifications/slack.py b/menu/pgtrakt/notifications/slack.py
new file mode 100644
index 00000000..cfba9c02
--- /dev/null
+++ b/menu/pgtrakt/notifications/slack.py
@@ -0,0 +1,39 @@
+import requests
+
+from misc.log import logger
+
+log = logger.get_logger(__name__)
+
+
+class Slack:
+ NAME = "Slack"
+
+ def __init__(self, webhook_url, sender_name='traktarr', sender_icon=':movie_camera:', channel=None):
+ self.webhook_url = webhook_url
+ self.sender_name = sender_name
+ self.sender_icon = sender_icon
+ self.channel = channel
+ log.debug("Initialized Slack notification agent")
+
+ def send(self, **kwargs):
+ if not self.webhook_url or not self.sender_name or not self.sender_icon:
+ log.error(
+ "You must specify an webhook_url, sender_name and sender_icon when initializing this class")
+ return False
+
+ # send notification
+ try:
+ payload = {
+ 'text': kwargs['message'],
+ 'username': self.sender_name,
+ 'icon_emoji': self.sender_icon,
+ }
+ if self.channel:
+ payload['channel'] = self.channel
+
+ resp = requests.post(self.webhook_url, json=payload, timeout=30)
+ return True if resp.status_code == 200 else False
+
+ except Exception:
+ log.exception("Error sending notification to %r", self.webhook_url)
+ return False
diff --git a/menu/pgtrakt/pgtrakt.py b/menu/pgtrakt/pgtrakt.py
new file mode 100644
index 00000000..63076a8f
--- /dev/null
+++ b/menu/pgtrakt/pgtrakt.py
@@ -0,0 +1,952 @@
+#!/usr/bin/env python3
+import os.path
+import signal
+import sys
+import time
+
+import click
+import schedule
+
+from pyfiglet import Figlet
+
+############################################################
+# INIT
+############################################################
+cfg = None
+log = None
+notify = None
+
+
+# Click
+@click.group(help='Add new shows & movies to Sonarr/Radarr from Trakt.')
+@click.version_option('1.2.3', prog_name='pgtrakt')
+@click.option(
+ '--config',
+ envvar='TRAKTARR_CONFIG',
+ type=click.Path(file_okay=True, dir_okay=False),
+ help='Configuration file',
+ show_default=True,
+ default=os.path.join(os.path.dirname(
+ os.path.realpath(sys.argv[0])), "config.json")
+)
+@click.option(
+ '--logfile',
+ envvar='TRAKTARR_LOGFILE',
+ type=click.Path(file_okay=True, dir_okay=False),
+ help='Log file',
+ show_default=True,
+ default=os.path.join(os.path.dirname(
+ os.path.realpath(sys.argv[0])), "activity.log")
+)
+def app(config, logfile):
+ # Setup global variables
+ global cfg, log, notify
+
+ # Load config
+ from misc.config import Config
+ cfg = Config(config_path=config, logfile=logfile).cfg
+
+ # Load logger
+ from misc.log import logger
+ log = logger.get_logger('pgtrack')
+
+ # Load notifications
+ from notifications import Notifications
+ notify = Notifications()
+
+ # Notifications
+ init_notifications()
+
+
+############################################################
+# Trakt OAuth
+############################################################
+
+@app.command(help='Authenticate traktarr.')
+def trakt_authentication():
+ from media.trakt import Trakt
+ trakt = Trakt(cfg)
+
+ if trakt.oauth_authentication():
+ log.info("Authentication information saved; please restart the application")
+ exit()
+
+
+def validate_trakt(trakt, notifications):
+ if not trakt.validate_client_id():
+ log.error("Aborting due to failure to validate Trakt API Key")
+ if notifications:
+ callback_notify(
+ {'event': 'error', 'reason': 'Failure to validate Trakt API Key'})
+ exit()
+ else:
+ log.info("Validated Trakt API Key")
+
+
+def validate_pvr(pvr, type, notifications):
+ if not pvr.validate_api_key():
+ log.error("Aborting due to failure to validate %s URL / API Key", type)
+ if notifications:
+ callback_notify(
+ {'event': 'error', 'reason': 'Failure to validate %s URL / API Key' % type})
+ return None
+ else:
+ log.info("Validated %s URL & API Key", type)
+
+
+def get_profile_id(pvr, profile):
+ # retrieve profile id for requested profile
+ profile_id = pvr.get_profile_id(profile)
+ if not profile_id or not profile_id > 0:
+ log.error("Aborting due to failure to retrieve Profile ID for: %s", profile)
+ exit()
+ log.info("Retrieved Profile ID for %s: %d", profile, profile_id)
+ return profile_id
+
+
+def get_profile_tags(pvr):
+ profile_tags = pvr.get_tags()
+ if profile_tags is None:
+ log.error("Aborting due to failure to retrieve Tag ID's")
+ exit()
+ log.info("Retrieved %d Tag ID's", len(profile_tags))
+ return profile_tags
+
+
+def get_objects(pvr, type, notifications):
+ objects_list = pvr.get_objects()
+ if not objects_list:
+ log.error("Aborting due to failure to retrieve %s shows list", type)
+ if notifications:
+ callback_notify(
+ {'event': 'error', 'reason': 'Failure to retrieve %s shows list' % type})
+ exit()
+ objects_type = 'movies' if type.lower() == 'radarr' else 'shows'
+ log.info("Retrieved %s %s list, %s found: %d", type,
+ objects_type, objects_type, len(objects_list))
+ return objects_list
+
+
+############################################################
+# SHOWS
+############################################################
+
+@app.command(help='Add a single show to Sonarr.')
+@click.option('--show_id', '-id', help='Trakt show_id.', required=True)
+@click.option('--folder', '-f', default=None, help='Add show with this root folder to Sonarr.')
+@click.option('--no-search', is_flag=True, help='Disable search when adding show to Sonarr.')
+def show(show_id, folder=None, no_search=False):
+ from media.sonarr import Sonarr
+ from media.trakt import Trakt
+ from helpers import sonarr as sonarr_helper
+
+ # replace sonarr root_folder if folder is supplied
+ if folder:
+ cfg['sonarr']['root_folder'] = folder
+
+ trakt = Trakt(cfg)
+ sonarr = Sonarr(cfg.sonarr.url, cfg.sonarr.api_key)
+
+ validate_trakt(trakt, False)
+ validate_pvr(sonarr, 'Sonarr', False)
+
+ profile_id = get_profile_id(sonarr, cfg.sonarr.profile)
+ profile_tags = get_profile_tags(sonarr)
+
+ # get trakt show
+ trakt_show = trakt.get_show(show_id)
+
+ if not trakt_show:
+ log.error("Aborting due to failure to retrieve Trakt show")
+ return None
+ else:
+ log.info("Retrieved Trakt show information for %s: %s (%d)", show_id, trakt_show['title'],
+ trakt_show['year'])
+
+ # determine which tags to use when adding this series
+ use_tags = sonarr_helper.series_tag_id_from_network(
+ profile_tags, cfg.sonarr.tags, trakt_show['network'])
+
+ # add show to sonarr
+ if sonarr.add_series(trakt_show['ids']['tvdb'], trakt_show['title'], trakt_show['ids']['slug'], profile_id,
+ cfg.sonarr.root_folder, use_tags, not no_search):
+ log.info("ADDED %s (%d) with tags: %s", trakt_show['title'], trakt_show['year'],
+ sonarr_helper.readable_tag_from_ids(profile_tags, use_tags))
+ else:
+ log.error("FAILED adding %s (%d) with tags: %s", trakt_show['title'], trakt_show['year'],
+ sonarr_helper.readable_tag_from_ids(profile_tags, use_tags))
+
+ return
+
+
+@app.command(help='Add multiple shows to Sonarr.')
+@click.option('--list-type', '-t',
+ help='Trakt list to process. For example, anticipated, trending, popular, person, watched, played, '
+ 'recommended, watchlist or any URL to a list', required=True)
+@click.option('--add-limit', '-l', default=0, help='Limit number of shows added to Sonarr.', show_default=True)
+@click.option('--add-delay', '-d', default=2.5, help='Seconds between each add request to Sonarr.', show_default=True)
+@click.option('--sort', '-s', default='votes', type=click.Choice(['votes', 'rating', 'release']),
+ help='Sort list to process.')
+@click.option('--genre', '-g', default=None, help='Only add shows from this genre to Sonarr.')
+@click.option('--folder', '-f', default=None, help='Add shows with this root folder to Sonarr.')
+@click.option('--actor', '-a', default=None, help='Only add movies from this actor to Radarr.')
+@click.option('--no-search', is_flag=True, help='Disable search when adding shows to Sonarr.')
+@click.option('--notifications', is_flag=True, help='Send notifications.')
+@click.option('--authenticate-user',
+ help='Specify which user to authenticate with to retrieve Trakt lists. Default: first user in the config')
+@click.option('--ignore-blacklist', is_flag=True, help='Ignores the blacklist when running the command.')
+@click.option('--remove-rejected-from-recommended', is_flag=True,
+ help='Removes rejected/existing shows from recommended.')
+def shows(list_type, add_limit=0, add_delay=2.5, sort='votes', genre=None, folder=None, actor=None, no_search=False,
+ notifications=False, authenticate_user=None, ignore_blacklist=False, remove_rejected_from_recommended=False):
+ from media.sonarr import Sonarr
+ from media.trakt import Trakt
+ from helpers import misc as misc_helper
+ from helpers import sonarr as sonarr_helper
+ from helpers import trakt as trakt_helper
+
+ added_shows = 0
+
+ # remove genre from shows blacklisted_genres if supplied
+ if genre:
+ misc_helper.unblacklist_genres(
+ genre, cfg['filters']['shows']['blacklisted_genres'])
+
+ # replace sonarr root_folder if folder is supplied
+ if folder:
+ cfg['sonarr']['root_folder'] = folder
+
+ # validate trakt client_id
+ trakt = Trakt(cfg)
+ sonarr = Sonarr(cfg.sonarr.url, cfg.sonarr.api_key)
+
+ validate_trakt(trakt, notifications)
+ validate_pvr(sonarr, 'Sonarr', notifications)
+
+ profile_id = get_profile_id(sonarr, cfg.sonarr.profile)
+ profile_tags = get_profile_tags(sonarr)
+
+ pvr_objects_list = get_objects(sonarr, 'Sonarr', notifications)
+
+ # get trakt series list
+ if list_type.lower() == 'anticipated':
+ trakt_objects_list = trakt.get_anticipated_shows(
+ genres=genre, languages=cfg.filters.shows.allowed_languages)
+ elif list_type.lower() == 'trending':
+ trakt_objects_list = trakt.get_trending_shows(
+ genres=genre, languages=cfg.filters.shows.allowed_languages)
+ elif list_type.lower() == 'popular':
+ trakt_objects_list = trakt.get_popular_shows(
+ genres=genre, languages=cfg.filters.shows.allowed_languages)
+ elif list_type.lower() == 'person':
+ if not actor:
+ log.error(
+ "You must specify an actor with the --actor / -a parameter when using the person list type!")
+ return None
+ trakt_objects_list = trakt.get_person_shows(person=actor, genres=genre,
+ languages=cfg.filters.shows.allowed_languages)
+ elif list_type.lower() == 'recommended':
+ trakt_objects_list = trakt.get_recommended_shows(authenticate_user, genres=genre,
+ languages=cfg.filters.shows.allowed_languages)
+ elif list_type.lower().startswith('played'):
+ most_type = misc_helper.substring_after(list_type.lower(), "_")
+ trakt_objects_list = trakt.get_most_played_shows(genres=genre, languages=cfg.filters.shows.allowed_languages,
+ most_type=most_type if most_type else None)
+ elif list_type.lower().startswith('watched'):
+ most_type = misc_helper.substring_after(list_type.lower(), "_")
+ trakt_objects_list = trakt.get_most_watched_shows(genres=genre, languages=cfg.filters.shows.allowed_languages,
+ most_type=most_type if most_type else None)
+ elif list_type.lower() == 'watchlist':
+ trakt_objects_list = trakt.get_watchlist_shows(authenticate_user)
+ else:
+ trakt_objects_list = trakt.get_user_list_shows(
+ list_type, authenticate_user)
+
+ if not trakt_objects_list:
+ log.error(
+ "Aborting due to failure to retrieve Trakt %s shows list", list_type)
+ if notifications:
+ callback_notify(
+ {'event': 'abort', 'type': 'shows', 'list_type': list_type,
+ 'reason': 'Failure to retrieve Trakt %s shows list' % list_type})
+ return None
+ else:
+ log.info("Retrieved Trakt %s shows list, shows found: %d",
+ list_type, len(trakt_objects_list))
+
+ # set remove_rejected_recommended to False if this is not the recommended list
+ if list_type.lower() != 'recommended':
+ remove_rejected_from_recommended = False
+
+ # build filtered series list without series that exist in sonarr
+ processed_series_list = sonarr_helper.remove_existing_series(pvr_objects_list, trakt_objects_list,
+ callback_remove_recommended
+ if remove_rejected_from_recommended else None)
+ if processed_series_list is None:
+ log.error(
+ "Aborting due to failure to remove existing Sonarr shows from retrieved Trakt shows list")
+ if notifications:
+ callback_notify({'event': 'abort', 'type': 'shows', 'list_type': list_type,
+ 'reason': 'Failure to remove existing Sonarr shows from retrieved Trakt %s shows list'
+ % list_type})
+ return None
+ else:
+ log.info("Removed existing Sonarr shows from Trakt shows list, shows left to process: %d",
+ len(processed_series_list))
+
+ # sort filtered series list
+ if sort == 'release':
+ sorted_series_list = misc_helper.sorted_list(
+ processed_series_list, 'show', 'first_aired')
+ log.info("Sorted shows list to process by release date")
+ elif sort == 'rating':
+ sorted_series_list = misc_helper.sorted_list(
+ processed_series_list, 'show', 'rating')
+ log.info("Sorted shows list to process by highest rating")
+ else:
+ sorted_series_list = misc_helper.sorted_list(
+ processed_series_list, 'show', 'votes')
+ log.info("Sorted shows list to process by highest votes")
+
+ # loop series_list
+ log.info("Processing list now...")
+ for series in sorted_series_list:
+ try:
+ # check if genre matches genre supplied via argument
+ if genre and not misc_helper.allowed_genres(genre, 'show', series):
+ log.debug("Skipping: %s because it was not from %s genre(s)",
+ series['show']['title'], genre.lower())
+ continue
+
+ # check if series passes out blacklist criteria inspection
+ if not trakt_helper.is_show_blacklisted(series, cfg.filters.shows, ignore_blacklist,
+ callback_remove_recommended
+ if remove_rejected_from_recommended else None):
+ log.info("Adding: %s | Genres: %s | Network: %s | Country: %s", series['show']['title'],
+ ', '.join(series['show']['genres']
+ ), series['show']['network'],
+ series['show']['country'].upper())
+
+ # determine which tags to use when adding this series
+ use_tags = sonarr_helper.series_tag_id_from_network(profile_tags, cfg.sonarr.tags,
+ series['show']['network'])
+ # add show to sonarr
+ if sonarr.add_series(series['show']['ids']['tvdb'], series['show']['title'],
+ series['show']['ids']['slug'], profile_id, cfg.sonarr.root_folder, use_tags,
+ not no_search):
+ log.info("ADDED %s (%d) with tags: %s", series['show']['title'], series['show']['year'],
+ sonarr_helper.readable_tag_from_ids(profile_tags, use_tags))
+ if notifications:
+ callback_notify(
+ {'event': 'add_show', 'list_type': list_type, 'show': series['show']})
+ added_shows += 1
+ else:
+ log.error("FAILED adding %s (%d) with tags: %s", series['show']['title'], series['show']['year'],
+ sonarr_helper.readable_tag_from_ids(profile_tags, use_tags))
+
+ # stop adding shows, if added_shows >= add_limit
+ if add_limit and added_shows >= add_limit:
+ break
+
+ # sleep before adding any more
+ time.sleep(add_delay)
+
+ except Exception:
+ log.exception("Exception while processing show %s: ",
+ series['show']['title'])
+
+ log.info("Added %d new show(s) to Sonarr", added_shows)
+
+ # send notification
+ if notifications:
+ notify.send(message="Added %d shows from Trakt's %s list" %
+ (added_shows, list_type))
+
+ return added_shows
+
+
+############################################################
+# MOVIES
+############################################################
+
+@app.command(help='Add a single movie to Radarr.')
+@click.option('--movie_id', '-id', help='Trakt movie_id.', required=True)
+@click.option('--folder', '-f', default=None, help='Add movie with this root folder to Radarr.')
+@click.option('--no-search', is_flag=True, help='Disable search when adding movie to Radarr.')
+def movie(movie_id, folder=None, no_search=False):
+ from media.radarr import Radarr
+ from media.trakt import Trakt
+
+ # replace radarr root_folder if folder is supplied
+ if folder:
+ cfg['radarr']['root_folder'] = folder
+
+ # validate trakt api_key
+ trakt = Trakt(cfg)
+ radarr = Radarr(cfg.radarr.url, cfg.radarr.api_key)
+
+ validate_trakt(trakt, False)
+ validate_pvr(radarr, 'Radarr', False)
+
+ profile_id = get_profile_id(radarr, cfg.radarr.profile)
+
+ # get trakt movie
+ trakt_movie = trakt.get_movie(movie_id)
+
+ if not trakt_movie:
+ log.error("Aborting due to failure to retrieve Trakt movie")
+ return None
+ else:
+ log.info("Retrieved Trakt movie information for %s: %s (%d)", movie_id, trakt_movie['title'],
+ trakt_movie['year'])
+
+ # add movie to radarr
+ if radarr.add_movie(trakt_movie['ids']['tmdb'], trakt_movie['title'], trakt_movie['year'],
+ trakt_movie['ids']['slug'], profile_id, cfg.radarr.root_folder, not no_search):
+ log.info("ADDED %s (%d)", trakt_movie['title'], trakt_movie['year'])
+ else:
+ log.error("FAILED adding %s (%d)",
+ trakt_movie['title'], trakt_movie['year'])
+
+ return
+
+
+@app.command(help='Add multiple movies to Radarr.')
+@click.option('--list-type', '-t',
+ help='Trakt list to process. For example, anticipated, trending, popular, boxoffice, person, watched, '
+ 'recommended, played, watchlist or any URL to a list', required=True)
+@click.option('--add-limit', '-l', default=0, help='Limit number of movies added to Radarr.', show_default=True)
+@click.option('--add-delay', '-d', default=2.5, help='Seconds between each add request to Radarr.', show_default=True)
+@click.option('--sort', '-s', default='votes', type=click.Choice(['votes', 'rating', 'release']),
+ help='Sort list to process.')
+@click.option('--rating', '-r', default=None, type=(int), help='Set a minimum rating threshold (according to Rotten Tomatoes)')
+@click.option('--genre', '-g', default=None, help='Only add movies from this genre to Radarr.')
+@click.option('--folder', '-f', default=None, help='Add movies with this root folder to Radarr.')
+@click.option('--actor', '-a', default=None, help='Only add movies from this actor to Radarr.')
+@click.option('--no-search', is_flag=True, help='Disable search when adding movies to Radarr.')
+@click.option('--notifications', is_flag=True, help='Send notifications.')
+@click.option('--authenticate-user',
+ help='Specify which user to authenticate with to retrieve Trakt lists. Default: first user in the config.')
+@click.option('--ignore-blacklist', is_flag=True, help='Ignores the blacklist when running the command.')
+@click.option('--remove-rejected-from-recommended', is_flag=True,
+ help='Removes rejected/existing movies from recommended.')
+def movies(list_type, add_limit=0, add_delay=2.5, sort='votes', rating=None, genre=None, folder=None, actor=None, no_search=False,
+ notifications=False, authenticate_user=None, ignore_blacklist=False, remove_rejected_from_recommended=False):
+ from media.radarr import Radarr
+ from media.trakt import Trakt
+ from helpers import misc as misc_helper
+ from helpers import radarr as radarr_helper
+ from helpers import trakt as trakt_helper
+ from helpers import rating as rating_helper
+
+ added_movies = 0
+
+ # remove genre from movies blacklisted_genres if supplied
+ if genre:
+ misc_helper.unblacklist_genres(
+ genre, cfg['filters']['movies']['blacklisted_genres'])
+
+ # replace radarr root_folder if folder is supplied
+ if folder:
+ cfg['radarr']['root_folder'] = folder
+
+ # validate trakt api_key
+ trakt = Trakt(cfg)
+ radarr = Radarr(cfg.radarr.url, cfg.radarr.api_key)
+
+ validate_trakt(trakt, notifications)
+ validate_pvr(radarr, 'Radarr', notifications)
+
+ profile_id = get_profile_id(radarr, cfg.radarr.profile)
+
+ pvr_objects_list = get_objects(radarr, 'Radarr', notifications)
+
+ # get trakt movies list
+ if list_type.lower() == 'anticipated':
+ trakt_objects_list = trakt.get_anticipated_movies(
+ genres=genre, languages=cfg.filters.movies.allowed_languages)
+ elif list_type.lower() == 'trending':
+ trakt_objects_list = trakt.get_trending_movies(
+ genres=genre, languages=cfg.filters.movies.allowed_languages)
+ elif list_type.lower() == 'popular':
+ trakt_objects_list = trakt.get_popular_movies(
+ genres=genre, languages=cfg.filters.movies.allowed_languages)
+ elif list_type.lower() == 'boxoffice':
+ trakt_objects_list = trakt.get_boxoffice_movies()
+ elif list_type.lower() == 'person':
+ if not actor:
+ log.error(
+ "You must specify an actor with the --actor / -a parameter when using the person list type!")
+ return None
+ trakt_objects_list = trakt.get_person_movies(person=actor, genres=genre,
+ languages=cfg.filters.movies.allowed_languages)
+
+ elif list_type.lower() == 'recommended':
+ trakt_objects_list = trakt.get_recommended_movies(authenticate_user, genres=genre,
+ languages=cfg.filters.movies.allowed_languages)
+ elif list_type.lower().startswith('played'):
+ most_type = misc_helper.substring_after(list_type.lower(), "_")
+ trakt_objects_list = trakt.get_most_played_movies(genres=genre, languages=cfg.filters.movies.allowed_languages,
+ most_type=most_type if most_type else None)
+ elif list_type.lower().startswith('watched'):
+ most_type = misc_helper.substring_after(list_type.lower(), "_")
+ trakt_objects_list = trakt.get_most_watched_movies(genres=genre, languages=cfg.filters.movies.allowed_languages,
+ most_type=most_type if most_type else None)
+ elif list_type.lower() == 'watchlist':
+ trakt_objects_list = trakt.get_watchlist_movies(authenticate_user)
+ else:
+ trakt_objects_list = trakt.get_user_list_movies(
+ list_type, authenticate_user)
+
+ if not trakt_objects_list:
+ log.error(
+ "Aborting due to failure to retrieve Trakt %s movies list", list_type)
+ if notifications:
+ callback_notify(
+ {'event': 'abort', 'type': 'movies', 'list_type': list_type,
+ 'reason': 'Failure to retrieve Trakt %s movies list' % list_type})
+ return None
+ else:
+ log.info("Retrieved Trakt %s movies list, movies found: %d",
+ list_type, len(trakt_objects_list))
+
+ # set remove_rejected_recommended to False if this is not the recommended list
+ if list_type.lower() != 'recommended':
+ remove_rejected_from_recommended = False
+
+ # build filtered movie list without movies that exist in radarr
+ processed_movies_list = radarr_helper.remove_existing_movies(pvr_objects_list, trakt_objects_list,
+ callback_remove_recommended
+ if remove_rejected_from_recommended else None)
+ if processed_movies_list is None:
+ log.error(
+ "Aborting due to failure to remove existing Radarr movies from retrieved Trakt movies list")
+ if notifications:
+ callback_notify({'event': 'abort', 'type': 'movies', 'list_type': list_type,
+ 'reason': 'Failure to remove existing Radarr movies from retrieved '
+ 'Trakt %s movies list' % list_type})
+ return None
+ else:
+ log.info("Removed existing Radarr movies from Trakt movies list, movies left to process: %d",
+ len(processed_movies_list))
+
+ # sort filtered movie list
+ if sort == 'release':
+ sorted_movies_list = misc_helper.sorted_list(
+ processed_movies_list, 'movie', 'released')
+ log.info("Sorted movies list to process by release date")
+ elif sort == 'rating':
+ sorted_movies_list = misc_helper.sorted_list(
+ processed_movies_list, 'movie', 'rating')
+ log.info("Sorted movies list to process by highest rating")
+ else:
+ sorted_movies_list = misc_helper.sorted_list(
+ processed_movies_list, 'movie', 'votes')
+ log.info("Sorted movies list to process by highest votes")
+
+ # loop movies
+ log.info("Processing list now...")
+ for movie in sorted_movies_list:
+ try:
+ # check if genre matches genre supplied via argument
+ if genre and not misc_helper.allowed_genres(genre, 'movie', movie):
+ log.debug("Skipping: %s because it was not from %s genre(s)",
+ movie['movie']['title'], genre.lower())
+ continue
+
+ # check if movie passes out blacklist criteria inspection
+ if not trakt_helper.is_movie_blacklisted(movie, cfg.filters.movies, ignore_blacklist,
+ callback_remove_recommended if remove_rejected_from_recommended
+ else None):
+ # Assuming the movie is not blacklisted, proceed to pull RT score if the user wishes to restrict
+ movieRating = None
+ if (rating != None and cfg['omdb']['api_key'] != ''):
+ movieRating = rating_helper.get_rating(
+ cfg['omdb']['api_key'], movie)
+ if (movieRating == -1):
+ log.debug("Skipping: %s because it did not have a rating/lacked imdbID",
+ movie['movie']['title'])
+ continue
+ if (rating == None or movieRating >= rating):
+ log.info("Adding: %s (%d) | Genres: %s | Country: %s", movie['movie']['title'], movie['movie']['year'],
+ ', '.join(movie['movie']['genres']), movie['movie']['country'].upper())
+ # add movie to radarr
+ if radarr.add_movie(movie['movie']['ids']['tmdb'], movie['movie']['title'], movie['movie']['year'],
+ movie['movie']['ids']['slug'], profile_id, cfg.radarr.root_folder, not no_search):
+ log.info(
+ "ADDED %s (%d)", movie['movie']['title'], movie['movie']['year'])
+ if notifications:
+ callback_notify(
+ {'event': 'add_movie', 'list_type': list_type, 'movie': movie['movie']})
+ added_movies += 1
+ else:
+ log.error("FAILED adding %s (%d)",
+ movie['movie']['title'], movie['movie']['year'])
+ else:
+ log.info("SKIPPING: %s (%d) | Genres: %s | Country: %s", movie['movie']['title'],
+ movie['movie']['year'],
+ ', '.join(movie['movie']['genres']), movie['movie']['country'].upper())
+ # stop adding movies, if added_movies >= add_limit
+ if add_limit and added_movies >= add_limit:
+ break
+
+ # sleep before adding any more
+ time.sleep(add_delay)
+
+ except Exception:
+ log.exception("Exception while processing movie %s: ",
+ movie['movie']['title'])
+
+ log.info("Added %d new movie(s) to Radarr", added_movies)
+
+ # send notification
+ if notifications:
+ notify.send(message="Added %d movies from Trakt's %s list" %
+ (added_movies, list_type))
+
+ return added_movies
+
+
+############################################################
+# CALLBACKS
+############################################################
+
+
+def callback_remove_recommended(media_type, media_info):
+ from media.trakt import Trakt
+
+ trakt = Trakt(cfg)
+
+ if not media_info[media_type]['title'] or not media_info[media_type]['year']:
+ log.debug("Skipping removing %s item from recommended list as no title/year was available:\n%s", media_type,
+ media_info)
+ return
+
+ media_name = '%s (%d)' % (
+ media_info[media_type]['title'], media_info[media_type]['year'])
+
+ if trakt.remove_recommended_item(media_type, media_info[media_type]['ids']['trakt']):
+ log.info("Removed rejected recommended %s: %s", media_type, media_name)
+ else:
+ log.info("FAILED removing rejected recommended %s: %s",
+ media_type, media_name)
+
+
+def callback_notify(data):
+ log.debug("Received callback data: %s", data)
+
+ # handle event
+ if data['event'] == 'add_movie':
+ if cfg.notifications.verbose:
+ notify.send(
+ message="Added %s movie: %s (%d)" % (data['list_type'], data['movie']['title'], data['movie']['year']))
+ return
+ elif data['event'] == 'add_show':
+ if cfg.notifications.verbose:
+ notify.send(
+ message="Added %s show: %s (%d)" % (data['list_type'], data['show']['title'], data['show']['year']))
+ return
+ elif data['event'] == 'abort':
+ notify.send(message="Aborted adding Trakt %s %s due to: %s" %
+ (data['list_type'], data['type'], data['reason']))
+ return
+ elif data['event'] == 'error':
+ notify.send(message="Error: %s" % data['reason'])
+ return
+ else:
+ log.error("Unexpected callback: %s", data)
+ return
+
+
+############################################################
+# AUTOMATIC
+############################################################
+
+
+def automatic_shows(add_delay=2.5, sort='votes', no_search=False, notifications=False, ignore_blacklist=False):
+ from media.trakt import Trakt
+
+ total_shows_added = 0
+ try:
+ log.info("Started")
+
+ for list_type, value in cfg.automatic.shows.items():
+ added_shows = None
+
+ if list_type.lower() == 'interval':
+ continue
+
+ if list_type.lower() in Trakt.non_user_lists or (
+ '_' in list_type and list_type.lower().partition("_")[0] in Trakt.non_user_lists):
+ limit = value
+
+ if limit <= 0:
+ log.info("Skipped Trakt's %s shows list", list_type)
+ continue
+ else:
+ log.info("Adding %d shows from Trakt's %s list",
+ limit, list_type)
+
+ local_ignore_blacklist = ignore_blacklist
+
+ if list_type.lower() in cfg.filters.shows.disabled_for:
+ local_ignore_blacklist = True
+
+ # run shows
+ added_shows = shows.callback(list_type=list_type, add_limit=limit,
+ add_delay=add_delay, sort=sort, no_search=no_search,
+ notifications=notifications, ignore_blacklist=local_ignore_blacklist)
+ elif list_type.lower() == 'watchlist':
+ for authenticate_user, limit in value.items():
+ if limit <= 0:
+ log.info("Skipped Trakt's %s for %s",
+ list_type, authenticate_user)
+ continue
+ else:
+ log.info("Adding %d shows from the %s from %s",
+ limit, list_type, authenticate_user)
+
+ local_ignore_blacklist = ignore_blacklist
+
+ if "watchlist:%s".format(authenticate_user) in cfg.filters.shows.disabled_for:
+ local_ignore_blacklist = True
+
+ # run shows
+ added_shows = shows.callback(list_type=list_type, add_limit=limit,
+ add_delay=add_delay, sort=sort, no_search=no_search,
+ notifications=notifications, authenticate_user=authenticate_user,
+ ignore_blacklist=local_ignore_blacklist)
+ elif list_type.lower() == 'lists':
+ for list, v in value.items():
+ if isinstance(v, dict):
+ authenticate_user = v['authenticate_user']
+ limit = v['limit']
+ else:
+ authenticate_user = None
+ limit = v
+
+ local_ignore_blacklist = ignore_blacklist
+
+ if "list:%s".format(list) in cfg.filters.shows.disabled_for:
+ local_ignore_blacklist = True
+
+ # run shows
+ added_shows = shows.callback(list_type=list, add_limit=limit,
+ add_delay=add_delay, sort=sort, no_search=no_search,
+ notifications=notifications, authenticate_user=authenticate_user,
+ ignore_blacklist=local_ignore_blacklist)
+
+ if added_shows is None:
+ log.error("Failed adding shows from Trakt's %s list", list_type)
+ time.sleep(10)
+ continue
+ total_shows_added += added_shows
+
+ # sleep
+ time.sleep(10)
+
+ log.info("Finished, added %d shows total to Sonarr!", total_shows_added)
+ # send notification
+ if notifications:
+ notify.send(message="Added %d shows total to Sonarr!" %
+ total_shows_added)
+
+ except Exception:
+ log.exception("Exception while automatically adding shows: ")
+ return
+
+
+def automatic_movies(add_delay=2.5, sort='votes', no_search=False, notifications=False, ignore_blacklist=False, rating_limit=None):
+ from media.trakt import Trakt
+
+ total_movies_added = 0
+ try:
+ log.info("Started")
+
+ for list_type, value in cfg.automatic.movies.items():
+ added_movies = None
+
+ if list_type.lower() == 'interval':
+ continue
+
+ if list_type.lower() in Trakt.non_user_lists or (
+ '_' in list_type and list_type.lower().partition("_")[0] in Trakt.non_user_lists):
+ limit = value
+
+ if limit <= 0:
+ log.info("Skipped Trakt's %s movies list", list_type)
+ continue
+ else:
+ log.info("Adding %d movies from Trakt's %s list",
+ limit, list_type)
+
+ local_ignore_blacklist = ignore_blacklist
+
+ if list_type.lower() in cfg.filters.movies.disabled_for:
+ local_ignore_blacklist = True
+
+ # run movies
+ added_movies = movies.callback(list_type=list_type, add_limit=limit,
+ add_delay=add_delay, sort=sort, no_search=no_search,
+ notifications=notifications, rating=rating_limit)
+ elif list_type.lower() == 'watchlist':
+ for authenticate_user, limit in value.items():
+ if limit <= 0:
+ log.info("Skipped Trakt's %s for %s",
+ list_type, authenticate_user)
+ continue
+ else:
+ log.info("Adding %d movies from the %s from %s",
+ limit, list_type, authenticate_user)
+
+ local_ignore_blacklist = ignore_blacklist
+
+ if "watchlist:%s".format(authenticate_user) in cfg.filters.movies.disabled_for:
+ local_ignore_blacklist = True
+
+ # run movies
+ added_movies = movies.callback(list_type=list_type, add_limit=limit,
+ add_delay=add_delay, sort=sort, no_search=no_search,
+ notifications=notifications, authenticate_user=authenticate_user,
+ ignore_blacklist=local_ignore_blacklist, rating=rating_limit)
+ elif list_type.lower() == 'lists':
+ for list, v in value.items():
+ if isinstance(v, dict):
+ authenticate_user = v['authenticate_user']
+ limit = v['limit']
+ else:
+ authenticate_user = None
+ limit = v
+
+ local_ignore_blacklist = ignore_blacklist
+
+ if "list:%s".format(list) in cfg.filters.movies.disabled_for:
+ local_ignore_blacklist = True
+
+ # run shows
+ added_movies = movies.callback(list_type=list, add_limit=limit,
+ add_delay=add_delay, sort=sort, no_search=no_search,
+ notifications=notifications, authenticate_user=authenticate_user,
+ ignore_blacklist=local_ignore_blacklist, rating=rating_limit)
+
+ if added_movies is None:
+ log.error("Failed adding movies from Trakt's %s list", list_type)
+ time.sleep(10)
+ continue
+ total_movies_added += added_movies
+
+ # sleep
+ time.sleep(10)
+
+ log.info("Finished, added %d movies total to Radarr!",
+ total_movies_added)
+ # send notification
+ if notifications:
+ notify.send(message="Added %d movies total to Radarr!" %
+ total_movies_added)
+
+ except Exception:
+ log.exception("Exception while automatically adding movies: ")
+ return
+
+
+@app.command(help='Run in automatic mode.')
+@click.option('--add-delay', '-d', default=2.5, help='Seconds between each add request to Sonarr / Radarr.',
+ show_default=True)
+@click.option('--sort', '-s', default='votes', type=click.Choice(['votes', 'rating', 'release']),
+ help='Sort list to process.')
+@click.option('--no-search', is_flag=True, help='Disable search when adding to Sonarr / Radarr.')
+@click.option('--run-now', is_flag=True, help="Do a first run immediately without waiting.")
+@click.option('--no-notifications', is_flag=True, help="Disable notifications.")
+@click.option('--ignore-blacklist', is_flag=True, help='Ignores the blacklist when running the command.')
+def run(add_delay=2.5, sort='votes', no_search=False, run_now=False, no_notifications=False, ignore_blacklist=False):
+ log.info("Automatic mode is now running...")
+
+ # Add tasks to schedule and do first run if enabled
+ if cfg.automatic.movies.interval:
+ movie_schedule = schedule.every(cfg.automatic.movies.interval).hours.do(
+ automatic_movies,
+ add_delay,
+ sort,
+ no_search,
+ not no_notifications,
+ ignore_blacklist,
+ int(cfg.filters.movies.rating_limit) if cfg.filters.movies.rating_limit != "" else None
+ )
+ if run_now:
+ movie_schedule.run()
+
+ # Sleep between tasks
+ time.sleep(add_delay)
+
+ if cfg.automatic.shows.interval:
+ shows_schedule = schedule.every(cfg.automatic.shows.interval).hours.do(
+ automatic_shows,
+ add_delay,
+ sort,
+ no_search,
+ not no_notifications,
+ ignore_blacklist
+ )
+ if run_now:
+ shows_schedule.run()
+
+ # Sleep between tasks
+ time.sleep(add_delay)
+
+ # Enter running schedule
+ while True:
+ try:
+ # Sleep until next run
+ log.info("Next job at %s", schedule.next_run())
+ time.sleep(max(schedule.idle_seconds(), 0))
+ # Check jobs to run
+ schedule.run_pending()
+
+ except Exception as e:
+ log.exception(
+ "Unhandled exception occurred while processing scheduled tasks: %s", e)
+ time.sleep(1)
+
+
+############################################################
+# MISC
+############################################################
+
+def init_notifications():
+ try:
+ for notification_name, notification_config in cfg.notifications.items():
+ if notification_name.lower() == 'verbose':
+ continue
+
+ notify.load(**notification_config)
+ except Exception:
+ log.exception("Exception initializing notification agents: ")
+ return
+
+
+# Handles exit signals, cancels jobs and exits cleanly
+def exit_handler(signum, frame):
+ log.info("Received %s, canceling jobs and exiting.",
+ signal.Signals(signum).name)
+ schedule.clear()
+ exit()
+
+
+############################################################
+# MAIN
+############################################################
+
+if __name__ == "__main__":
+
+ print("")
+ print("""PGTrakt Started
+
+""")
+
+ # Register the signal handlers
+ signal.signal(signal.SIGTERM, exit_handler)
+ signal.signal(signal.SIGINT, exit_handler)
+
+ # Start application
+ app()
diff --git a/menu/pgtrakt/pgtrakt.sh b/menu/pgtrakt/pgtrakt.sh
new file mode 100644
index 00000000..d78b7ab2
--- /dev/null
+++ b/menu/pgtrakt/pgtrakt.sh
@@ -0,0 +1,581 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Author(s): Admin9705
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+
+# KEY VARIABLE RECALL & EXECUTION
+mkdir -p /var/plexguide/pgtrakt
+
+# FUNCTIONS START ##############################################################
+
+# FIRST FUNCTION
+variable() {
+ file="$1"
+ if [ ! -e "$file" ]; then echo "$2" >$1; fi
+}
+
+deploycheck() {
+ dcheck=$(systemctl status pgtrakt | grep "\(running\)\>" | grep "\")
+ if [ "$dcheck" != "" ]; then
+ dstatus="✅ DEPLOYED"
+ else dstatus="⚠️ NOT DEPLOYED"; fi
+}
+
+sonarrcheck() {
+ pcheck=$(docker ps | grep "\")
+ if [ "$pcheck" == "" ]; then
+
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+⛔️ WARNING! - Sonarr is not Installed/Running! Cannot Proceed!
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+EOF
+ read -p 'Confirm Info | PRESS [ENTER] ' typed ")
+ if [ "$pcheck" == "" ]; then
+
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+⛔️ WARNING! - Radarr is not Installed/Running! Cannot Proceed!
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+EOF
+ read -p 'Confirm Info | PRESS [ENTER] ' typed exit
+EOF
+ read -p '↘️ Type Sonarr Location | Press [ENTER]: ' typed /var/plexguide/pgtrak.sprofile
+ read -p '🌎 Acknowledge Info | Press [ENTER] ' typed exit
+EOF
+ read -p '↘️ Type Radarr Location | Press [ENTER]: ' typed /var/plexguide/pgtrak.rprofile
+ read -p '🌎 Acknowledge Info | Press [ENTER] ' typed exit
+EOF
+ read -p '↘️ Type API Client | Press [ENTER]: ' typed /var/plexguide/pgtrak.client
+ read -p '↘️ Type API Secret | Press [ENTER]: ' typed /var/plexguide/pgtrak.secret
+
+ if [[ "$typed" == "exit" || "$typed" == "Exit" || "$typed" == "EXIT" || "$typed" == "z" || "$typed" == "Z" ]]; then
+ question1
+ else
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+✅ SYSTEM MESSAGE: PGTrak API Notice
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+NOTE: The API Client and Secret is set! Ensure to setup your and
+ prior to deploying PGTrak.
+
+INFO: Messed up? Rerun this API Interface to update the information!
+
+EOF
+
+ read -p '🌎 Acknowledge Info | Press [ENTER] ' typed exit
+EOF
+ read -p '↘️ Type Sonarr Location | Press [ENTER]: ' typed /dev/null 2>&1
+
+ file="$typed/test"
+ if [ -e "$file" ]; then
+
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+✅ SYSTEM MESSAGE: Sonarr Path Completed!
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+EOF
+
+ ### Removes /mnt if /mnt/unionfs exists
+ #check=$(echo $typed | head -c 12)
+ #if [ "$check" == "/mnt/unionfs" ]; then
+ #typed=${typed:4}
+ #fi
+
+ echo "$typed" >/var/plexguide/pgtrak.spath
+ read -p '🌎 Acknowledge Info | Press [ENTER] ' typed >> mkdir $typed/testfolder
+
+EOF
+ read -p '🌎 Acknowledge Info | Press [ENTER] ' typed exit
+EOF
+ read -p '↘️ Type Radarr Location | Press [ENTER]: ' typed /dev/null 2>&1
+
+ file="$typed/test"
+ if [ -e "$file" ]; then
+
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+✅ SYSTEM MESSAGE: Radarr Path Completed!
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+EOF
+
+ ### Removes /mnt if /mnt/unionfs exists
+ #check=$(echo $typed | head -c 12)
+ #if [ "$check" == "/mnt/unionfs" ]; then
+ #typed=${typed:4}
+ #fi
+
+ echo "$typed" >/var/plexguide/pgtrak.rpath
+ read -p '🌎 Acknowledge Info | Press [ENTER] ' typed >> mkdir $typed/testfolder
+
+EOF
+ read -p '🌎 Acknowledge Info | Press [ENTER] ' typed /var/plexguide/pgtrakt/video.transcodes && question1
+ elif [ "$typed" == "2" ]; then
+ echo "True" >/var/plexguide/pgtrakt/video.transcodes && question1
+ else badinput; fi
+}
+
+selection2() {
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+🚀 Limit Amount of Different IPs a User Can Make?
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+⚡ Reference: http://pgtrakt.pgblitz.com
+
+Set a Number from [1] 99
+
+EOF
+ read -p 'Type Number | PRESS [ENTER] ' typed /var/plexguide/pgtrakt/multiple.ips && question1
+ else badinput; fi
+}
+
+selection3() {
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+🚀 Limit How Long a User Can Pause For!
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+⚡ Reference: http://pgtrakt.pgblitz.com
+
+Set a Number from [5] 999 Mintues
+
+EOF
+ read -p 'Type Number | PRESS [ENTER] ' typed /var/plexguide/pgtrakt/kick.minutes && question1
+ else badinput; fi
+}
+
+# FIRST QUESTION
+question1() {
+
+ api=$(cat /var/plexguide/pgtrak.secret)
+ if [ "$api" == "NOT-SET" ]; then api="NOT-SET"; else api="SET"; fi
+
+ rpath=$(cat /var/plexguide/pgtrak.rpath)
+ spath=$(cat /var/plexguide/pgtrak.spath)
+ rprofile=$(cat /var/plexguide/pgtrak.rprofile)
+ sprofile=$(cat /var/plexguide/pgtrak.sprofile)
+ deploycheck
+
+ tee <<-EOF
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+🚀 PGTrakt Interface
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+⚡ Reference: http://pgtrakt.pgblitz.com
+
+NOTE: Changes Made? Must Redeploy PGTrak when Complete!
+
+[1] Trakt API-Key [$api]
+[2] Sonarr Path [$spath]
+[3] Raddar Path [$rpath]
+[4] Sonarr Profile [$sprofile]
+[5] Radarr Profile [$rprofile]
+[6] Deploy PGTrak [$dstatus]
+Z - Exit
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+EOF
+
+ read -p '↘️ Type Number | Press [ENTER]: ' typed } 1>/dev/null 2>&1
+ info1=$(echo ${info:0:32}) 1>/dev/null 2>&1
+ echo "$info1" >/var/plexguide/pgtrak.rapi
+ fi
+
+ file="/opt/appdata/sonarr/config.xml"
+ if [ -e "$file" ]; then
+ info=$(cat /opt/appdata/sonarr/config.xml)
+ info=${info#*} 1>/dev/null 2>&1
+ info2=$(echo ${info:0:32}) 1>/dev/null 2>&1
+ echo "$info2" >/var/plexguide/pgtrak.sapi
+ fi
+ fi
+ # keys for sonarr and radarr need to be added
+ ansible-playbook /opt/plexguide/menu/pgtrakt/pgtrakt.yml && question1
+
+ elif [[ "$typed" == "Z" || "$typed" == "z" ]]; then
+ exit
+ else badinput; fi
+}
+
+# FUNCTIONS END ##############################################################
+token
+variable /var/plexguide/pgtrak.client "NOT-SET"
+variable /var/plexguide/pgtrak.secret "NOT-SET"
+variable /var/plexguide/pgtrak.rpath "NOT-SET"
+variable /var/plexguide/pgtrak.spath "NOT-SET"
+variable /var/plexguide/pgtrak.sprofile "NOT-SET"
+variable /var/plexguide/pgtrak.rprofile "NOT-SET"
+variable /var/plexguide/pgtrak.rprofile "NOT-SET"
+
+deploycheck
+question1
diff --git a/menu/pgtrakt/pgtrakt.yml b/menu/pgtrakt/pgtrakt.yml
new file mode 100644
index 00000000..0b8dc3e6
--- /dev/null
+++ b/menu/pgtrakt/pgtrakt.yml
@@ -0,0 +1,140 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# YML Author: Admin9705
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+# Origin: https://github.com/l3uddz/plex_patrol
+################################################################################
+---
+- hosts: localhost
+ gather_facts: false
+ tasks:
+ # FACTS ######################################################################
+ - name: 'Set Known Facts'
+ set_fact:
+ pgrole: 'pgtrakt'
+ extport: '0'
+
+ - name: Server ID
+ shell: 'cat /var/plexguide/pg.serverid'
+ register: serverid
+
+ - name: Token Recall
+ shell: 'cat /var/plexguide/plex.token'
+ register: plextoken
+
+ - name: Recall User
+ shell: 'cat /var/plexguide/plex.user'
+ register: user
+
+ - name: Register IP
+ shell: 'cat /var/plexguide/server.ip'
+ register: ipaddress
+
+ - name: Sonarr API
+ shell: 'cat /var/plexguide/pgtrak.sapi'
+ register: sapi
+ ignore_errors: True
+
+ - name: Radarr API
+ shell: 'cat /var/plexguide/pgtrak.rapi'
+ register: rapi
+ ignore_errors: True
+
+ - name: Sonarr Profile
+ shell: 'cat /var/plexguide/pgtrak.sprofile'
+ register: sprofile
+ ignore_errors: True
+
+ - name: Radarr Profile
+ shell: 'cat /var/plexguide/pgtrak.rprofile'
+ register: rprofile
+ ignore_errors: True
+
+ - name: Trakt.tv Client
+ shell: 'cat /var/plexguide/pgtrak.client'
+ register: client
+ ignore_errors: True
+
+ - name: Trakt.tv Secret
+ shell: 'cat /var/plexguide/pgtrak.secret'
+ register: secret
+ ignore_errors: True
+
+ - name: Radarr Path
+ shell: 'cat /var/plexguide/pgtrak.rpath'
+ register: rpath
+ ignore_errors: True
+
+ - name: API Sonnar
+ shell: 'cat /var/plexguide/pgtrak.spath'
+ register: spath
+ ignore_errors: True
+
+ - name: Install pip requirements
+ pip:
+ requirements: /opt/plexguide/menu/pgtrakt/requirements.txt
+ executable: pip3
+
+ - name: 'Including cron job'
+ include_tasks: '/opt/coreapps/apps/_core.yml'
+
+ - name: Create Basic Directories
+ file: 'path={{item}} state=directory mode=0775 owner=1000 group=1000'
+ with_items:
+ - '/opt/appdata/pgtrakt/'
+
+ - name: Transfer Files
+ copy:
+ src: /opt/plexguide/menu/pgtrakt
+ dest: /opt/appdata
+ owner: '1000'
+ group: '1000'
+ mode: a+x
+ force: yes
+
+ - name: Import default config
+ template:
+ src: /opt/appdata/pgtrakt/config.json.sample
+ dest: /opt/appdata/pgtrakt/config.json
+ owner: '1000'
+ group: '1000'
+ mode: 0775
+ force: yes
+
+ - name: Set pgtrak.py as executable
+ file:
+ path: /opt/appdata/pgtrakt/pgtrakt.py
+ owner: '1000'
+ group: '1000'
+ mode: a+x
+
+ - name: 'Create /usr/local/bin symlink'
+ file:
+ src: '/opt/appdata/pgtrakt/pgtrakt.py'
+ dest: '/bin/pgtrakt'
+ state: link
+
+ - name: Check Service's Existance
+ stat:
+ path: '/etc/systemd/systemd/pgtrakt.service'
+ register: pgp
+
+ - name: Stop service
+ service:
+ name: pgtrakt
+ state: stopped
+ when: pgp.stat.exists
+
+ - name: PGTrakt Service
+ template:
+ src: /opt/appdata/pgtrakt/systemd/pgtrakt.service
+ dest: /etc/systemd/system/pgtrakt.service
+ force: yes
+
+ - name: Daemon-Reload
+ systemd: state=stopped name=pgtrakt daemon_reload=yes enabled=no
+
+ - name: Start PGTrakt
+ systemd: state=started name=pgtrakt enabled=yes
diff --git a/menu/pgtrakt/requirements.txt b/menu/pgtrakt/requirements.txt
new file mode 100644
index 00000000..80660023
--- /dev/null
+++ b/menu/pgtrakt/requirements.txt
@@ -0,0 +1,6 @@
+backoff==1.5.0
+schedule==0.5.0
+attrdict==2.0.0
+click==6.7
+requests~=2.20.0
+pyfiglet
diff --git a/menu/pgtrakt/systemd/pgtrakt.service b/menu/pgtrakt/systemd/pgtrakt.service
new file mode 100644
index 00000000..51394c84
--- /dev/null
+++ b/menu/pgtrakt/systemd/pgtrakt.service
@@ -0,0 +1,15 @@
+# /etc/systemd/system/pgtrakt.service
+
+[Unit]
+Description=pgtrakt
+After=network-online.target
+
+[Service]
+Type=simple
+WorkingDirectory=/opt/appdata/pgtrakt
+ExecStart=/usr/bin/python3 /opt/appdata/pgtrakt/pgtrakt.py run
+Restart=always
+RestartSec=10
+
+[Install]
+WantedBy=default.target
diff --git a/menu/pgui/_cron.yml b/menu/pgui/_cron.yml
new file mode 100644
index 00000000..8a3073ab
--- /dev/null
+++ b/menu/pgui/_cron.yml
@@ -0,0 +1,33 @@
+#!/bin/bash
+#
+# Title: PGBlitz (Reference Title File)
+# Authors: Admin9705, Deiteq, and many PGBlitz Contributors
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+---
+- cron:
+ name: Daily G/TDrive used space check | file checker
+ special_time: 'daily'
+ job: 'bash /opt/appdata/pgui/gtused.sh'
+ state: absent
+ ignore_errors: yes
+
+- cron:
+ name: Daily G/TDrive used space check | file checker
+ special_time: 'daily'
+ job: 'bash /opt/appdata/pgui/ckeck.sh'
+ state: absent
+ ignore_errors: yes
+
+- cron:
+ name: Daily check for mgerfs / rclone new version
+ special_time: 'daily'
+ job: 'bash /opt/appdata/pgui/ckeck.sh >/dev/null 2>&1'
+ state: present
+
+- cron:
+ name: Daily G/TDrive used space checker | file & folder
+ special_time: 'daily'
+ job: 'bash /opt/appdata/pgui/gtused.sh >/dev/null 2>&1'
+ state: present
\ No newline at end of file
diff --git a/menu/pgui/check.sh b/menu/pgui/check.sh
new file mode 100644
index 00000000..f684c4fb
--- /dev/null
+++ b/menu/pgui/check.sh
@@ -0,0 +1,46 @@
+#!/bin/bash
+#
+# Title: PTS Community
+# Author: MrDoob
+# URL: WTFH >-!-< why you need this ^^
+# GNU: General Public License v3.0
+#
+################################################################################
+mkdir -p /var/plexguide/checkers
+rm -rf /var/plexguide/checkers/*.log
+#mkdir -p /var/plexguide/checkers
+
+mgversion="$(curl -s https://api.github.com/repos/trapexit/mergerfs/releases/latest | grep -oP '"tag_name": "\K(.*)(?=")')"
+touch /var/plexguide/checkers/mgfs.log
+touch /var/plexguide/checkers/mergerfs.log
+
+mergfs="$(mergerfs -v | grep 'mergerfs version:' | awk '{print $3}')"
+echo "$mergfs" >> /var/plexguide/checkers/mgfs.log
+
+mgstored="$(tail -n 1 /var/plexguide/checkers/mgfs.log)"
+
+if [[ "$mgversion" == "$mgstored" ]];then
+ echo " ✅ No update needed !" >/var/plexguide/checkers/mergerfs.log
+elif [[ "$mgversion" != "$mgstored" ]]; then
+ echo " ⛔ Update possible !" >/var/plexguide/checkers/mergerfs.log
+else echo "stupid line"
+
+fi
+
+
+
+rcversion="$(curl -s https://api.github.com/repos/rclone/rclone/releases/latest | grep -oP '"tag_name": "\K(.*)(?=")')"
+touch /var/plexguide/checkers/rclonestored.log
+touch /var/plexguide/checkers/rclone.log
+rcstored="$(rclone --version | awk '{print $2}' | tail -n 3 | head -n 1 )"
+echo "$rcstored" >> /var/plexguide/checkers/rclonestored.log
+
+rcstored="$(tail -n 1 /var/plexguide/checkers/rclonestored.log)"
+
+if [[ "$rcversion" == "$rcstored" ]]; then
+ echo " ✅ No update needed !" >/var/plexguide/checkers/rclone.log
+elif [[ "rcversion" != "rcstored" ]]; then
+ echo " ⛔ Update possible !" >/var/plexguide/checkers/rclone.log
+else echo "stupid line"
+
+fi
diff --git a/menu/pgui/dynamic.yml b/menu/pgui/dynamic.yml
new file mode 100644
index 00000000..6397d88e
--- /dev/null
+++ b/menu/pgui/dynamic.yml
@@ -0,0 +1,21 @@
+ - name: Update APT package cache
+ apt: update_cache=yes cache_valid_time=600
+
+ - name: Upgrade APT to the latest packages
+ apt: upgrade=dist
+ register: apt_result
+
+ - name: Install a list of packages
+ command: apt-get install -y jq dnsutils ctop
+ register: apt_result
+ changed_when: "'packages will be installed' in apt_result.stdout"
+
+ - name: Autoremove unused packages
+ command: apt-get -y autoremove
+ register: apt_result
+ changed_when: "'packages will be REMOVED' in apt_result.stdout"
+
+ - name: Purge residual kernel packages
+ shell: apt-get remove -y --purge $(dpkg -l | grep "^rc\s*linux-image-" | awk '{print $2}' | tr '\n' ' ')
+ register: apt_result
+ changed_when: "'packages will be REMOVED' in apt_result.stdout"
diff --git a/menu/pgui/gtused.sh b/menu/pgui/gtused.sh
new file mode 100644
index 00000000..068f36df
--- /dev/null
+++ b/menu/pgui/gtused.sh
@@ -0,0 +1,64 @@
+#!/bin/bash
+#
+# Title: PGBlitz (G/TDrive used space)
+# Author(s): Admin9705
+# Coder: MrDoob
+# URL: https://pgblitz.com - http://github.pgblitz.com
+# GNU: General Public License v3.0
+################################################################################
+#functions
+config="/opt/appdata/plexguide/rclone.conf"
+log="/var/plexguide"
+useragent="$(cat /var/plexguide/uagent)"
+
+#if else loop for checking what is running
+
+if grep -q "gdrive:" $config; then
+ rclone size gdrive: \
+ --verbose=1 \
+ --fast-list \
+ --retries 3 \
+ --no-update-modtime \
+ --user-agent="$useragent" \
+ --timeout=30m \
+ --exclude="**encrypt**" \
+ --config /opt/appdata/plexguide/rclone.conf | awk '{print $3,$4}' >>$log/gduncrypt.log
+ sed -i 's/Total size: / /g' $log/gduncrypt.log
+fi
+sleep 2
+if grep -q "tdrive:" $config; then
+ rclone size tdrive: \
+ --verbose=1 \
+ --fast-list \
+ --retries 3 \
+ --no-update-modtime \
+ --user-agent="$useragent" \
+ --timeout=30m \
+ --exclude="**encrypt**" \
+ --config /opt/appdata/plexguide/rclone.conf | awk '{print $3,$4}' >>$log/tduncrypt.log
+ sed -i 's/Total size: / /g' $log/tduncrypt.log
+fi
+sleep 2
+if grep -q "gcrypt:" $config; then
+ rclone size gcrypt: \
+ --verbose=1 \
+ --fast-list \
+ --retries 3 \
+ --user-agent="$useragent" \
+ --no-update-modtime \
+ --timeout=30m \
+ --config /opt/appdata/plexguide/rclone.conf | awk '{print $3,$4}' >>$log/gdcrypt.log
+ sed -i 's/Total size: / /g' $log/gdcrypt.log
+fi
+sleep 2
+if grep -q "tcrypt:" $config; then
+ rclone size tcrypt: \
+ --verbose=1 \
+ --fast-list \
+ --retries 3 \
+ --user-agent="$useragent" \
+ --no-update-modtime \
+ --timeout=30m \
+ --config /opt/appdata/plexguide/rclone.conf | awk '{print $3,$4}' >>$log/tdcrypt.log
+ sed -i 's/Total size: / /g' $log/tdcrypt.log
+fi
diff --git a/menu/pgui/gtused.yml b/menu/pgui/gtused.yml
new file mode 100644
index 00000000..419893ef
--- /dev/null
+++ b/menu/pgui/gtused.yml
@@ -0,0 +1,27 @@
+#!/bin/bash
+#
+# Title: PTS Community
+# Author: MrDoob
+# URL: WTFH >-!-< why you need this ^^
+# GNU: General Public License v3.0
+#
+################################################################################
+---
+- cron:
+ name: Daily G/TDrive used space check | file checker
+ special_time: 'daily'
+ job: 'bash /opt/appdata/pgui/gtused.sh'
+ state: absent
+ ignore_errors: yes
+
+- cron:
+ name: Daily check for mgerfs / rclone new version
+ special_time: 'daily'
+ job: 'bash /opt/appdata/pgui/ckeck.sh'
+ state: present
+
+- cron:
+ name: Daily G/TDrive used space checker | file & folder
+ special_time: 'daily'
+ job: 'bash /opt/appdata/pgui/gtused.sh'
+ state: present
\ No newline at end of file
diff --git a/menu/pgui/index.php b/menu/pgui/index.php
new file mode 100644
index 00000000..fe7854c5
--- /dev/null
+++ b/menu/pgui/index.php
@@ -0,0 +1,344 @@
+
+
+
+
+
+
+
+
+Com-UI
+
+
+
+
+
+
+
+
+
+
+
+
+ Community UI
+ Auto-Refreshes Every 10 Seconds
+
+
+