File nile-1.1.2.obscpio of Package HeroicGamesLauncher

07070100000000000041ED00000000000000000000000266E061F900000000000000000000000000000000000000000000001300000000nile-1.1.2/.github07070100000001000081A400000000000000000000000166E061F900000011000000000000000000000000000000000000001F00000000nile-1.1.2/.github/FUNDING.ymlko_fi: imlinguin
07070100000002000041ED00000000000000000000000266E061F900000000000000000000000000000000000000000000001D00000000nile-1.1.2/.github/workflows07070100000003000081A400000000000000000000000166E061F90000036F000000000000000000000000000000000000002800000000nile-1.1.2/.github/workflows/build.yamlname: Build binaries
on: [push]

jobs:
  build:
    strategy:
      fail-fast: false
      max-parallel: 3
      matrix:
        os: [ubuntu-20.04, macos-12, macos-14, windows-2022]
    runs-on: ${{ matrix.os }}

    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-python@v5
      with:
        python-version: '3.9'

    - name: Install pyinstaller and dependencies
      run: pip3 install --upgrade pyinstaller -r requirements.txt

    - name: Set strip on Linux and Mac
      id: strip
      run: echo "option=--strip" >> $GITHUB_OUTPUT
      if: runner.os != 'Windows'

    - name: Build
      run: pyinstaller
        --onefile
        --name nile 
        ${{ steps.strip.outputs.option }}
        nile/cli.py
      env:
        PYTHONOPTIMIZE: 1

    - uses: actions/upload-artifact@v4
      with:
        name: nile-${{ matrix.os }}
        path: dist/*
07070100000004000081A400000000000000000000000166E061F90000002D000000000000000000000000000000000000001600000000nile-1.1.2/.gitignore__pycache__
env
build
dist
*.egg-info
*.spec
07070100000005000041ED00000000000000000000000266E061F900000000000000000000000000000000000000000000001300000000nile-1.1.2/.vscode07070100000006000081A400000000000000000000000166E061F90000002D000000000000000000000000000000000000002100000000nile-1.1.2/.vscode/settings.json{
    "python.formatting.provider": "black"
}07070100000007000081A400000000000000000000000166E061F900008864000000000000000000000000000000000000001600000000nile-1.1.2/LICENSE.md### GNU GENERAL PUBLIC LICENSE

Version 3, 29 June 2007

Copyright (C) 2007 Free Software Foundation, Inc.
<https://fsf.org/>

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.

        <one line to give the program's name and a brief idea of what it does.>
        Copyright (C) <year>  <name of author>

        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 <https://www.gnu.org/licenses/>.

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:

        <program>  Copyright (C) <year>  <name of author>
        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 <https://www.gnu.org/licenses/>.

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 <https://www.gnu.org/licenses/why-not-lgpl.html>.
07070100000008000081A400000000000000000000000166E061F900000AB4000000000000000000000000000000000000001500000000nile-1.1.2/README.md<p align="center">
    <img width="180px" src="assets/icon.svg" alt="Logo" />
</p>

<p align="center">
    <a href="https://github.com/imLinguin/nile/stargazers">
        <img src="https://img.shields.io/github/stars/imLinguin/nile?color=d98e04" alt="stars"/>
    </a>
    <img src="https://img.shields.io/github/license/imLinguin/nile?color=d98e03" alt="loicense"/>
    <a href="https://ko-fi.com/imlinguin" target="_blank">
        <img src="https://img.shields.io/badge/Ko--Fi-Donate-d98e04?style=flat&logo=ko-fi" />
    </a>
</p>

# Nile
Linux native Amazon Games client, based on [this research](https://github.com/Lariaa/GameLauncherResearch/wiki/Amazon-Games)

Nile aims to be CLI and GUI tool for managing and playing games from Amazon. 

## Features
- Login to Amazon Account
- Download games
- Play games (with Wine/Proton on Linux)
- Play games using [Bottles](https://usebottles.com) (`--bottle` parameter)

## Might not work
- Online games, that use `FuelPump` (I don't have any game like that to test)

# Purpose
This is my attempt to make Amazon Games useful for Linux users, who want to play titles obtained thanks to [Prime](https://prime.amazon.com) membership.

# Dependencies
## Arch and derivatives (Manjaro, Garuda, EndeavourOS)
`sudo pacman -S python-pycryptodome python-zstandard python-requests python-protobuf python-json5`
## Debian and derivatives (Ubuntu, Pop!_OS)
`sudo apt install python3-pycryptodome python3-requests python3-zstandard python3-protobuf python3-json5`

## With pip
> Do this after cloning the repo and cd into the directory
> Do not install if you installed dependencies through your package manager  

This is **NOT** recommended as it can potentially collide with distribution packages [source](https://peps.python.org/pep-0668/)  
new versions of `pip` will prevent you from doing it outside of virtual environment

`pip3 install -r requirements.txt`

## Building PyInstaller executable

If you wish to test nile in Heroic flatpak you likely need to build the `nile` executable using pyinstaller

- Get pyinstaller

```
pip install pyinstaller
```

- Build the binary (assuming you are in the nile directory)

```
pyinstaller --onefile --name nile nile/cli.py
```

# Contributing

I'm always open for contributors

black is used for code formatting

## Setting up dev environment:
- Clone the repo `git clone https://github.com/imLinguin/nile`
- CD into the directory `cd nile`
- Setup virtual environment `python3 -m venv env`
- Install [dependencies](#dependencies)
- Run nile `./bin/nile`


# Prior work

This is based on Rodney's work here: https://github.com/derrod/twl.py
Some of his code is implemented in nile, since nothing changed since then in terms of downloading and patching
07070100000009000041ED00000000000000000000000266E061F900000000000000000000000000000000000000000000001200000000nile-1.1.2/assets0707010000000A000081A400000000000000000000000166E061F9000060E4000000000000000000000000000000000000001B00000000nile-1.1.2/assets/icon.svg<svg width="2160" height="2160" viewBox="0 0 2160 2160" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_301_217)">
<rect x="486" y="1005" width="817" height="100" fill="#734002"/>
<path d="M2827.47 1069.95H-3479.64V1772.37H2827.47V1069.95Z" fill="url(#paint0_linear_301_217)"/>
<path d="M-3556.12 1457.58H-591.572C-465.743 1447.22 -339.275 1447.22 -213.446 1457.58C-87.5113 1475.53 38.1033 1397.02 162.436 1457.58C286.769 1455.02 409.82 1485.78 530.948 1457.58H887.39C1933.64 1457.58 2816.9 1252.07 2816.9 1252.07L2750.99 2160H-3556.12V1457.58Z" fill="url(#paint1_linear_301_217)"/>
<g style="mix-blend-mode:color-dodge" opacity="0.34">
<path d="M2827.47 1069.95V1198.66H2079.87L1225.35 1123.89L691.705 1155.94L-131.198 1166.62L-323.893 1155.94L-1893.65 1262.75L-1733.42 1316.16L-3335.65 1433.66L-3479.64 1435.58L-1915.01 1305.48L-2064.55 1252.07L-774.759 1155.94H-2195.72L-2342.27 1123.89L-2919.07 1145.26L-3479.64 1101.14V1069.95H2827.47Z" fill="#734002"/>
</g>
<path d="M2528.51 1628.27V1378.43C2528.51 1378.43 901.178 1390.93 873.833 1339.66C849.693 1294.8 -1943.2 1304.94 -1983.15 1198.66C-2026.09 1084.9 -3216.33 1178.58 -3469.69 1102.53C-3602.68 1248.22 -2141.45 1168.86 -2124.79 1218C-2088.47 1323.96 -1404.85 1317.65 -1404.85 1317.65C-1404.85 1317.65 -1114.74 1322.67 -748.046 1364.55C-357.851 1409.09 46.2307 1455.02 51.0374 1455.02C60.1167 1455.02 -75 1529.5 -75 1529.5L51.0374 1737.5L294 1891.5L408.5 1936.5C537.333 1946 926.739 1934.97 1034.5 1923C1183 1906.5 1748 1800.5 1748 1800.5C1748 1800.5 1961.5 1775 1961.5 1812.5L2528.51 1628.27Z" fill="url(#paint2_linear_301_217)"/>
<g style="mix-blend-mode:lighten" opacity="0.2">
<path d="M2496.46 1621.86L2528.5 1317.87C2528.5 1317.87 901.176 1371.06 873.724 1315.2C855.245 1277.6 -323.352 1288.81 -1105.02 1273.43L792.866 1330.47L900.535 1362.52C900.535 1362.52 1357.81 1392.85 1349.16 1406.1C1340.51 1419.34 370.092 1356.43 285.494 1350.87C331.104 1354.83 624.204 1380.46 590.344 1406.1C563 1426.93 -306.475 1373.52 -748.048 1342.65C-357.746 1390.93 46.8691 1440.92 51.0349 1440.92C55.2007 1440.92 119.824 1455.13 194.06 1471.58C266.842 1487.84 405.057 1549.76 479.5 1584.5C638 1646.5 514 1646.5 514 1646.5L98 1652C228.833 1663.5 434.7 1710.4 211.5 1806L408 1965.5H447C460.244 1971.83 547.958 1982.8 792.866 1976C1099 1967.5 1571 1874 1641 1854.5C1697 1838.9 1814 1835 1865.5 1835H1875.5L2021.5 1734L2496.46 1621.86Z" fill="white"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.18">
<path d="M2528.51 1387.51V1628.27L2006.18 1606.91L2395.52 1602.64C2395.52 1602.64 793.295 1545.6 786.672 1526.58C780.05 1507.57 1971.14 1564.61 1971.14 1539.3C1971.14 1513.98 1097.08 1501.27 1090.67 1482.26C1084.26 1463.24 1743.09 1488.56 1743.09 1475.95C1743.09 1463.35 2078.81 1469.55 2078.81 1450.53C2078.81 1431.52 1614.38 1400.54 1574.22 1403.43C1534.06 1406.31 1302.16 1370.74 1302.16 1370.74L2528.51 1387.51Z" fill="#D98E04"/>
</g>
<g style="mix-blend-mode:multiply" opacity="0.18">
<path d="M2128.86 1610.77V1851.53L1606.54 1830.17L1995.88 1825.9C1995.88 1825.9 393.65 1768.86 387.028 1749.84C380.405 1730.83 1571.5 1787.87 1571.5 1762.55C1571.5 1737.24 697.432 1724.53 691.023 1705.51C684.615 1686.5 1343.45 1711.82 1343.45 1699.21C1343.45 1686.61 1679.17 1692.8 1679.17 1673.79C1679.17 1654.78 1214.74 1623.8 1174.58 1626.69C1134.41 1629.57 902.517 1594 902.517 1594L2128.86 1610.77Z" fill="#D98E04"/>
</g>
<g style="mix-blend-mode:lighten">
<g style="mix-blend-mode:lighten" opacity="0.18">
<path d="M907.059 1454.06C881.889 1458.33 856.178 1458.33 831.007 1454.06C856.178 1449.78 881.889 1449.78 907.059 1454.06Z" fill="white"/>
</g>
<g style="mix-blend-mode:lighten" opacity="0.18">
<path d="M837.303 1479.37C837.303 1481.19 820.319 1482.58 799.384 1482.58C778.448 1482.58 761.357 1481.19 761.357 1479.37C786.504 1475.24 812.156 1475.24 837.303 1479.37Z" fill="white"/>
</g>
<g style="mix-blend-mode:lighten" opacity="0.18">
<path d="M729.631 1438.25C750.632 1438.25 767.657 1436.81 767.657 1435.04C767.657 1433.27 750.632 1431.84 729.631 1431.84C708.629 1431.84 691.604 1433.27 691.604 1435.04C691.604 1436.81 708.629 1438.25 729.631 1438.25Z" fill="white"/>
</g>
<g style="mix-blend-mode:lighten" opacity="0.18">
<path d="M808.79 1425.54C842.003 1425.54 868.927 1424.1 868.927 1422.33C868.927 1420.56 842.003 1419.13 808.79 1419.13C775.578 1419.13 748.653 1420.56 748.653 1422.33C748.653 1424.1 775.578 1425.54 808.79 1425.54Z" fill="white"/>
</g>
<g style="mix-blend-mode:lighten" opacity="0.18">
<path d="M929.167 1450.85C962.38 1450.85 989.304 1449.42 989.304 1447.65C989.304 1445.88 962.38 1444.44 929.167 1444.44C895.955 1444.44 869.03 1445.88 869.03 1447.65C869.03 1449.42 895.955 1450.85 929.167 1450.85Z" fill="white"/>
</g>
<g style="mix-blend-mode:lighten" opacity="0.18">
<path d="M1084.37 1482.58C1084.37 1486.1 1041.64 1488.88 989.41 1488.88C937.178 1488.88 894.345 1486.1 894.345 1482.58C894.345 1479.05 937.071 1476.27 989.41 1476.27C1041.75 1476.27 1084.37 1479.05 1084.37 1482.58Z" fill="white"/>
</g>
<g style="mix-blend-mode:lighten" opacity="0.18">
<path d="M799.385 1463.56C799.385 1467.09 753.989 1469.87 698.018 1469.87C642.047 1469.87 596.65 1467.09 596.65 1463.56C596.65 1460.04 642.047 1457.26 698.018 1457.26C753.989 1457.26 799.385 1460.04 799.385 1463.56Z" fill="white"/>
</g>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1798.31 1057.99L1499.55 183.279H1482.14L775.026 1055.32L1798.31 1057.99Z" fill="url(#paint3_linear_301_217)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1499.55 183.279H1511.52L2147.49 1055.53L1756.55 1057.88L1638.2 633.291L1630.08 618.871L1517.5 286.035L1523.48 264.672L1499.55 183.279Z" fill="url(#paint4_linear_301_217)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1630.08 618.872L1592.37 633.292H1638.2H1693.85L1630.08 618.872Z" fill="#A65D03"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1499.55 287.211L1517.5 286.036L1558.84 286.677L1523.48 264.673L1499.55 287.211Z" fill="#A65D03"/>
<g style="mix-blend-mode:soft-light" opacity="0.3">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1576.78 458.649L1419.01 467.088L1439.1 514.407H1592.38L1576.78 458.649Z" fill="#D98E04"/>
</g>
<g style="mix-blend-mode:multiply">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1657.86 704.11L1888.37 700.158L1979.8 825.559H1847.56L1852.05 841.047H1991.12L2023.38 885.375L1711.48 896.163L1657.86 704.11Z" fill="#A65D03"/>
</g>
<g style="mix-blend-mode:soft-light" opacity="0.3">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1776.52 702.08L1743.95 633.291L1834.31 626.135L1888.36 700.157L1776.52 702.08Z" fill="#D98E04"/>
</g>
<g style="mix-blend-mode:multiply">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1456.4 379.712L1546.02 370.312L1657.86 384.092L1718.21 466.766L1571.87 447.006L1449.14 448.394L1456.4 379.712Z" fill="#A65D03"/>
</g>
<g style="mix-blend-mode:multiply">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2061.08 936.966L1900.64 936.219L1924.36 1016.86L2114.7 1010.46L2061.08 936.966Z" fill="#A65D03"/>
</g>
<g style="mix-blend-mode:soft-light" opacity="0.3">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1900.64 936.219H1722.69L1740.95 1016.86H1924.35L1900.64 936.219Z" fill="#D98E04"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1481.93 1074.86L1170.99 93.8751H1151.76L668.421 723.123L657.206 724.939L373.291 1053.29L1481.93 1074.86Z" fill="url(#paint5_linear_301_217)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1170.98 93.8751H1184.12L1977.22 1068.46L1481.92 1074.86L1437.38 942.094L1425.1 932.587L1410.78 879.607L1408.75 855.68L1348.19 669.074L1330.67 660.956L1170.98 93.8751Z" fill="url(#paint6_linear_301_217)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M988.116 312.312H1013.22L1016.1 356.96L1112.02 399.259L1144.92 396.802L1210.93 367.749L1250.24 356.96L1356.09 360.485L1383.44 380.46L1418.58 399.259L1431.61 392.423L1186.68 88H1152.61L979.998 301.737L988.116 312.312Z" fill="url(#paint7_linear_301_217)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1431.61 392.423L1186.68 88H1170.56L1250.67 347.667L1220.23 350.979L1151.86 381.207L1112.56 384.305L1017.06 349.697L1012.79 305.048L979.998 301.737L988.116 312.312H1013.22L1016.1 356.96L1112.02 399.259L1144.92 396.802L1210.93 367.749L1250.24 356.96L1356.09 360.485L1383.44 380.46L1418.58 399.259L1431.61 392.423Z" fill="url(#paint8_linear_301_217)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1431.61 392.423L1405.44 382.276L1359.51 350.445L1302.15 345.424L1250.67 347.668L1220.23 350.979L1151.86 381.207L1112.56 384.305L1017.06 349.697L1012.79 305.048L979.998 301.737L988.116 312.312L1011.51 309.962L1016.1 356.96L1112.02 399.259L1144.92 396.802L1233.47 361.019L1251.31 360.485L1344.24 359.951L1356.09 360.485L1383.44 380.46L1418.58 399.259L1431.61 392.423Z" fill="url(#paint9_linear_301_217)"/>
<g style="mix-blend-mode:soft-light" opacity="0.3">
<path fill-rule="evenodd" clip-rule="evenodd" d="M687.332 699.944L1136.06 692.146L1149.52 584.37H1030.85L1035.87 566.211H1064.92L1080.41 519.96L846.594 491.12L687.332 699.944Z" fill="#D98E04"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1408.76 855.679L1159.66 861.661L1409.72 867.643H1541.1L1408.76 855.679Z" fill="#A65D03"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1387.71 931.839H1466.97L1437.38 942.094L1387.71 931.839Z" fill="#F2BB16"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1294.14 659.46L1350.43 676.337L1389.74 659.46H1294.14Z" fill="#F2BB16"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M657.206 724.938H701.855L672.267 718.422L657.206 724.938Z" fill="#D98E04"/>
<g style="mix-blend-mode:soft-light" opacity="0.3">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1408.76 855.68L1364 718.422L910.145 741.281L857.913 855.68H1408.76Z" fill="#D98E04"/>
</g>
<g style="mix-blend-mode:multiply">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1699.72 1072.09L1626.98 999.24L1453.09 989.199H1070.15L1062.14 1066.21L1481.93 1074.86L1699.72 1072.09Z" fill="#A65D03"/>
</g>
<g style="mix-blend-mode:multiply">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1644.07 989.198L1594.08 894.88H1743.09L1727.17 874.692H1646.85L1611.81 810.924L1748.22 803.019L1907.9 989.198H1644.07Z" fill="#A65D03"/>
</g>
<g style="mix-blend-mode:soft-light" opacity="0.3">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1368.28 731.668L1630.51 741.281L1683.27 806.759H1392.74L1368.28 731.668Z" fill="#D98E04"/>
</g>
<g style="mix-blend-mode:multiply">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1313.26 601.247L1595.15 618.872L1621.21 665.443L1330.67 660.957L1313.26 601.247Z" fill="#A65D03"/>
</g>
<g style="mix-blend-mode:multiply">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1430.01 413.679L1336.22 400.541L1358.44 462.387L1448.91 467.087L1455.11 477.448L1272.03 458.648L1291.04 524.553L1525.39 533.098L1430.01 413.679Z" fill="#A65D03"/>
</g>
<g style="mix-blend-mode:soft-light" opacity="0.3">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1427.77 529.573L1479.14 609.577L1595.15 618.87L1525.4 533.098L1427.77 529.573Z" fill="#D98E04"/>
</g>
<g style="mix-blend-mode:multiply">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1007.45 514.513L1047.19 438.46L1264.34 430.876L1291.05 524.553L1007.45 514.513Z" fill="#A65D03"/>
</g>
<g style="mix-blend-mode:soft-light" opacity="0.3">
<path fill-rule="evenodd" clip-rule="evenodd" d="M731.447 867.643H1063.75L1054.14 942.093L694.488 931.839L731.447 867.643Z" fill="#D98E04"/>
</g>
<g style="mix-blend-mode:multiply">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1153.04 233.055L1142.36 310.816L1233.47 292.764L1373.4 321.605L1310.27 243.629L1212.65 225.685L1153.04 233.055Z" fill="#A65D03"/>
</g>
<g style="mix-blend-mode:multiply">
<path fill-rule="evenodd" clip-rule="evenodd" d="M963.548 738.611L986.406 694.71L1209.86 692.147L1203.13 726.541L963.548 738.611Z" fill="#A65D03"/>
</g>
<g style="mix-blend-mode:soft-light" opacity="0.3">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1143.11 989.199L1156.04 958.757H1346.49L1348.84 986.315L1143.11 989.199Z" fill="#D98E04"/>
</g>
<g style="mix-blend-mode:multiply">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1020.27 252.815L1212.65 225.684L1188.51 152.836L1093.12 162.877L1020.27 252.815Z" fill="#A65D03"/>
</g>
<g style="mix-blend-mode:multiply">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1336.76 719.812L1329.71 692.147H1355.56L1466.33 702.081L1468.25 719.812L1364 718.423L1336.76 719.812Z" fill="#D98E04"/>
</g>
<g style="mix-blend-mode:multiply">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1236.03 601.247H1313.26L1424.45 608.19L1407.15 583.622L1303.86 568.775L1233.79 574.222L1236.03 601.247Z" fill="#D98E04"/>
</g>
<g style="mix-blend-mode:soft-light">
<path fill-rule="evenodd" clip-rule="evenodd" d="M850.538 697.06L884.612 704.11L913.772 695.992L850.538 697.06Z" fill="#F2BB16"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M715.631 865.721L729.303 871.382L760.279 867.644L715.631 865.721Z" fill="#D98E04"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M975.084 939.85L1046.86 942.628H997.195L975.084 939.85Z" fill="#D98E04"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1032.34 936.538H1058.94L1046.87 942.627L1032.34 936.538Z" fill="#D98E04"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1258.25 523.378L1291.04 524.553L1276.94 529.574L1258.25 523.378Z" fill="#D98E04"/>
<g style="mix-blend-mode:soft-light">
<path fill-rule="evenodd" clip-rule="evenodd" d="M957.248 418.592H987.263L972.629 425.108L957.248 418.592Z" fill="#F2BB16"/>
</g>
<g style="mix-blend-mode:soft-light">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1076.78 519.747L1102.95 521.243L1087.99 524.554L1076.78 519.747Z" fill="#F2BB16"/>
</g>
<g style="mix-blend-mode:soft-light">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1006.81 418.593H1061.18L1040.45 413.039L1006.81 418.593Z" fill="#F2BB16"/>
</g>
<g style="mix-blend-mode:soft-light">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1100.59 561.617H1155.71L1134.67 556.17L1100.59 561.617Z" fill="#F2BB16"/>
</g>
<g style="mix-blend-mode:soft-light">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1211.36 988.771L1290.08 989.305L1251.31 995.5L1211.36 988.771Z" fill="#F2BB16"/>
</g>
<g style="mix-blend-mode:multiply">
<path fill-rule="evenodd" clip-rule="evenodd" d="M846.584 491.12L871.578 458.648L948.912 467.086L940.047 491.227L846.584 491.12Z" fill="#D98E04"/>
</g>
<g style="mix-blend-mode:multiply">
<path fill-rule="evenodd" clip-rule="evenodd" d="M875.216 1063.12L373.291 1053.29L655.497 726.434L669.81 719.918L875.216 1063.12Z" fill="#A65D03"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M508.738 1067.49L301.089 524.445L288.378 513.337L-36.3399 902.25L-54.8187 909.407L-199.34 1067.49H508.738Z" fill="url(#paint10_linear_301_217)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M508.739 1067.49L750.034 1061.94L310.918 524.445L288.38 513.337L301.091 524.445L428.628 860.165L426.705 873.517L470.606 967.835L475.84 998.918L481.501 1001.05L508.739 1067.49Z" fill="url(#paint11_linear_301_217)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M-54.8154 909.407L122.497 910.475L-38.6863 902.251L-54.8154 909.407Z" fill="#F2BB16"/>
<g style="mix-blend-mode:multiply">
<path fill-rule="evenodd" clip-rule="evenodd" d="M147.808 681.784L279.831 673.666L283.997 646.428L237.853 650.167L242.446 637.563L340.609 628.697L402.134 636.174L365.07 590.778L301.088 524.445L288.377 513.337L215.849 600.391L272.674 596.332L268.081 604.45L209.013 608.402L147.808 681.784Z" fill="#A65D03"/>
</g>
<g style="mix-blend-mode:soft-light" opacity="0.3">
<path fill-rule="evenodd" clip-rule="evenodd" d="M282.288 860.166L299.806 790.416H402.775L428.625 860.166H282.288Z" fill="#D98E04"/>
</g>
<g style="mix-blend-mode:soft-light" opacity="0.3">
<path fill-rule="evenodd" clip-rule="evenodd" d="M269.155 734.657H103.592L130.296 702.613H368.707L380.35 731.025L269.155 734.657Z" fill="#D98E04"/>
</g>
<g style="mix-blend-mode:soft-light" opacity="0.3">
<path fill-rule="evenodd" clip-rule="evenodd" d="M-142.193 1005.01H208.374L190.215 1038.55H33.0899L7.77473 1067.49H-199.34L-142.193 1005.01Z" fill="#D98E04"/>
</g>
<g style="mix-blend-mode:multiply">
<path fill-rule="evenodd" clip-rule="evenodd" d="M174.835 1067.49L208.375 1005.01H483.103L508.739 1067.49H174.835Z" fill="#A65D03"/>
</g>
<g style="mix-blend-mode:multiply">
<path fill-rule="evenodd" clip-rule="evenodd" d="M560.227 1066.32L550.4 1036.3L726.218 1032.89L750.038 1061.94L560.227 1066.32Z" fill="#A65D03"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M428.627 860.165H362.722L426.704 873.517L428.627 860.165Z" fill="#D98E04"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M443.365 995.821L483.1 1005.01L492.5 995.821H443.365Z" fill="#F2BB16"/>
<g style="mix-blend-mode:soft-light" opacity="0.3">
<path fill-rule="evenodd" clip-rule="evenodd" d="M470.609 967.835H673.13L626.239 910.475L426.708 873.517L470.609 967.835Z" fill="#D98E04"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M426.704 873.517L362.722 860.165H428.627L460.244 873.517H426.704Z" fill="#A65D03"/>
<g style="mix-blend-mode:soft-light" opacity="0.3">
<path fill-rule="evenodd" clip-rule="evenodd" d="M-38.6904 902.249L69.1925 910.154L154.645 773.11H185.087L198.011 743.736H96.0032L-38.6904 902.249Z" fill="#D98E04"/>
</g>
<g style="mix-blend-mode:multiply">
<path fill-rule="evenodd" clip-rule="evenodd" d="M601.134 1035.24L571.439 967.835H673.127L726.214 1032.89L601.134 1035.24Z" fill="#D98E04"/>
</g>
<g style="mix-blend-mode:multiply">
<path fill-rule="evenodd" clip-rule="evenodd" d="M380.349 731.026L482.678 734.657L541.32 806.437L402.78 790.415H341.789V734.657L380.349 731.026Z" fill="#A65D03"/>
</g>
<g style="mix-blend-mode:multiply">
<path fill-rule="evenodd" clip-rule="evenodd" d="M117.477 988.877L122.497 964.203L288.381 967.835V988.877H117.477Z" fill="#D98E04"/>
</g>
<g style="mix-blend-mode:multiply">
<path fill-rule="evenodd" clip-rule="evenodd" d="M467.931 797.999L477.33 815.517L550.392 817.653L541.313 806.438L467.931 797.999Z" fill="#D98E04"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M297.883 967.835H334.093L316.042 964.203L297.883 967.835Z" fill="#F2BB16"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M355.461 947.54L375.542 934.081L395.624 939.315L355.461 947.54Z" fill="#F2BB16"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M447.323 906.95L486.097 910.155L437.816 910.475L447.323 906.95Z" fill="#F2BB16"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.8379 842.755H79.6652L21.4515 831.432L11.8379 842.755Z" fill="#F2BB16"/>
<g style="mix-blend-mode:soft-light">
<path fill-rule="evenodd" clip-rule="evenodd" d="M164.259 789.133H191.817L198.012 790.414L218.413 797.998L174.833 793.512L164.259 789.133Z" fill="#F2BB16"/>
</g>
<g style="mix-blend-mode:soft-light">
<path fill-rule="evenodd" clip-rule="evenodd" d="M218.417 774.606H253.453L232.624 775.994L218.417 774.606Z" fill="#F2BB16"/>
</g>
<g style="mix-blend-mode:soft-light">
<path fill-rule="evenodd" clip-rule="evenodd" d="M274.923 693.427H297.034L288.382 696.311L274.923 693.427Z" fill="#F2BB16"/>
</g>
<g style="mix-blend-mode:soft-light">
<path fill-rule="evenodd" clip-rule="evenodd" d="M263.699 649.74H294.782L282.926 653.158L263.699 649.74Z" fill="#F2BB16"/>
</g>
<g style="mix-blend-mode:soft-light">
<path fill-rule="evenodd" clip-rule="evenodd" d="M284.957 680.823L304.718 679.328L313.69 681.571L284.957 680.823Z" fill="#F2BB16"/>
</g>
<g style="mix-blend-mode:soft-light">
<path fill-rule="evenodd" clip-rule="evenodd" d="M79.6631 884.412L117.476 887.617L98.5696 891.996L79.6631 884.412Z" fill="#F2BB16"/>
</g>
<g style="mix-blend-mode:multiply">
<path fill-rule="evenodd" clip-rule="evenodd" d="M309.096 649.739V662.984H353.637L348.083 648.137L309.096 649.739Z" fill="#D98E04"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M-1303.92 1074.86L-1210.13 1036.41L-1149.14 976.594L-877.617 1047.95L-696.139 1029.68L-630.447 1057.88L-605.773 1047.95L-514.766 976.594L-376.227 954.804L-378.257 976.594H-333.288L-262.256 1057.99L140.437 1052.33L169.491 1057.99L907.904 1074.86L1392.1 1066.21L1640.76 999.239L1683.81 995.501V1032.03L1817.75 954.804L1956.83 942.627L1977.34 958.756L1969.54 985.78L1978.19 997.423L1956.83 1029.68L2131.04 1008.75H2190.97L2325.55 1098.68L-1303.92 1074.86Z" fill="url(#paint12_linear_301_217)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1683.81 995.5L1640.77 999.238L1392.1 1066.21L1448.71 1071.45L1635.43 1057.99L1683.81 995.5Z" fill="#A65D03"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1683.81 1032.03L1660.63 1052.33L1871.59 1022.31L1883.55 1016.86L1966.98 958.756L1956.83 942.627L1817.76 954.804L1683.81 1032.03Z" fill="#A65D03"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1956.83 1029.68L1930.23 1055.85L1883.55 1066.21H2046.45L2162.66 1035.77L2190.97 1008.74H2131.04L1956.83 1029.68Z" fill="#A65D03"/>
<g style="mix-blend-mode:multiply">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1803.34 1080.2L1813.8 1052.33L1871.59 1037.48V1022.31L1883.55 1016.86L1966.98 958.756H1977.34L1969.54 985.78L1978.19 997.423L1956.83 1029.68L1930.23 1055.85L1883.55 1066.21V1079.56L1803.34 1080.2Z" fill="#A65D03"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1969.54 985.78L1917.09 1021.78L1871.59 1037.48L1813.8 1052.33L1927.88 1036.41L1978.19 997.423L1969.54 985.78Z" fill="#734002"/>
<g style="mix-blend-mode:multiply">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1635.42 1057.99L1640.76 1079.24H1653.58L1660.63 1052.33L1683.8 1032.03V995.5L1635.42 1057.99Z" fill="#A65D03"/>
</g>
<path d="M686.5 1068.5L720.5 1065.5L894 1066.5L1143.5 1069L1166 1069.5H1185L1187.5 1071L1122 1078.5L841 1080.5L728 1079.5L684.5 1076L686.5 1068.5Z" fill="#734002"/>
</g>
<defs>
<linearGradient id="paint0_linear_301_217" x1="1913.5" y1="2199.5" x2="-1020.5" y2="1242.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#C17979"/>
<stop offset="0.0001" stop-color="#1D1D1D"/>
<stop offset="1" stop-color="#A65D03"/>
</linearGradient>
<linearGradient id="paint1_linear_301_217" x1="416" y1="2059" x2="1999" y2="1932" gradientUnits="userSpaceOnUse">
<stop stop-color="#734002"/>
<stop offset="1" stop-color="#0D0A00"/>
</linearGradient>
<linearGradient id="paint2_linear_301_217" x1="-60" y1="1438" x2="1538" y2="1591" gradientUnits="userSpaceOnUse">
<stop stop-color="#FEDC03"/>
<stop offset="1" stop-color="#D98E04"/>
</linearGradient>
<linearGradient id="paint3_linear_301_217" x1="1286.67" y1="1057.99" x2="1286.67" y2="183.386" gradientUnits="userSpaceOnUse">
<stop stop-color="#1D1D1D"/>
<stop offset="1" stop-color="#A65D03"/>
</linearGradient>
<linearGradient id="paint4_linear_301_217" x1="1823.63" y1="1057.88" x2="1823.63" y2="183.386" gradientUnits="userSpaceOnUse">
<stop stop-color="#734002"/>
<stop offset="0.99" stop-color="#452716"/>
</linearGradient>
<linearGradient id="paint5_linear_301_217" x1="927.661" y1="1074.97" x2="927.661" y2="93.9818" gradientUnits="userSpaceOnUse">
<stop offset="0.06" stop-color="#A65D03"/>
<stop offset="1" stop-color="#D98E04"/>
</linearGradient>
<linearGradient id="paint6_linear_301_217" x1="-80065.7" y1="199647" x2="-80065.7" y2="109553" gradientUnits="userSpaceOnUse">
<stop stop-color="#D98E04"/>
<stop offset="0.99" stop-color="#452716"/>
</linearGradient>
<linearGradient id="paint7_linear_301_217" x1="979.998" y1="243.63" x2="1431.61" y2="243.63" gradientUnits="userSpaceOnUse">
<stop stop-color="#F2BB16"/>
<stop offset="1" stop-color="#A65D03"/>
</linearGradient>
<linearGradient id="paint8_linear_301_217" x1="307973" y1="4623.05" x2="327068" y2="4623.05" gradientUnits="userSpaceOnUse">
<stop stop-color="#452716"/>
<stop offset="0.99" stop-color="#1C0035"/>
<stop offset="0.9901" stop-color="#010D00"/>
</linearGradient>
<linearGradient id="paint9_linear_301_217" x1="979.998" y1="350.552" x2="1431.61" y2="350.552" gradientUnits="userSpaceOnUse">
<stop offset="0.38" stop-color="#A65D03"/>
<stop offset="0.99" stop-color="#010D00"/>
</linearGradient>
<linearGradient id="paint10_linear_301_217" x1="154.752" y1="1067.49" x2="154.752" y2="513.337" gradientUnits="userSpaceOnUse">
<stop offset="0.06" stop-color="#A65D03"/>
<stop offset="1" stop-color="#D98E04"/>
</linearGradient>
<linearGradient id="paint11_linear_301_217" x1="-634.929" y1="112852" x2="-634.929" y2="84102.3" gradientUnits="userSpaceOnUse">
<stop stop-color="#3A1645"/>
<stop offset="0.99" stop-color="#452716"/>
</linearGradient>
<linearGradient id="paint12_linear_301_217" x1="2.30646e+06" y1="15708.7" x2="2.30646e+06" y2="13428.7" gradientUnits="userSpaceOnUse">
<stop stop-color="#3A1645"/>
<stop offset="0.99" stop-color="#734002"/>
</linearGradient>
<clipPath id="clip0_301_217">
<path d="M0 0H2160V1080C2160 1676.47 1676.47 2160 1080 2160V2160C483.532 2160 0 1676.47 0 1080V0Z" fill="white"/>
</clipPath>
</defs>
</svg>
0707010000000B000041ED00000000000000000000000266E061F900000000000000000000000000000000000000000000000F00000000nile-1.1.2/bin0707010000000C000081ED00000000000000000000000166E061F900000070000000000000000000000000000000000000001400000000nile-1.1.2/bin/nile#!/bin/bash

cd "$(dirname $(readlink -f "$0"))/.."
/usr/bin/env python3 -m nile.cli "$@"
STATUS=$?
exit $STATUS0707010000000D000041ED00000000000000000000000266E061F900000000000000000000000000000000000000000000001000000000nile-1.1.2/nile0707010000000E000081A400000000000000000000000166E061F900000158000000000000000000000000000000000000001C00000000nile-1.1.2/nile/__init__.py#
# mmm   mm   mmmmmm   mm        mmmmmmmm 
# ###   ##   ""##""   ##        ##"""""" 
# ##"#  ##     ##     ##        ##       
# ## ## ##     ##     ##        #######  
# ##  #m##     ##     ##        ##       
# ##   ###   mm##mm   ##mmmmmm  ##mmmmmm 
# ""   """   """"""   """"""""  """""""" 

version = "1.1.2"
codename = "Will A. Zeppeli"
0707010000000F000041ED00000000000000000000000266E061F900000000000000000000000000000000000000000000001400000000nile-1.1.2/nile/api07070100000010000081A400000000000000000000000166E061F900002339000000000000000000000000000000000000002500000000nile-1.1.2/nile/api/authorization.pyimport webbrowser
import requests
from urllib.parse import urlencode, urlparse, parse_qs
import nile.constants as constants
import logging
import hashlib
import time
import secrets
import base64
import uuid
import json


class AuthenticationManager:
    def __init__(self, session, config_manager, library_manager):
        self.logger = logging.getLogger("AUTH_MANAGER")
        self.challenge = ""
        self.verifier = bytes()
        self.device_id = ""
        self.serial = None
        self.session_manager = session
        self.config = config_manager
        self.library_manager = library_manager

    def generate_code_verifier(self) -> bytes:
        self.logger.debug("Generating code_verifier")
        code_verifier = secrets.token_bytes(32)

        code_verifier = base64.urlsafe_b64encode(code_verifier).rstrip(b"=")
        self.verifier = code_verifier
        return code_verifier

    def generate_challange(self, code_verifier: bytes) -> bytes:
        self.logger.debug("Generating challange")
        hash = hashlib.sha256(code_verifier)
        return base64.urlsafe_b64encode(hash.digest()).rstrip(b"=")

    def generate_device_serial(self) -> str:
        self.logger.debug("Generating serial")
        serial = uuid.uuid1().hex.upper()
        self.serial = serial
        return serial

    def generate_client_id(self, serial) -> str:
        self.logger.debug("Generating client_id")
        serialEx = f"{serial}#A2UMVHOX7UP4V7"
        clientId = serialEx.encode("ascii")
        clientIdHex = clientId.hex()
        self.client_id = clientIdHex
        return clientIdHex

    def get_auth_url(self, client_id: str, challenge: bytes):
        base_url = "https://amazon.com/ap/signin?"

        arguments = {
            "openid.ns": "http://specs.openid.net/auth/2.0",
            "openid.claimed_id": "http://specs.openid.net/auth/2.0/identifier_select",
            "openid.identity": "http://specs.openid.net/auth/2.0/identifier_select",
            "openid.mode": "checkid_setup",
            "openid.oa2.scope": "device_auth_access",
            "openid.ns.oa2": "http://www.amazon.com/ap/ext/oauth/2",
            "openid.oa2.response_type": "code",
            "openid.oa2.code_challenge_method": "S256",
            "openid.oa2.client_id": f"device:{client_id}",
            "language": "en_US",
            "marketPlaceId": constants.MARKETPLACEID,
            "openid.return_to": "https://www.amazon.com",
            "openid.pape.max_auth_age": 0,
            "openid.assoc_handle": "amzn_sonic_games_launcher",
            "pageId": "amzn_sonic_games_launcher",
            "openid.oa2.code_challenge": challenge,
        }
        return base_url + urlencode(arguments)

    def register_device(self, code, client_id, verifier_str, serial):
        self.logger.info(f"Registerring a device. ID: {client_id}")
        data = {
            "auth_data": {
                "authorization_code": code,
                "client_domain": "DeviceLegacy",
                "client_id": client_id,
                "code_algorithm": "SHA-256",
                "code_verifier": verifier_str,
                "use_global_authentication": False,
            },
            "registration_data": {
                "app_name": "AGSLauncher for Windows",
                "app_version": "1.0.0",
                "device_model": "Windows",
                "device_name": None,
                "device_serial": serial,
                "device_type": "A2UMVHOX7UP4V7",
                "domain": "Device",
                "os_version": "10.0.19044.0",
            },
            "requested_extensions": ["customer_info", "device_info"],
            "requested_token_type": ["bearer", "mac_dms"],
            "user_context_map": {},
        }
        response = self.session_manager.session.post(
            f"{constants.AMAZON_API}/auth/register", json=data
        )
        if not response.ok:
            self.logger.error("Failed to register a device")
            print(response.content)
            return

        res_json = response.json()
        self.logger.info("Succesfully registered a device")
        if self.logger.level > logging.DEBUG:
            print(res_json)
        config_data = res_json["response"]["success"]
        config_data["NILE"] = {"token_obtain_time": time.time()}

        self.config.write("user", config_data)

        self.logger.info("Written data to the config")

    def refresh_token(self):
        url = f"{constants.AMAZON_API}/auth/token"
        self.logger.info("Refreshing token")
        user_conf_content = self.config.get("user")
        refresh_token = user_conf_content["tokens"]["bearer"]["refresh_token"]
        request_data = {
            "source_token": refresh_token,
            "source_token_type": "refresh_token",
            "requested_token_type": "access_token",
            "app_name": "AGSLauncher for Windows",
            "app_version": "1.0.0",
        }
        try:

            response = self.session_manager.session.post(url, json=request_data)
        except requests.exceptions.ConnectionError as e:
            self.logger.error(f"Failed to refresh the token {e}")
            pass
            return None

        if not response.ok:
            self.logger.error(f"Failed to refresh the token {response}")
            return None

        res_json = response.json()

        user_conf_content = self.config.get("user")
        user_conf_content["tokens"]["bearer"]["access_token"] = res_json["access_token"]
        user_conf_content["tokens"]["bearer"]["expires_in"] = res_json["expires_in"]
        user_conf_content["NILE"]["token_obtain_time"] = time.time()
        self.config.write("user", user_conf_content)

    def is_token_expired(self):
        token_obtain_time, expires_in = self.config.get(
            "user", ["NILE//token_obtain_time", "tokens//bearer//expires_in"]
        )
        if not token_obtain_time or not expires_in:
            return False
        return time.time() > token_obtain_time + int(expires_in)

    def get_authorization_header(self) -> str:
        return f'bearer {self.config.get("user", "tokens//bearer//access_token")}'

    def is_logged_in(self) -> bool:
        self.logger.debug("Checking stored credentials")
        tokens = self.config.get("user", "tokens")
        return bool(tokens)

    def handle_redirect(self, redirect):
        """Handles login through a redirect URL"""
        if redirect.find("openid.oa2.authorization_code") > 0:
            self.logger.info("Got authorization code")

            # Parse auth code
            parsed = urlparse(redirect)
            query = parse_qs(parsed.query)
            code = query["openid.oa2.authorization_code"][0]
            self.register_device(code,
                                 self.client_id,
                                 self.verifier.decode("utf-8"),
                                 self.serial)
            self.library_manager.sync()

    def handle_login(self, code, client_id, code_verifier, serial):
        """Handles login through individual args"""
        self.register_device(code,
                             client_id,
                             code_verifier,
                             serial)
        self.library_manager.sync()

    def login(self, non_interactive=False):
        code_verifier = self.generate_code_verifier()
        challenge = self.generate_challange(code_verifier)

        serial = self.generate_device_serial()
        client_id = self.generate_client_id(serial)

        url = self.get_auth_url(client_id, challenge)
        if non_interactive:
            data = {
                'client_id': client_id,
                'code_verifier': code_verifier.decode("utf-8"),
                'serial': serial,
                'url': url
            }
            print(json.dumps(data))
            return
            
        print("Login URL: ", url)
        print(
            "The login URL will be opened in your browser, after you login paste the amazon.com url you were redirected to here")
        input("Press ENTER to proceed")
        try:
            webbrowser.open(url)
        except:
            pass
        redirect = input("Paste amazon.com url you got redirected to: ")
        self.handle_redirect(redirect)

    def logout(self):
        if not self.is_logged_in():
            self.logger.error("You are not logged in")
            return
        token = self.config.get("user", "tokens//bearer//access_token")
        response = self.session_manager.session.post(
            f"{constants.AMAZON_API}/auth/deregister",
            headers={"Authorization": f"bearer {token}"},
            json={
                "request_metadata": {
                    "app_name": "AGSLauncher for Windows",
                    "app_version": "1.0.0",
                }
            },
        )

        if response.ok:
            self.logger.info("Successfully deregistered a device")
            self.config.write("user", {})
07070100000011000081A400000000000000000000000166E061F900000A4E000000000000000000000000000000000000001F00000000nile-1.1.2/nile/api/graphql.pyimport requests
import logging
import locale
import os
import nile.constants as constants


class GraphQLHandler:
    def __init__(self, config_manager):
        self.config = config_manager
        self.session = requests.Session()
        # This will cause problems on certain systems for sure
        lang, encoding = locale.getdefaultlocale()
        lang = lang.replace("_", "-")

        self.session.headers.update(
            {
                "User-Agent": "GraphQL.Client/3.2.2.0",
                "client-id": "AmazonGamesApp",
                "prime-gaming-language": lang,
                "Content-Type": "application/json; charset=utf-8",
            }
        )

    def make_graphql_request(self, data):
        token = self.config.get("user", "tokens//bearer//access_token")
        response = self.session.post(
            constants.AMAZON_GAMING_GRAPHQL,
            data=data,
            headers={"x-amz-access-token": token},
        )
        if not response.ok:
            return None

        return response.json()

    def get_offers(self):
        return self.make_graphql_request(
            '{"query":"query{primeOffers(dateOverride:null,group:null){catalogId,title,startTime,endTime,deliveryMethod,tags{type,tag},productList{sku,vendor},content{publisher},assets{location2x},offerAssets{screenshots{location}},self{eligibility{canClaim,isClaimed,isPrimeGaming,maxOrdersExceeded,inRestrictedMarketplace,missingRequiredAccountLink,conflictingClaimAccount{obfuscatedEmail}}},game{id,gameplayVideoLinks,keywords,releaseDate,esrb,pegi,usk,website,genres,gameModes,externalWebsites{socialMediaType,link},developerName,logoImage{defaultMedia{src1x,src2x,type},tablet{src1x,src2x,type},desktop{src1x,src2x,type},description,alt,videoPlaceholderImage{src1x,src2x,type}},pgCrownImage{defaultMedia{src1x,src2x,type},tablet{src1x,src2x,type},desktop{src1x,src2x,type},description,alt,videoPlaceholderImage{src1x,src2x,type}},trailerImage{defaultMedia{src1x,src2x,type},tablet{src1x,src2x,type},desktop{src1x,src2x,type},description,alt,videoPlaceholderImage{src1x,src2x,type}},publisher,background{defaultMedia{src1x,src2x,type},tablet{src1x,src2x,type},desktop{src1x,src2x,type},description,alt,videoPlaceholderImage{src1x,src2x,type}},banner{defaultMedia{src1x,src2x,type},tablet{src1x,src2x,type},desktop{src1x,src2x,type},description,alt,videoPlaceholderImage{src1x,src2x,type}},description,otherDevelopers}}}"}'
        )

    def get_account_entitlement(self):
        return self.make_graphql_request(
            '{"query": "query{currentUser{isTwitchPrime,isAmazonPrime,twitchAccounts{tuid}}}"}'
        )
07070100000012000081A400000000000000000000000166E061F900001B32000000000000000000000000000000000000001F00000000nile-1.1.2/nile/api/library.pyfrom nile import constants
from nile.proto import sds_proto2_pb2
import logging
import uuid
import json
import time
import hashlib

from nile.utils.config import ConfigType


class Library:
    def __init__(self, config_manager, session_manager):
        self.config = config_manager
        self.session_manager = session_manager
        self.logger = logging.getLogger("LIBRARY")

    def request_sds(self, target, token, body):
        headers = {
            "X-Amz-Target": target,
            "x-amzn-token": token,
            "UserAgent": "com.amazon.agslauncher.win/3.0.9202.1",
            "Content-Type": "application/json",
            "Content-Encoding": "amz-1.0",
        }
        response = self.session_manager.session.post(
            f"{constants.AMAZON_SDS}/amazon/",
            headers=headers,
            json=body,
        )

        return response

    def request_distribution(self, target, token, body):
        headers = {
            "X-Amz-Target": target,
            "x-amzn-token": token,
            "UserAgent": "com.amazon.agslauncher.win/3.0.9202.1",
            "Content-Type": "application/json",
            "Content-Encoding": "amz-1.0",
        }
        url = (constants.AMAZON_GAMING_DISTRIBUTION_ENTITLEMENTS 
               if target.endswith(".GetEntitlements") 
               else constants.AMAZON_GAMING_DISTRIBUTION)
        response = self.session_manager.session.post(
            url,
            headers=headers,
            json=body,
        )

        return response


    def _get_sync_request_data(self, serial, next_token=None, sync_point=None):
        request_data = {
            "Operation": "GetEntitlements",
            "clientId": "Sonic",
            "syncPoint": sync_point,
            "nextToken": next_token,
            "maxResults": 50,
            "productIdFilter": None,
            "keyId": "d5dc8b8b-86c8-4fc4-ae93-18c0def5314d",
            "hardwareHash": hashlib.sha256(serial.encode()).hexdigest().upper(),
        }

        return request_data

    def sync(self):
        self.logger.info("Synchronizing library")

        sync_point = self.config.get("syncpoint", cfg_type=ConfigType.RAW)

        if sync_point is not None:
            if not sync_point:
                sync_point = None
            else:
                sync_point = float(sync_point.decode())
                self.logger.debug(f"Using sync_point {sync_point}")

        token, serial = self.config.get(
            "user",
            [
                "tokens//bearer//access_token",
                "extensions//device_info//device_serial_number",
            ],
        )
        games = list()
        if sync_point:
            cached_games = self.config.get('library')
            if cached_games:
                games.extend(cached_games)
            if len(games) == 0:
                sync_point = None
            else:
                # If there are games without titles refresh all metadata
                for game in games:
                    if not game['product'].get('title'):
                        sync_point = None
                        games = []
                        self.logger.warning("Found game without title locally, ignoring sync_point")
                        break

        next_token = None
        while True:
            request_data = self._get_sync_request_data(serial, next_token, sync_point)

            response = self.request_distribution(
                "com.amazon.animusdistributionservice.entitlement.AnimusEntitlementsService.GetEntitlements",
                token,
                request_data,
            )
            json_data = response.json()
            games.extend(json_data["entitlements"])

            if not "nextToken" in json_data:
                break
            else:
                self.logger.info("Got next token in response, making next request")
                next_token = json_data["nextToken"]

            if not response.ok:
                self.logger.error("There was an error syncing library")
                self.logger.debug(response.content)
                return
        # Remove duplicates
        games_dict = dict()
        for game in games:
            if not games_dict.get(game["product"]["id"]):
                games_dict[game["product"]["id"]] = game

        self.config.write("library", list(games_dict.values()))
        self.config.write("syncpoint", str(time.time()).encode(), cfg_type=ConfigType.RAW)
        self.logger.info("Successfully synced the library")

    def get_game_manifest(self, id: str):
        token = self.config.get("user", "tokens//bearer//access_token")

        request_data = {
            "entitlementId": id,
            "Operation": "GetGameDownload",
        }

        response = self.request_distribution(
            "com.amazon.animusdistributionservice.external.AnimusDistributionService.GetGameDownload",
            token,
            request_data,
        )

        if not response.ok:
            self.logger.error("There was an error getting game manifest")
            self.logger.debug(response.content)
            return

        response_json = response.json()

        return response_json

    def get_patches(self, id, version, file_list):
        token = self.config.get("user", "tokens//bearer//access_token")

        request_data = {
            "Operation": "GetPatches",
            "versionId": version,
            "fileHashes": file_list,
            "deltaEncodings": ["FUEL_PATCH", "NONE"],
            "adgGoodId": id,
        }

        response = self.request_sds(
            "com.amazonaws.gearbox.softwaredistribution.service.model.SoftwareDistributionService.GetPatches",
            token,
            request_data,
        )

        if not response.ok:
            self.logger.error("There was an error getting patches")
            self.logger.debug(response.content)
            return

        response_json = response.json()

        return response_json["patches"]

    def get_versions(self, game_ids):
        token = self.config.get("user", "tokens//bearer//access_token")

        request_data = {"adgProductIds": game_ids, "Operation": "GetLiveVersions"}

        response = self.request_distribution(
            "com.amazon.animusdistributionservice.external.AnimusDistributionService.GetLiveVersionIds",
            token,
            request_data,
        )

        if not response.ok:
            self.logger.error("There was an error getting versions")
            self.logger.debug(response.content)
            return

        response_json = response.json()

        return response_json["adgProductIdToVersionIdMap"]

    def get_installed_game_info(self, id):
        installed_array = self.config.get("installed")
        if not installed_array:
            return dict()
        for game in installed_array:
            if game["id"] == id:
                return game

        return dict()
07070100000013000081A400000000000000000000000166E061F900000738000000000000000000000000000000000000002300000000nile-1.1.2/nile/api/self_update.pyimport urllib.parse
import os
from nile.models import manifest
from nile.constants import CONFIG_PATH
from nile.models.hash_pairs import PatchBuilder

class SelfUpdateHandler:
    def __init__(self, session, library):
        self.session_manager = session
        self.library_manager = library

    def get_manifest(self):
        response = self.session_manager.session.get("https://gaming.amazon.com/api/distribution/v2/public/download/channel/87d38116-4cbf-4af0-a371-a5b498975346")
        data = response.json()
        return data

    def get_sdk(self):
        if os.path.exists(os.path.join(CONFIG_PATH, 'SDK')):
            return
        ag_manifest = self.get_manifest()
        url = urllib.parse.urlparse(ag_manifest['downloadUrl'])
        url = url._replace(path=url.path + '/manifest.proto')

        url = urllib.parse.urlunparse(url)

        response = self.session_manager.session.get(url)
        raw_manifest = response.content

        launcher_manifest = manifest.Manifest()
        launcher_manifest.parse(raw_manifest)

        for file in launcher_manifest.packages[0].files:
            if 'FuelSDK_x64.dll' in file.path or 'AmazonGamesSDK_' in file.path:
                url = urllib.parse.urlparse(ag_manifest['downloadUrl'])
                url = url._replace(path=url.path + '/files/' + file.hash.value)
                url = urllib.parse.urlunparse(url)

                response = self.session_manager.session.get(url, stream=True)
            
                filepath = os.path.join(CONFIG_PATH, 'SDK', file.path.replace('\\', os.sep).replace('/', os.sep))
                os.makedirs(os.path.dirname(filepath), exist_ok=True)
                with open(filepath, 'wb') as filehandle:
                    for chunk in response.iter_content(1024 * 1024):
                        filehandle.write(chunk)
        
07070100000014000081A400000000000000000000000166E061F90000019B000000000000000000000000000000000000001F00000000nile-1.1.2/nile/api/session.pyimport requests
from multiprocessing import cpu_count

class APIHandler():
    def __init__(self, config_manager):
        self.config = config_manager
        self.session = requests.Session()
        adapter = requests.adapters.HTTPAdapter(pool_maxsize=cpu_count())
        self.session.mount("https://", adapter)
        self.session.headers.update({
            'User-Agent': 'AGSLauncher/1.0.0'
        })
07070100000015000081A400000000000000000000000166E061F900000F0F000000000000000000000000000000000000001D00000000nile-1.1.2/nile/arguments.pyfrom os import path

def get_arguments():
    import argparse

    parser = argparse.ArgumentParser(description="Unofficial Amazon Games client")
    parser.add_argument(
        "--version",
        dest="version",
        action="store_true",
        help="Display nile version",
    )
    sub_parsers = parser.add_subparsers(dest="command")

    auth_parser = sub_parsers.add_parser("auth", help="Authorization related things")
    auth_parser.add_argument("--login", "-l", action="store_true", help="Login action")
    auth_parser.add_argument("--non-interactive", action="store_true", help="Display login data as JSON. Use `nile register` to finish login")
    auth_parser.add_argument(
        "--logout", action="store_true", help="Logout from the accout and deregister"
    )

    register_parser = sub_parsers.add_parser("register", help="Register device after web login")
    register_parser.add_argument("--code", help="The authorization code from web login")
    register_parser.add_argument("--client-id", help="The client id to register")
    register_parser.add_argument("--code-verifier", help="The code verifier used to generate login URL")
    register_parser.add_argument("--serial", help="The device serial to register")

    library_parser = sub_parsers.add_parser(
        "library", help="Your games library is in this place"
    )
    library_parser.add_argument("sub_command", choices=["list", "sync"])
    library_parser.add_argument(
        "--installed", "-i", action="store_true", help="List only installed games"
    )
    install_parser = sub_parsers.add_parser(
        "install", aliases=["update", "verify"], help="Install a game"
    )
    install_parser.add_argument("id", help="Specify a ID of the game to be installed")
    install_parser.add_argument("--max-workers", help="Specify max threads to be used")
    install_parser.add_argument(
        "--base-path",
        dest="base_path",
        help="Specify base installation path e.g /home/USERNAME/Games/nile It'll append save filename to that path",
    )
    install_parser.add_argument(
        "--path", dest="exact_path", help="Specify exact install location"
    )
    install_parser.add_argument(
        '--info', '-i', action="store_true", help="Print game install info instead of downloading" 
    )
    install_parser.add_argument(
        '--json', '-j', action="store_true", help="Print info in JSON format"
    )

    check_for_updates_parser = sub_parsers.add_parser("list-updates")
    check_for_updates_parser.add_argument(
        "--json", action="store_true", help="Output data in json format"
    )

    launch_parser = sub_parsers.add_parser("launch", help="Launch installed games")
    launch_parser.add_argument("id")
    launch_parser.add_argument(
        "--bottle", "-b", help="Specify bottle to use (requires Bottles)"
    )
    launch_parser.add_argument(
        "--wine-prefix", dest="wine_prefix", help="Specify wineprefix to be used"
    )
    launch_parser.add_argument("--wine", help="Specify wine binary")
    launch_parser.add_argument(
        "--no-wine",
        dest="dont_use_wine",
        action="store_true",
        help="Don't use wine (useful when specifying custom wrapper)",
    )
    launch_parser.add_argument(
        "--wrapper", help="Wrapper to bue used when launching a game"
    )

    uninstall_parser = sub_parsers.add_parser(
        "uninstall", help="Uninstall game - deletes game files specified in manifest"
    )
    uninstall_parser.add_argument("id", help="Game id")
    import_parser = sub_parsers.add_parser("import", help="Import games installed outside nile")
    import_parser.add_argument("--path", help="Path to the game's installation folder", type=path.abspath)
    import_parser.add_argument("id", help="The id of the game to import")

    return parser.parse_known_args(), parser
07070100000016000081ED00000000000000000000000166E061F900003447000000000000000000000000000000000000001700000000nile-1.1.2/nile/cli.py#!/usr/bin/env python3

import sys
import os
import logging
import json
from nile.arguments import get_arguments
from nile.downloading import manager
from nile.utils.config import Config
from nile.utils.launch import Launcher
from nile.utils.uninstall import Uninstaller
from nile.utils.importer import Importer
from nile.api import authorization, session, library, self_update
from nile import constants, version, codename


class CLI:
    def __init__(
        self, session_manager, config_manager, logger, arguments, unknown_arguments
    ):
        self.config = config_manager
        self.session = session_manager
        self.library_manager = library.Library(self.config, self.session)
        self.auth_manager = authorization.AuthenticationManager(
            self.session, self.config, self.library_manager
        )
        self.arguments = arguments
        self.logger = logger
        self.unknown_arguments = unknown_arguments

        self.self_update = self_update.SelfUpdateHandler(self.session, self.library_manager)
        try:
            self.self_update.get_sdk()
        except Exception:
            self.logger.warning("There was an error getting sdk")

        self.__migrate_old_ids()

    # Function that migrates installed and manifests from old id to product.id
    def __migrate_old_ids(self):
        installed = self.config.get("installed") or []
        library = self.config.get("library") or []
        if installed:
            for installed_game in installed:
                old_id = installed_game['id']
                for game in library:
                    if game['id'] == old_id:
                        installed_game['id'] = game['product']['id']
                        manifest_path = os.path.join(constants.CONFIG_PATH, 'manifests', game['id']+'.json')
                        if os.path.exists(manifest_path):
                            os.rename(manifest_path, os.path.join(constants.CONFIG_PATH, 'manifests', game["product"]["id"]+".raw"))
                        break
        self.config.write("installed", installed)


    def handle_auth(self):
        if self.arguments.login:
            if not self.auth_manager.is_logged_in():
                self.auth_manager.login(self.arguments.non_interactive)
                return True
            else:
                self.logger.error("You are already logged in")
                return False
        elif self.arguments.logout:
            if self.auth_manager.is_logged_in() and self.auth_manager.is_token_expired():
                self.auth_manager.refresh_token()
            self.auth_manager.logout()
            return False
        self.logger.error("Specify auth action, use --help")
    
    def handle_register(self):
        if self.auth_manager.is_logged_in():
            self.logger.error("You are already logged in")
            return False

        code = self.arguments.code
        client_id = self.arguments.client_id
        code_verifier = self.arguments.code_verifier
        serial = self.arguments.serial

        if not code:
            self.logger.error("--code is required, use --help")
            return False
        if not client_id:
            self.logger.error("--client-id is required, use --help")
            return False
        if not code_verifier:
            self.logger.error("--code-verifier is required, use --help")
            return False
        if not serial:
            self.logger.error("--serial is required, use --help")
            return False

        self.auth_manager.handle_login(code, client_id, code_verifier, serial)
        return True

    def sort_by_title(self, element):
        return (
            element["product"].get("title")
            if element["product"].get("title") is not None
            else ""
        )

    def handle_library(self):
        cmd = self.arguments.sub_command

        if cmd == "list":
            games_list = ""
            games = self.config.get("library")
            installed = self.config.get("installed")
            installed_dict = dict()
            if installed:
                for game in installed:
                    installed_dict[game["id"]] = game
            games.sort(key=self.sort_by_title)
            displayed_count = 0
            for game in games:
                if self.arguments.installed and not installed_dict.get(game["product"]["id"]):
                    continue
                genres = (
                    (f'GENRES: {game["product"]["productDetail"]["details"]["genres"]}')
                    if game["product"]["productDetail"]["details"].get("genres")
                    else ""
                )
                if not constants.SUPPORTS_COLORS:
                    games_list += f'{"(INSTALLED) " if installed_dict.get(game["product"]["id"]) and not self.arguments.installed else ""}{game["product"].get("title")} ID: {game["product"]["id"]} {genres}\n'
                else:
                    games_list += f'{constants.SHCOLORS["green"]}{"(INSTALLED) " if installed_dict.get(game["product"]["id"]) and not self.arguments.installed else ""}{constants.SHCOLORS["clear"]}{game["product"].get("title")} {constants.SHCOLORS["red"]}ID: {game["product"]["id"]}{constants.SHCOLORS["clear"]} {genres}\n'

                displayed_count += 1
            games_list += f"\n*** TOTAL {displayed_count} ***\n"
            print(games_list)

        elif cmd == "sync":
            if not self.auth_manager.is_logged_in():
                self.logger.error("User not logged in")
                sys.exit(1)
            if self.auth_manager.is_logged_in() and self.auth_manager.is_token_expired():
                self.auth_manager.refresh_token()
            self.library_manager.sync()

    def handle_install(self):
        games = self.config.get("library")
        games.sort(key=self.sort_by_title)
        matching_game = None
        for game in games:
            if game["product"]["id"] == self.arguments.id:
                matching_game = game
                break
        if not matching_game:
            self.logger.error("Couldn't find what you are looking for")
            return
        if self.auth_manager.is_logged_in() and self.auth_manager.is_token_expired():
            self.auth_manager.refresh_token()
        self.logger.info(f"Found: {matching_game['product'].get('title')}")
        self.download_manager = manager.DownloadManager(
            self.config, self.library_manager, self.session, matching_game
        )
        if not self.arguments.info:
            self.download_manager.download(
                force_verifying=bool(self.arguments.command == "verify"),
                base_install_path=self.arguments.base_path,
                install_path=self.arguments.exact_path,
            )
            self.logger.info("Download complete")
        else:
            self.download_manager.info(self.arguments.json)

    def list_updates(self):
        installed_array = self.config.get("installed")
        games = self.config.get("library")

        if not installed_array:
            if self.arguments.json:
                # An empty string is not valid JSON, so create an empty but
                # valid JSON string instead.
                print(json.dumps(list()))
            self.logger.error("No games installed")
            return

        if self.auth_manager.is_logged_in() and self.auth_manager.is_token_expired():
            self.auth_manager.refresh_token()

        # Prepare array of game ids
        game_ids = dict()
        for game in games:
            for installed_game in installed_array:
                if game["product"]["id"] == installed_game["id"]:
                    game_ids.update({game["product"]["id"]: installed_game})
        self.logger.debug(
            f"Checking for updates for {list(game_ids.keys())}, count: {len(game_ids)}"
        )
        versions = self.library_manager.get_versions(list(game_ids.keys())) or []

        updateable = list()

        for product_id, game in game_ids.items():
            version = versions[product_id]
            if version != game["version"]:
                updateable.append(product_id)
        self.logger.debug(f"Updateable games: {updateable}")
        if self.arguments.json:
            print(json.dumps(updateable))
            return

        if len(updateable) == 0:
            self.logger.info("No updates available")
            return

        games.sort(key=self.sort_by_title)

        print("Games with updates:")
        for game in games:
            if game["product"]["id"] in updateable:
                print(game["product"].get("title"), game["product"]["id"])
        print(f"NUMBER OF GAMES: {len(updateable)}")

    def handle_launch(self):
        games = self.config.get("library")
        matching_game = None
        self.logger.info(f"Searching for {self.arguments.id}")
        for game in games:
            if game["product"]["id"] == self.arguments.id:
                matching_game = game
                break
        if not matching_game:
            self.logger.error("No game match")
            return
        self.logger.debug(f"Found {matching_game['product'].get('title')}")

        self.logger.debug(
            f"Checking if game {matching_game['product'].get('title')} id: {matching_game['id']} is installed"
        )
        installed_games = self.config.get("installed")

        if not installed_games:
            self.logger.error("No game is installed")
            return

        found = None
        for installed_game in installed_games:
            if installed_game["id"] == matching_game["product"]["id"]:
                found = installed_game
                break

        if not found:
            self.logger.error("Game is not installed")
            return

        launcher = Launcher(self.config, self.arguments, self.unknown_arguments, matching_game)

        launcher.start(found["path"])

    def handle_uninstall(self):
        uninstaller = Uninstaller(self.config, self.arguments)
        uninstaller.uninstall()
    
    def handle_import(self):
        id = self.arguments.id
        if not id:
            self.logger.error("id is required")
            return

        path = self.arguments.path
        if not path:
            self.logger.error("--path is required")
            return
        
        if self.auth_manager.is_logged_in() and self.auth_manager.is_token_expired():
            self.auth_manager.refresh_token()

        games = self.config.get("library")
        matching_game = None
        self.logger.info(f"Searching for {self.arguments.id}")
        for game in games:
            if game["product"]["id"] == self.arguments.id:
                matching_game = game
                break
        if not matching_game:
            self.logger.error("No game match")
            return
        self.logger.debug(f"Found {matching_game['product'].get('title')}")

        self.logger.debug(
            f"Checking if game {matching_game['product'].get('title')} id: {matching_game['id']} is already installed"
        )
        installed_games = self.config.get("installed")

        if installed_games:
            for installed_game in installed_games:
                if installed_game["id"] == matching_game["product"]["id"]:
                    self.logger.error(
                        f"{matching_game['product'].get('title')} is already installed"
                    )
                    return
        self.download_manager = manager.DownloadManager(
            self.config, self.library_manager, self.session, matching_game
        )
        importer = Importer(
            matching_game, path, self.config, self.library_manager, self.session, self.download_manager
        )
        importer.import_game()
 
def main():
    (arguments, unknown_arguments), parser = get_arguments()
    if arguments.version:
        print(version, codename)
        return 0
    debug_mode = "-d" in unknown_arguments or "--debug" in unknown_arguments
    if debug_mode:
        if "-d" in unknown_arguments:
            unknown_arguments.remove("-d")
        elif "--debug" in unknown_arguments:
            unknown_arguments.remove("--debug")

    logging_level = logging.DEBUG if debug_mode else logging.INFO
    logging.basicConfig(
        level=logging_level, format="%(levelname)s [%(name)s]:\t %(message)s"
    )
    logger = logging.getLogger("CLI")

    config_manager = Config()
    session_manager = session.APIHandler(config_manager)
    cli = CLI(session_manager, config_manager, logger, arguments, unknown_arguments)

    command = arguments.command

    if command == "auth":
        cli.handle_auth()
    elif command == "register":
        cli.handle_register()
    elif command == "library":
        cli.handle_library()
    elif command in ["install", "verify", "update"]:
        cli.handle_install()
    elif command == "list-updates":
        cli.list_updates()
    elif command == "launch":
        cli.handle_launch()
    elif command == "uninstall":
        cli.handle_uninstall()
    elif command == "import":
        cli.handle_import()
    else:
        print(
            "You didn't provide any argument, GUI will be there someday, for now here is help"
        )
        parser.print_help()

    return 0


if __name__ == "__main__":
    sys.exit(main())
07070100000017000081A400000000000000000000000166E061F90000038D000000000000000000000000000000000000001D00000000nile-1.1.2/nile/constants.pyimport os
import sys
from platformdirs import user_config_dir

MARKETPLACEID = "ATVPDKIKX0DER"
AMAZON_API = "https://api.amazon.com"
AMAZON_SDS = "https://sds.amazon.com"
AMAZON_GAMING_GRAPHQL = "https://gaming.amazon.com/graphql"
AMAZON_GAMING_DISTRIBUTION = "https://gaming.amazon.com/api/distribution/v2/public"
AMAZON_GAMING_DISTRIBUTION_ENTITLEMENTS = "https://gaming.amazon.com/api/distribution/entitlements"

FUZZY_SEARCH_RATIO = 0.7

DEFAULT_INSTALL_PATH = os.path.join(
    os.getenv("HOME", os.path.expanduser("~")), "Games", "nile"
)

CONFIG_PATH = os.path.join(
    os.getenv("NILE_CONFIG_PATH", os.getenv("XDG_CONFIG_HOME", user_config_dir(roaming=True))), "nile"
)

ILLEGAL_FNAME_CHARS = [":","/","*","?","<",">","\\","|","™","\"","®"]

SHCOLORS = {
    "clear": "\033[0m",
    "red": "\033[31m",
    "green": "\033[1;32m",
    "blue": "\033[34m",
}
SUPPORTS_COLORS = sys.platform != "win32"
07070100000018000041ED00000000000000000000000266E061F900000000000000000000000000000000000000000000001C00000000nile-1.1.2/nile/downloading07070100000019000081A400000000000000000000000166E061F9000020AD000000000000000000000000000000000000002700000000nile-1.1.2/nile/downloading/manager.pyimport json
import logging
import os
import urllib.parse
from concurrent.futures import ThreadPoolExecutor
from multiprocessing import cpu_count
from time import sleep
import nile.utils.download as dl_utils
from nile.models import manifest, hash_pairs, patch_manifest
from nile.downloading.progress import ProgressBar
from nile.downloading.worker import DownloadWorker
from nile.utils.config import ConfigType
from nile import constants

class DownloadManager:
    def __init__(self, config_manager, library_manager, session_manager, game):
        self.config = config_manager
        self.library_manager = library_manager
        self.session = session_manager
        self.game = game
        self.progress_bar = None
        self.logger = logging.getLogger("DOWNLOAD")
        self.logger.debug("Initialized Download Manager")

        self.manifest = None
        self.old_manifest = None
        self.threads = []

    def get_manifest(self):
        game_manifest = self.library_manager.get_game_manifest(self.game["id"])
        self.logger.debug("Got download manifest")
        self.version = game_manifest["versionId"]
        self.downloadUrl = game_manifest["downloadUrl"]

        url = urllib.parse.urlparse(self.downloadUrl)
        url = url._replace(path=url.path + '/manifest.proto')

        url = urllib.parse.urlunparse(url)
        self.logger.debug("Getting protobuff manifest")
        response = self.session.session.get(url)
        r_manifest = manifest.Manifest()
        self.logger.debug("Parsing manifest data")
        r_manifest.parse(response.content)
        self.protobuff_manifest = response.content
        return r_manifest

    def get_patchmanifest(self, comparison: manifest.ManifestComparison):
        self.logger.info("Generating patches, this might take a second...")
        hash_pair_builder = hash_pairs.PatchBuilder(comparison)
        hash_pair_builder.build_hashpairs()
        patches = []
        for hash_pair in hash_pair_builder.get_next_hashes():
            patches.extend(
                self.library_manager.get_patches(
                    self.game["id"], self.version, hash_pair
                )
            )

        return patch_manifest.PatchManifest.build_patch_manifest(comparison, patches)

    def get_installed_version(self):
        installed_games = self.config.get("installed")
        if installed_games:
            for game in installed_games:
                if self.game["product"]["id"] == game["id"]:
                    return game["version"]
        return None

    def load_installed_manifest(self):
        old_manifest_pb = self.config.get(f"manifests/{self.game['product']['id']}", cfg_type=ConfigType.RAW)
        old_manifest = None
        if old_manifest_pb:
            old_manifest = manifest.Manifest()
            old_manifest.parse(old_manifest_pb)
        return old_manifest

    def download(self, force_verifying=False, base_install_path="", install_path=""):
        game_location = base_install_path
        directory_name = self.game['product'].get("title") or self.game['product']['id']
        directory_name = dl_utils.save_directory_name(directory_name)
        if not base_install_path:
            game_location = install_path
        else:
            game_location = os.path.join(
                base_install_path,
                directory_name,
            )
        if not base_install_path and not install_path:
            game_location = os.path.join(
                constants.DEFAULT_INSTALL_PATH,
                directory_name,
            )
        saved_location = self.library_manager.get_installed_game_info(
            self.game["id"]
        ).get("path")
        if saved_location:
            game_location = saved_location

        self.install_path = game_location
        if not force_verifying:
            self.manifest = self.get_manifest()
            self.old_manifest = self.load_installed_manifest()
        else:
            self.logger.debug("Loading manifest from the disk")
            self.version = self.get_installed_version()
            self.manifest = self.load_installed_manifest()

        if not self.manifest:
            self.logger.error("Unable to load manifest")
            return
        self.logger.debug(f"Number of packages: {len(self.manifest.packages)}")

        comparison = manifest.ManifestComparison.compare(
            self.manifest, self.old_manifest
        )

        if len(comparison.new) == 0:
            self.logger.info("Game is up to date")
            self.finish(False)
            return
        total_size = sum(f.size for f in comparison.new)
        self.info()

        if not dl_utils.check_available_space(total_size, game_location):
            self.logger.error("Not enough space available")
            return

        readable_size = dl_utils.get_readable_size(total_size)

        self.progress_bar = ProgressBar(total_size, f"{round(readable_size[0],2)}{readable_size[1]}")
        self.progress_bar.start()

        self.thpool = ThreadPoolExecutor(max_workers=6)
        for f in comparison.new:
            file_path = os.path.join(
                game_location, f.path.replace("\\", "/")
            )
            dir, _ = os.path.split(file_path)
            if not os.path.exists(dir):
                os.makedirs(dir)
            url = urllib.parse.urlparse(self.downloadUrl)
            url = url._replace(path=url.path + '/files/' + f.hash.value)
            url = urllib.parse.urlunparse(url)
            worker = DownloadWorker(url, f, game_location, self.session, self.progress_bar)
            # worker.execute()
            self.threads.append(self.thpool.submit(worker.execute))

        while True:
            is_done = False
            for thread in self.threads:
                is_done = thread.done()
                if is_done == False:
                    break
            if is_done:
                break
            # self.progressbar.print()
            sleep(0.5)

        for f in comparison.removed:
            file_path = os.path.join(game_location, f.path.replace("\\", "/"))
            if os.path.exists(file_path):
                os.remove(file_path)

        self.progress_bar.completed = True
        self.finish(force_verifying)

    def info(self, json_format=False):
        self.manifest = self.get_manifest()

        if not self.manifest:
            self.logger.error("Unable to load manifest")
            return
        self.logger.debug(f"Number of packages: {len(self.manifest.packages)}")

        total_size = sum(f.size for f in self.manifest.packages[0].files)
        if json_format:
            output = {
                'download_size': total_size
            }
            print(json.dumps(output))
        else:
            readable_size = dl_utils.get_readable_size(total_size)
            self.logger.info(
                f"Download size: {round(readable_size[0],2)}{readable_size[1]}"
            )

    def finish(self, verifying):
        # Save manifest to the file

        self.config.write(
            f"manifests/{self.game['product']['id']}", self.protobuff_manifest, cfg_type=ConfigType.RAW
        )

        # Save data to installed.json file
        installed_array = self.config.get("installed")

        if not installed_array:
            installed_array = list()

        total_size = sum(f.size for f in self.manifest.packages[0].files)

        # If we verify we don't want to overwrite version or path
        if verifying:
            for game in installed_array:
                if game["id"] == self.game["product"]["id"]:
                    game.update({"size": total_size})
                    break

            self.config.write("installed", installed_array)
            return
        installed_game_data = dict(
            id=self.game["product"]["id"], version=self.version, path=self.install_path, size=total_size
        )
        updated = False
        # Swap existing entry in case of updating etc..
        for i, game in enumerate(installed_array):
            if game["id"] == self.game["product"]["id"]:
                installed_array[i] = installed_game_data
                updated = True
                break

        if not updated:
            installed_array.append(installed_game_data)

        self.config.write("installed", installed_array)
0707010000001A000081A400000000000000000000000166E061F900000C1B000000000000000000000000000000000000002800000000nile-1.1.2/nile/downloading/progress.pyimport threading
import logging
from time import sleep, time


class ProgressBar(threading.Thread):
    def __init__(self, max_val, total_readable_size):
        self.logger = logging.getLogger("PROGRESS")
        self.downloaded = 0
        self.total = max_val
        self.started_at = time()
        self.last_update = time()
        self.total_readable_size = total_readable_size
        self.completed = False

        self.written_total = 0

        self.written_since_last_update = 0
        self.downloaded_since_last_update = 0

        super().__init__(target=self.print_progressbar)

    def print_progressbar(self):
        while True:
            if self.completed:
                break
            percentage = (self.downloaded / self.total) * 100
            running_time = time() - self.started_at
            runtime_h = int(running_time // 3600)
            runtime_m = int((running_time % 3600) // 60)
            runtime_s = int((running_time % 3600) % 60)

            time_since_last_update = time() - self.last_update
            if time_since_last_update == 0:
                time_since_last_update = 1

            # average_speed = self.downloaded / running_time

            if percentage > 0:
                estimated_time = (100 * running_time) / percentage - running_time
            else:
                estimated_time = 0

            estimated_h = int(estimated_time // 3600)
            estimated_time = estimated_time % 3600
            estimated_m = int(estimated_time // 60)
            estimated_s = int(estimated_time % 60)

            write_speed = self.written_since_last_update / time_since_last_update
            download_speed = self.downloaded_since_last_update / time_since_last_update

            self.written_total += self.written_since_last_update
            self.downloaded += self.downloaded_since_last_update

            self.read_since_last_update = self.written_since_last_update = 0
            self.decompressed_since_last_update = self.downloaded_since_last_update = 0

            self.logger.info(
                f"= Progress: {percentage:.02f} {self.downloaded}/{self.total}, "
                + f"Running for: {runtime_h:02d}:{runtime_m:02d}:{runtime_s:02d}, "
                + f"ETA: {estimated_h:02d}:{estimated_m:02d}:{estimated_s:02d}"
            )

            self.logger.info(
                f"= Downloaded: {self.downloaded / 1024 / 1024:.02f} MiB, "
                f"Written: {self.written_total / 1024 / 1024:.02f} MiB"
            )

            self.logger.info(
                f" + Download\t- {download_speed / 1024 / 1024:.02f} MiB/s"
            )

            self.logger.info(
                f" + Disk\t- {write_speed / 1024 / 1024:.02f} MiB/s"
            )

            self.last_update = time()
            sleep(1)

    def update_downloaded_size(self, addition):
        self.downloaded += addition

    def update_download_speed(self, addition):
        self.downloaded_since_last_update += addition

    def update_bytes_written(self, addition):
        self.written_since_last_update += addition0707010000001B000081A400000000000000000000000166E061F900000ACD000000000000000000000000000000000000002600000000nile-1.1.2/nile/downloading/worker.pyimport os
import shutil
from nile.utils.download import calculate_checksum, get_hashing_function
#from nile.models.patcher import Patcher
from nile.models.manifest import File


class DownloadWorker:
    def __init__(self, download_url, file_data, path, session_manager, progress):
        self.download_url = download_url
        self.data: File  = file_data
        self.path = path
        self.session = session_manager.session
        self.progress = progress

    def execute(self):
        file_path = os.path.join(
            self.path, self.data.path.replace("\\", os.sep)
        )
        if os.path.exists(file_path):
            if self.verify_downloaded_file(file_path):
                return
        self.get_file(file_path)
        if not self.verify_downloaded_file(file_path):
            print(f"Checksum error for {file_path}")

    def verify_downloaded_file(self, path) -> bool:
        return self.data.hash.value == calculate_checksum(get_hashing_function(self.data.hash.algorithm), path)

    def get_file(self, path):
        if os.path.exists(path + ".patch"):
            os.remove(path+".patch")

        with open(path + ".patch", "ab") as f:
            response = self.session.get(
                self.download_url, stream=True, allow_redirects=True
            )
            total = response.headers.get("Content-Length")
            if total is None:
                self.progress.update_download_speed(len(response.content))
                written = f.write(response.content)
                self.progress.update_bytes_written(written)
            else:
                total = int(total)
                for data in response.iter_content(
                    chunk_size=max(int(total / 1000), 1024 * 1024)
                ):
                    self.progress.update_download_speed(len(data))
                    written = f.write(data)
                    self.progress.update_bytes_written(written)
            f.close()

        shutil.move(path + ".patch", path)
        """
        if self.data.patch_hash:
            patch_sum = calculate_checksum(
                get_hashing_function(self.data.patch_hash_type), path + ".patch"
            )
            if self.data.patch_hash != patch_sum:
                return self.get_file(path)
            # Patch the file here
            patcher = Patcher(
                open(path, "rb"), open(path + ".patch", "rb"), open(path + ".new", "wb")
            )

            patcher.run()

            if (
                calculate_checksum(
                    get_hashing_function(self.data.target_hash_type), path + ".new"
                )
                == self.data.target_hash
            ):
                shutil.move(path + ".new", path)
        else:
        """
0707010000001C000041ED00000000000000000000000266E061F900000000000000000000000000000000000000000000001700000000nile-1.1.2/nile/models0707010000001D000081A400000000000000000000000166E061F9000004B6000000000000000000000000000000000000002500000000nile-1.1.2/nile/models/hash_pairs.pyfrom nile.models.manifest import ManifestComparison

class PatchBuilder():
    max_hashpairs_per_request = 1000
    def __init__(self, manifest_comparison: ManifestComparison):
        self.mc = manifest_comparison
        self.hashpairs = []

    def build_hashpairs(self):
        for old_f, new_f in self.mc.updated:
            self.hashpairs.append(dict(
                sourceHash=dict(value=old_f.hash.value,
                                algorithm=old_f.hash.algorithm.upper()),
                targetHash=dict(value=new_f.hash.value,
                                algorithm=new_f.hash.algorithm.upper())
            ))

        for f in self.mc.new:
            self.hashpairs.append(dict(
                sourceHash=None,
                targetHash=dict(value=f.hash.value,
                                algorithm=f.hash.algorithm.upper()),
            ))

    def get_next_hashes(self):
        """Iterator that yields new fileHashes for requests"""
        if not self.hashpairs:
            self.build_hashpairs()

        while self.hashpairs:
            yield self.hashpairs[:self.max_hashpairs_per_request]
            self.hashpairs = self.hashpairs[self.max_hashpairs_per_request:]0707010000001E000081A400000000000000000000000166E061F900001033000000000000000000000000000000000000002300000000nile-1.1.2/nile/models/manifest.pyfrom nile.proto import sds_proto2_pb2 as sds
import struct
import lzma

try:
    from Crypto.PublicKey import RSA
    from Crypto.Signature import PKCS1_v1_5
    from Crypto.Hash import SHA256
except ModuleNotFoundError:
    print("Module Crypto not found, trying to use newer Cryptodome")
    from Cryptodome.PublicKey import RSA
    from Cryptodome.Signature import PKCS1_v1_5
    from Cryptodome.Hash import SHA256
    pass
# Handles only V3 manifests


AMZ_RSA_KEY = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6fSRMUi3VpTtv9P4+KvM
AcAIP4SYbTQfB1ns7vyUjsj8nrF2lGNtQTtGLnrNmM2ElZ2R7VmQtNiRtPMxToIW
Rajin0H0OyzGrHA8P6w96Mj4q1JeORCzJeVFgLOBClCCMmB+5bJBWnJcq/sEMwu9
gGynCeiYNLt7ZMVpL1GOsNjl+yLk7OMMGpMj1JWCVFfgYE9Lud1QZJllFAWhRBoT
wTctAUZTikObFUoBm+KEiCsKIcay4WOybvJwxTNBUl2GL8c+ihrT2ntLPpb9aIJE
/gXU3Ihl5oXe/0P/QN0CRu/ybXWLiGzIYqKIok4nepkdo8V3gWR55K801pOuck0B
awIDAQAB
-----END PUBLIC KEY-----"""


class Hash:
    def __init__(self, data):
        self.value = data.value.hex()
        self.raw_value = data.value
        self.algorithm = sds.HashAlgorithm.Name(data.algorithm)


class Dir:
    def __init__(self, data):
        self.path = data.path
        self.mode = data.mode


class File:
    def __init__(self, data):
        self.path = data.path
        self.mode = data.mode
        self.size = data.size
        self.created = data.created
        self.hash = Hash(data.hash)
        self.hidden = data.hidden
        self.system = data.system


class Package:
    def __init__(self, package):
        self.name = package.name
        self.files = [File(f_data) for f_data in package.files]
        self.dirs = [Dir(dir_data) for dir_data in package.dirs]


class Manifest:
    _amz_rsa_key = RSA.importKey(AMZ_RSA_KEY)

    def __init__(self):
        self.header_pb = None
        self.manifest_pb = None
        self.packages = []

    def parse(self, content):
        header = sds.ManifestHeader()
        header_size = struct.unpack(">I", content[:4])[0]

        self.header_pb = header.FromString(content[4 : 4 + header_size])
        raw_manifest = self._decompress(content[4 + header_size :])

        if not self._verify(raw_manifest):
            raise ValueError("Failed to verify manifest signature")
        self.manifest_pb = sds.Manifest().FromString(raw_manifest)
        for package in self.manifest_pb.packages:
            self.packages.append(Package(package))

    def _verify(self, manifest_content):
        if self.header_pb.signature.algorithm == sds.sha256_with_rsa:
            signer = PKCS1_v1_5.new(self._amz_rsa_key)
            digest = SHA256.new(manifest_content)
            return signer.verify(digest, self.header_pb.signature.value)
        else:
            raise ValueError("Unknown signature algorithm!")

    def _decompress(self, content):
        if self.header_pb.compression.algorithm == sds.lzma:
            return lzma.decompress(content)
        elif self.header_pb.compression.algorithm == sds.none:
            return content
        else:
            raise ValueError("Unknown compression algorithm!")


class ManifestComparison:
    def __init__(self):
        self.new = []
        self.removed = []
        self.updated = []

    @classmethod
    def compare(cls, manifest, old_manifest=None):
        comparison = cls()

        if old_manifest:
            old_files = dict()
            for f in old_manifest.packages[0].files:
                old_files[f.path] = f

            for f in manifest.packages[0].files:
                if f.path not in old_files:
                    comparison.new.append(f)
                    continue

                if f.hash.value != old_files[f.path].hash.value:
                    comparison.new.append(f)
                # delete files that are in old_files and new manifest from this dict
                del old_files[f.path]

            # all that remains are files that were removed!
            comparison.removed = old_files.values()
        else:
            # In this case there are just new files
            comparison.new = [f for f in manifest.packages[0].files]

        return comparison
0707010000001F000081A400000000000000000000000166E061F900000A4D000000000000000000000000000000000000002900000000nile-1.1.2/nile/models/patch_manifest.pyfrom nile.models.manifest import ManifestComparison
import enum
import os


class PatchType(enum.Enum):
    NONE = 0
    FUEL_PATCH = 1


class PatchFile:
    def __init__(
        self,
        filename,
        path,
        target_hash,
        urls,
        size,
        target_hash_type="sha256",
        patch_hash=None,
        patch_hash_type=None,
        patch_type=PatchType.NONE,
        patch_offset=0,
    ):
        self.filename = filename
        self.path = path
        self.patch_hash = patch_hash
        self.patch_hash_type = patch_hash_type
        self.target_hash = target_hash
        self.target_hash_type = target_hash_type
        self.patch_type = patch_type
        self.patch_offset = patch_offset
        self.urls = urls
        self.download_size = size


class PatchManifest:
    def __init__(self):
        self.dirs = set()
        self.files = list()

    @classmethod
    def build_patch_manifest(cls, comparison: ManifestComparison, patches_list: list):
        patchmanifest = cls()

        patches = dict()
        for patch in patches_list:
            patches[patch["targetHash"]["value"]] = patch

        for old_file, new_file in comparison.updated:
            path, filename = os.path.split(new_file.path)
            patchmanifest.dirs.add(path)

            patch = patches[new_file.hash.value]

            patch_type = (
                PatchType.NONE if patch["type"] == "NONE" else PatchType.FUEL_PATCH
            )

            patchmanifest.files.append(
                PatchFile(
                    filename=filename,
                    path=path,
                    urls=patch["downloadUrls"],
                    size=patch["size"],
                    target_hash=new_file.hash.value,
                    target_hash_type=new_file.hash.algorithm.lower(),
                    patch_hash=patch["patchHash"]["value"],
                    patch_hash_type=patch["patchHash"]["algorithm"].lower(),
                    patch_type=patch_type,
                )
            )

        for new_file in comparison.new:
            path, filename = os.path.split(new_file.path)
            patchmanifest.dirs.add(path)

            patch = patches[new_file.hash.value]
            patchmanifest.files.append(
                PatchFile(
                    filename=filename,
                    path=path,
                    urls=patch["downloadUrls"],
                    size=patch["size"],
                    target_hash=new_file.hash.value,
                    target_hash_type=new_file.hash.algorithm.lower(),
                )
            )

        return patchmanifest
07070100000020000081A400000000000000000000000166E061F9000010B2000000000000000000000000000000000000002200000000nile-1.1.2/nile/models/patcher.py# FUEL_PATCH patcher
# https://github.com/derrod/twl.py/blob/master/src/patching.py
import logging
import zstandard as zstd

from enum import Enum

logger = logging.getLogger('PATCHER')


class Instructions(Enum):
    Seek = 0
    Copy = 1
    Insert = 2
    Merge = 3


class PatchCompression(Enum):
    NONE = 0  # nothing
    ZSTD = 1  # zstandard


class Patcher:
    """
    Patcher to apply FUEL_PATCH patches
    Written based on reference in Twitch App, so not perfectly pythonic
    """

    def __init__(self, source_fp, patch_fp, target_fp, block_size=1024*1024):
        self._source = source_fp
        self._patch = patch_fp
        self._patch_raw = None
        self._target = target_fp
        self.block_size = block_size  # work on data in 1 MiB blocks

    def run(self):
        patch_compression = PatchCompression(int.from_bytes(self._patch.read(1), byteorder='big'))
        if patch_compression == PatchCompression.ZSTD:
            logger.debug('Delta patch is zstandard compressed')
            dctx = zstd.ZstdDecompressor()
            self._patch_raw = self._patch
            with dctx.stream_reader(self._patch_raw) as zreader:
                self._patch = zreader
                self.apply_patches()
        else:
            logger.debug('Delta patch is not compressed')
            self.apply_patches()

        # close files
        self._patch.close()
        if self._patch_raw:
            self._patch_raw.close()
        self._source.close()
        self._target.close()

    def apply_patches(self):
        while True:
            try:
                instruction, length = self.read_instruction()
                self.apply_instruction(instruction, length)
            except EOFError:
                break
            except Exception as e:
                print(repr(e))
                break

    def read_instruction(self):
        next_byte = self._patch.read(1)
        if not next_byte:
            raise EOFError

        patch_start_byte = int.from_bytes(next_byte, byteorder='big')
        instruction = Instructions(patch_start_byte >> 6)
        bit_count = 5 if instruction == Instructions.Seek else 6
        length = self.get_length(patch_start_byte, bit_count)
        if instruction == instruction.Seek and patch_start_byte & 32:
            length *= -1
        return instruction, length

    def get_length(self, encoded_length, bit_count):
        bitmask = 2**bit_count - 1
        length = encoded_length & bitmask
        if length < (bitmask - 4):
            return length + 1
        else:
            byte_count = length - (bitmask - 5)
            length = int.from_bytes(self._patch.read(byte_count), byteorder='big')
            return length + 1

    def apply_instruction(self, instruction, length):
        if instruction == Instructions.Seek:
            # logger.debug(f'Seek {length}')
            self._source.seek(length, 1)
        elif instruction == Instructions.Copy:
            # logger.debug(f'Copy {length}')
            self.copy(self._source, self._target, length)
        elif instruction == Instructions.Insert:
            # logger.debug(f'Insert {length}')
            self.copy(self._patch, self._target, length)
        elif instruction == Instructions.Merge:
            # logger.debug(f'Merge {length}')
            self.merge(length)

    def copy(self, source, target, length):
        while length > 0:
            target.write(source.read(min(self.block_size, length)))
            length -= self.block_size

    def merge(self, length):
        while length > 0:
            source_buffer = self._source.read(min(self.block_size, length))
            if not source_buffer:
                raise ValueError('Merge length is longer than source')

            patch_buffer = self._patch.read(min(self.block_size, length))
            if not patch_buffer:
                raise ValueError('Merge length is longer than patch')

            if len(patch_buffer) != len(source_buffer):
                raise ValueError('Patch and Source do not have same length!')

            for s, p in zip(source_buffer, patch_buffer):
                result = (s + p) % 2**8
                self._target.write(result.to_bytes(1, byteorder='big'))

            length -= self.block_size07070100000021000041ED00000000000000000000000266E061F900000000000000000000000000000000000000000000001600000000nile-1.1.2/nile/proto07070100000022000081A400000000000000000000000166E061F90000180A000000000000000000000000000000000000002800000000nile-1.1.2/nile/proto/sds_proto2_pb2.py# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler.  DO NOT EDIT!
# source: sds-proto2.proto
"""Generated protocol buffer code."""
from google.protobuf.internal import enum_type_wrapper
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import message as _message
from google.protobuf import reflection as _reflection
from google.protobuf import symbol_database as _symbol_database
# @@protoc_insertion_point(imports)

_sym_db = _symbol_database.Default()




DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x10sds-proto2.proto\x12\x12tv.twitch.fuel.sds\"R\n\x13\x43ompressionSettings\x12;\n\talgorithm\x18\x01 \x01(\x0e\x32(.tv.twitch.fuel.sds.CompressionAlgorithm\"!\n\x03\x44ir\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x0c\n\x04mode\x18\x02 \x01(\r\"\x89\x01\n\x04\x46ile\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x0c\n\x04mode\x18\x02 \x01(\r\x12\x0c\n\x04size\x18\x03 \x01(\x03\x12\x0f\n\x07\x63reated\x18\x04 \x01(\t\x12&\n\x04hash\x18\x05 \x01(\x0b\x32\x18.tv.twitch.fuel.sds.Hash\x12\x0e\n\x06hidden\x18\x06 \x01(\x08\x12\x0e\n\x06system\x18\x07 \x01(\x08\"K\n\x04Hash\x12\x34\n\talgorithm\x18\x01 \x01(\x0e\x32!.tv.twitch.fuel.sds.HashAlgorithm\x12\r\n\x05value\x18\x02 \x01(\x0c\"9\n\x08Manifest\x12-\n\x08packages\x18\x01 \x03(\x0b\x32\x1b.tv.twitch.fuel.sds.Package\"\xa8\x01\n\x0eManifestHeader\x12<\n\x0b\x63ompression\x18\x01 \x01(\x0b\x32\'.tv.twitch.fuel.sds.CompressionSettings\x12&\n\x04hash\x18\x02 \x01(\x0b\x32\x18.tv.twitch.fuel.sds.Hash\x12\x30\n\tsignature\x18\x03 \x01(\x0b\x32\x1d.tv.twitch.fuel.sds.Signature\"g\n\x07Package\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\'\n\x05\x66iles\x18\x02 \x03(\x0b\x32\x18.tv.twitch.fuel.sds.File\x12%\n\x04\x64irs\x18\x03 \x03(\x0b\x32\x17.tv.twitch.fuel.sds.Dir\"U\n\tSignature\x12\x39\n\talgorithm\x18\x01 \x01(\x0e\x32&.tv.twitch.fuel.sds.SignatureAlgorithm\x12\r\n\x05value\x18\x02 \x01(\x0c**\n\x14\x43ompressionAlgorithm\x12\x08\n\x04none\x10\x00\x12\x08\n\x04lzma\x10\x01*)\n\rHashAlgorithm\x12\n\n\x06sha256\x10\x00\x12\x0c\n\x08shake128\x10\x01*)\n\x12SignatureAlgorithm\x12\x13\n\x0fsha256_with_rsa\x10\x00')

_COMPRESSIONALGORITHM = DESCRIPTOR.enum_types_by_name['CompressionAlgorithm']
CompressionAlgorithm = enum_type_wrapper.EnumTypeWrapper(_COMPRESSIONALGORITHM)
_HASHALGORITHM = DESCRIPTOR.enum_types_by_name['HashAlgorithm']
HashAlgorithm = enum_type_wrapper.EnumTypeWrapper(_HASHALGORITHM)
_SIGNATUREALGORITHM = DESCRIPTOR.enum_types_by_name['SignatureAlgorithm']
SignatureAlgorithm = enum_type_wrapper.EnumTypeWrapper(_SIGNATUREALGORITHM)
none = 0
lzma = 1
sha256 = 0
shake128 = 1
sha256_with_rsa = 0


_COMPRESSIONSETTINGS = DESCRIPTOR.message_types_by_name['CompressionSettings']
_DIR = DESCRIPTOR.message_types_by_name['Dir']
_FILE = DESCRIPTOR.message_types_by_name['File']
_HASH = DESCRIPTOR.message_types_by_name['Hash']
_MANIFEST = DESCRIPTOR.message_types_by_name['Manifest']
_MANIFESTHEADER = DESCRIPTOR.message_types_by_name['ManifestHeader']
_PACKAGE = DESCRIPTOR.message_types_by_name['Package']
_SIGNATURE = DESCRIPTOR.message_types_by_name['Signature']
CompressionSettings = _reflection.GeneratedProtocolMessageType('CompressionSettings', (_message.Message,), {
  'DESCRIPTOR' : _COMPRESSIONSETTINGS,
  '__module__' : 'sds_proto2_pb2'
  # @@protoc_insertion_point(class_scope:tv.twitch.fuel.sds.CompressionSettings)
  })
_sym_db.RegisterMessage(CompressionSettings)

Dir = _reflection.GeneratedProtocolMessageType('Dir', (_message.Message,), {
  'DESCRIPTOR' : _DIR,
  '__module__' : 'sds_proto2_pb2'
  # @@protoc_insertion_point(class_scope:tv.twitch.fuel.sds.Dir)
  })
_sym_db.RegisterMessage(Dir)

File = _reflection.GeneratedProtocolMessageType('File', (_message.Message,), {
  'DESCRIPTOR' : _FILE,
  '__module__' : 'sds_proto2_pb2'
  # @@protoc_insertion_point(class_scope:tv.twitch.fuel.sds.File)
  })
_sym_db.RegisterMessage(File)

Hash = _reflection.GeneratedProtocolMessageType('Hash', (_message.Message,), {
  'DESCRIPTOR' : _HASH,
  '__module__' : 'sds_proto2_pb2'
  # @@protoc_insertion_point(class_scope:tv.twitch.fuel.sds.Hash)
  })
_sym_db.RegisterMessage(Hash)

Manifest = _reflection.GeneratedProtocolMessageType('Manifest', (_message.Message,), {
  'DESCRIPTOR' : _MANIFEST,
  '__module__' : 'sds_proto2_pb2'
  # @@protoc_insertion_point(class_scope:tv.twitch.fuel.sds.Manifest)
  })
_sym_db.RegisterMessage(Manifest)

ManifestHeader = _reflection.GeneratedProtocolMessageType('ManifestHeader', (_message.Message,), {
  'DESCRIPTOR' : _MANIFESTHEADER,
  '__module__' : 'sds_proto2_pb2'
  # @@protoc_insertion_point(class_scope:tv.twitch.fuel.sds.ManifestHeader)
  })
_sym_db.RegisterMessage(ManifestHeader)

Package = _reflection.GeneratedProtocolMessageType('Package', (_message.Message,), {
  'DESCRIPTOR' : _PACKAGE,
  '__module__' : 'sds_proto2_pb2'
  # @@protoc_insertion_point(class_scope:tv.twitch.fuel.sds.Package)
  })
_sym_db.RegisterMessage(Package)

Signature = _reflection.GeneratedProtocolMessageType('Signature', (_message.Message,), {
  'DESCRIPTOR' : _SIGNATURE,
  '__module__' : 'sds_proto2_pb2'
  # @@protoc_insertion_point(class_scope:tv.twitch.fuel.sds.Signature)
  })
_sym_db.RegisterMessage(Signature)

if _descriptor._USE_C_DESCRIPTORS == False:

  DESCRIPTOR._options = None
  _COMPRESSIONALGORITHM._serialized_start=798
  _COMPRESSIONALGORITHM._serialized_end=840
  _HASHALGORITHM._serialized_start=842
  _HASHALGORITHM._serialized_end=883
  _SIGNATUREALGORITHM._serialized_start=885
  _SIGNATUREALGORITHM._serialized_end=926
  _COMPRESSIONSETTINGS._serialized_start=40
  _COMPRESSIONSETTINGS._serialized_end=122
  _DIR._serialized_start=124
  _DIR._serialized_end=157
  _FILE._serialized_start=160
  _FILE._serialized_end=297
  _HASH._serialized_start=299
  _HASH._serialized_end=374
  _MANIFEST._serialized_start=376
  _MANIFEST._serialized_end=433
  _MANIFESTHEADER._serialized_start=436
  _MANIFESTHEADER._serialized_end=604
  _PACKAGE._serialized_start=606
  _PACKAGE._serialized_end=709
  _SIGNATURE._serialized_start=711
  _SIGNATURE._serialized_end=796
# @@protoc_insertion_point(module_scope)
07070100000023000041ED00000000000000000000000266E061F900000000000000000000000000000000000000000000001600000000nile-1.1.2/nile/utils07070100000024000081A400000000000000000000000166E061F900001036000000000000000000000000000000000000002000000000nile-1.1.2/nile/utils/config.pyimport os
import json
# YAML ready (currently not needed though)
# import yaml
import logging
from typing import Union
from enum import Enum
import nile.constants as constants


class ConfigType(Enum):
    RAW = 0
    JSON = 1
    # YAML = 2


class Config:
    """
    Handles writing and reading from config with automatic in memory caching
    TODO: Make memory caching opt out in some cases
    """

    def __init__(self):
        self.logger = logging.getLogger("CONFIG")
        self.cache = {}

    def _join_path_name(self, name: str, cfg_type: ConfigType) -> str:
        extension = "json"
        if cfg_type == ConfigType.RAW:
            extension = "raw"
        # elif cfg_type == ConfigType.YAML:
        #     extension = "yaml"
        elif cfg_type == ConfigType.JSON:
            extension = "json"
        return os.path.join(constants.CONFIG_PATH, f"{name}.{extension}")

    def check_if_config_dir_exists(self):
        """
        Checks if config dir exists
        creates directory if needed
        """
        if not os.path.exists(constants.CONFIG_PATH):
            os.makedirs(constants.CONFIG_PATH)

    def remove(self, store, cfg_type=ConfigType.JSON):
        """
        Remove file
        """
        path = self._join_path_name(store, cfg_type)
        if os.path.exists(path):
            os.remove(path)

    def write(self, store: str, data: any, cfg_type=ConfigType.JSON):
        """
        Stringifies data to json and overrides file contents
        """
        self.check_if_config_dir_exists()
        directory, filename = os.path.split(
            self._join_path_name(store, cfg_type))
        os.makedirs(directory, exist_ok=True)
        file_path = self._join_path_name(store, cfg_type)
        self.cache.update({store: data})
        mode = "w"
        if cfg_type == ConfigType.RAW:
            parsed = data
            mode += "b"
        # elif cfg_type == ConfigType.YAML:
        #     parsed = yaml.safe_dump(data)
        elif cfg_type == ConfigType.JSON:
            parsed = json.dumps(data)
        stream = open(file_path, mode)

        stream.write(parsed)

    def get(self, store: str, key: Union[str, list] = None, cfg_type=ConfigType.JSON) -> any or None:
        """
        Get value of provided key from store
        Double slash separated keys can access object values
        e.g tokens//bearer//access_token
        If no key provided returns whole file
        """
        file_path = self._join_path_name(store, cfg_type)
        if os.path.exists(file_path) and os.path.isfile(file_path):
            if not self.cache.get(store):
                if cfg_type == ConfigType.RAW:
                    stream = open(file_path, "rb")
                    data = stream.read()
                    stream.close()
                    return data
                elif cfg_type == ConfigType.JSON:
                    stream = open(file_path, "r")
                    data = stream.read()
                    parsed = json.loads(data)
                    self.cache.update({store: parsed})
                    stream.close()
                # elif cfg_type == ConfigType.YAML:
                #     stream = open(file_path, "r")
                #     parsed = yaml.safe_load(stream)
                #     stream.close()

            else:
                parsed = self.cache[store]
            if not key:
                return parsed
            if type(key) is str:
                keys = key.split("//")
                return self._get_value_based_on_keys(parsed, keys)
            elif type(key) is list:
                array = list()
                for option in key:
                    keys = option.split("//")
                    array.append(self._get_value_based_on_keys(parsed, keys))
                return array

        if type(key) is list:
            return [None for i in key]
        return None

    def _get_value_based_on_keys(self, parsed, keys):
        if len(keys) > 1:
            iterator = parsed.copy()
            for key in keys:
                iterator = iterator[key]

            return iterator
        return parsed.get(keys[0])
07070100000025000081A400000000000000000000000166E061F90000045A000000000000000000000000000000000000002200000000nile-1.1.2/nile/utils/download.pyimport os
import shutil
import hashlib
from nile.constants import ILLEGAL_FNAME_CHARS


def get_readable_size(size):
    power = 2**10
    n = 0
    power_labels = {0: "", 1: "K", 2: "M", 3: "G"}
    while size > power:
        size /= power
        n += 1
    return size, power_labels[n] + "B"


def save_directory_name(title: str) -> str:
    output = title
    for char in ILLEGAL_FNAME_CHARS:
        output = output.replace(char, "")
    return output


def check_available_space(size, path) -> bool:
    if not os.path.exists(path):
        os.makedirs(path)

    _, _, available = shutil.disk_usage(path)

    return size < available


def calculate_checksum(hashing_function, path):
    with open(path, 'rb') as f:
        calculate = hashing_function()
        while True:
            chunk = f.read(16 * 1024)
            if not chunk:
                break
            calculate.update(chunk)

        return calculate.hexdigest()

def get_hashing_function(h_type):
    if h_type.lower() == "sha256":
        return hashlib.sha256
    elif h_type.lower() == "shake128":
        return hashlib.shake_12807070100000026000081A400000000000000000000000166E061F900000EAC000000000000000000000000000000000000002200000000nile-1.1.2/nile/utils/importer.pyimport os
import logging
from time import sleep
from nile.models import manifest
from nile.downloading.worker import DownloadWorker
from nile.utils.config import ConfigType
from multiprocessing import cpu_count
from concurrent.futures import ThreadPoolExecutor, as_completed

class Importer:
    def __init__(self, game, folder_path, config, library_manager, session_manager, download_manager):
        self.game = game
        self.folder_path = folder_path
        self.config = config
        self.library_manager = library_manager
        self.session_manager = session_manager
        self.download_manager = download_manager
        self.logger = logging.getLogger("IMPORT")

        self.threads = []

    def get_patchmanifest(self):
        game_manifest = self.library_manager.get_game_manifest(self.game['id'])

        download_url = game_manifest["downloadUrls"][0]
        self.logger.debug("Getting protobuff manifest")
        response = self.session_manager.session.get(download_url)
        r_manifest = manifest.Manifest()
        self.logger.debug("Parsing manifest data")
        r_manifest.parse(response.content)
        self.protobuff_manifest = response.content

        self.version = game_manifest["versionId"]
        self.download_manager.version = self.version
        comparison = manifest.ManifestComparison.compare(
            r_manifest
        )
        return self.download_manager.get_patchmanifest(comparison)

    def stop_threads(self):
        self.thpool.shutdown(wait=False, cancel_futures=True)

    def verify_integrity(self):
        patchmanifest = self.get_patchmanifest()
        self.thpool = ThreadPoolExecutor(max_workers=cpu_count())

        for file in patchmanifest.files:
            local_path = os.path.join(self.folder_path, file.path.replace("\\", os.sep), file.filename)
            self.logger.debug(f"Verifying: {local_path}")
            if not os.path.isfile(local_path):
                self.stop_threads()
                self.logger.error(f"{local_path} is missing or corrupted")
                return False

            worker = DownloadWorker(
                file,
                local_path,
                self.session_manager,
                None
            )
            self.threads.append(self.thpool.submit(worker.verify_downloaded_file, local_path))

        for thread in as_completed(self.threads):
            if thread.cancelled() or not thread.result():
                self.stop_threads()
                return False

        return True

    def import_game(self):
        if not os.path.isdir(self.folder_path):
            self.logger.error(f"{self.folder_path} is not a directory")
            return

        self.logger.info(f"\tVerifying local files")
        if not self.verify_integrity():
            self.logger.error(
                f"There are missing or corrupted files for {self.game['product'].get('title')}. Failed import."
            )
            return

        self.logger.info(f"\tImporting {self.game['product'].get('title')}")
        self.finish()
        self.logger.info(f"Imported {self.game['product'].get('title')}")


    def finish(self):
        # Save manifest to the file

        self.config.write(
            f"manifests/{self.game['product']['id']}", self.protobuff_manifest, cfg_type=ConfigType.RAW
        )

        # Save data to installed.json file
        installed_array = self.config.get("installed")

        if not installed_array:
            installed_array = list()

        installed_game_data = dict(
            id=self.game["product"]["id"], version=self.version, path=self.folder_path
        )

        installed_array.append(installed_game_data)

        self.config.write("installed", installed_array)
07070100000027000081A400000000000000000000000166E061F9000025E5000000000000000000000000000000000000002000000000nile-1.1.2/nile/utils/launch.pyimport sys
import os
import shutil
import shlex
import subprocess
import logging
import json5
import time
import signal
import ctypes
from nile.constants import CONFIG_PATH
from nile.utils.process import Process

class NoMoreChildren(Exception):
    pass


class LaunchInstruction:
    def __init__(self):
        self.version = str()
        self.command = str()
        self.arguments = list()
        self.cwd = str()

    @classmethod
    def parse(cls, game_path, path, unknown_arguments):
        instruction = cls()
        stream = open(path, "r")
        raw_data = stream.read()
        json_data = json5.loads(raw_data)
        stream.close()
        instruction.version = json_data["SchemaVersion"]
        instruction.command = os.path.join(game_path, json_data["Main"]["Command"].replace("\\", os.sep))
        instruction.arguments = json_data["Main"].get("Args") or list()
        instruction.arguments.extend(unknown_arguments)
        instruction.cwd = game_path
        working_dir_override = json_data["Main"].get("WorkingSubdirOverride")
        if working_dir_override:
            instruction.cwd = os.path.join(game_path, working_dir_override.replace("\\", os.sep))
        return instruction


class Launcher:
    def __init__(self, config_manager, arguments, unknown_arguments, game):
        self.config = config_manager
        self.game = game
        self.bottle = arguments.bottle
        self.wrapper = arguments.wrapper
        self.wine_prefix = arguments.wine_prefix
        self.wine_bin = arguments.wine
        if not self.wine_bin:
            self.wine_bin = shutil.which("wine")
        self.dont_use_wine = arguments.dont_use_wine
        self.logger = logging.getLogger("LAUNCHER")
        self.unknown_arguments = unknown_arguments
        self.flatpak_installed = shutil.which("flatpak")


        self.env = self.sanitize_environment()
        self.bottles_bin = self._get_bottles_bin()

    def _get_installed_data(self):
        return self.config.get("installed")

    def sanitize_environment(self) -> dict:
        env = os.environ.copy()
        # For pyinstaller environment - avoid shadowing libraries for subprocesses
        # /tmp/hash/nile/utils/launch.py -> /tmp/hash/nile/utils -> /tmp/hash
        if not getattr(sys, 'frozen', False) and not hasattr(sys, '_MEIPASS'):
            return env
        bundle_dir = sys._MEIPASS

        ld_library = env.get("LD_LIBRARY_PATH")
        if ld_library:
            splitted = ld_library.split(":")
            try:
                splitted.remove(bundle_dir)
            except ValueError:
                pass
            env.update({"LD_LIBRARY_PATH": ":".join(splitted)})
        
        return env


    def get_scummvm_command(self):
        os_path = shutil.which("scummvm")
        output_command = []
        if not os_path and self.flatpak_installed:
            flatpak_exists = subprocess.run(
                ["flatpak", "info", "org.scummvm.ScummVM"], stdout=subprocess.DEVNULL, env=self.env).returncode == 0
            if flatpak_exists:
                output_command = ["flatpak", "run", "org.scummvm.ScummVM"]
        elif os_path:
            output_command = [os_path]
        return output_command


    def _get_bottles_bin(self):
        os_path = shutil.which("bottles-cli")
        if os_path:
            return [os_path, "run"]
        elif self.flatpak_installed:
            process = subprocess.run(
                ["flatpak", "info", "com.usebottles.bottles"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env=self.env
            )

            if process.returncode != 1:
                return [
                    self.flatpak_installed,
                    "run",
                    "--command=bottles-cli",
                    "com.usebottles.bottles",
                    "run",
                ]
        return None

    def create_bottles_command(self, exe, arguments=[]):
        command = self._get_bottles_bin() + ["-b", self.bottle, "-e", exe]
        if len(arguments) > 0:
            command.extend(["-a"] + arguments)
        return command

    def start(self, game_path):

        if not os.path.exists(game_path):
            self.logger.error(
                "Unable to launch a game: installation path doesn't exist"
            )
            return

        # Parse fuel.json file
        fuel_path = os.path.join(game_path, "fuel.json")

        if not os.path.exists(fuel_path):
            self.logger.error("Unable to launch a game: fuel.json doesn't exist")
            return

        instruction = LaunchInstruction.parse(
            game_path, fuel_path, self.unknown_arguments
        )

        display_name = self.config.get('user', 'extensions//customer_info//given_name') or ''
        
        sdk_path = os.path.join(CONFIG_PATH, 'SDK', 'Amazon Games Services')
        fuel_dir = os.path.join(sdk_path, 'Legacy')
        amazon_sdk = os.path.join(sdk_path, 'AmazonGamesSDK')

        self.env.update({
            'FUEL_DIR': fuel_dir,
            'AMAZON_GAMES_SDK_PATH': amazon_sdk,
            'AMAZON_GAMES_FUEL_ENTITLEMENT_ID': self.game['id'],
            'AMAZON_GAMES_FUEL_PRODUCT_SKU': self.game['product']['sku'],
            'AMAZON_GAMES_FUEL_DISPLAY_NAME': display_name
            })


        command = list()
        if self.wrapper:
            splited_wrapper = shlex.split(self.wrapper)
            command.extend(splited_wrapper)
        if sys.platform == "win32":
            command.append(instruction.command)
            command.extend(instruction.arguments)
        else:
            scummvm_command = self.get_scummvm_command()
            if instruction.command.lower().endswith("scummvm.exe") and len(scummvm_command) > 0:
                self.logger.info(f"Using native scummvm {scummvm_command}")
                command = scummvm_command
                command.extend(instruction.arguments)
            else:
                if not self.dont_use_wine and not self.bottle:
                    if self.wine_prefix:
                        self.env.update({"WINEPREFIX": self.wine_prefix})
                    command.append(self.wine_bin)
                    command.append(instruction.command)
                    command.extend(instruction.arguments)
                elif self.bottle and self.bottles_bin:
                    command = self.create_bottles_command(
                        instruction.command, arguments=instruction.arguments
                    )
                else: 
                    command.append(instruction.command)
                    command.extend(instruction.arguments)

        self.logger.info("Launching")
        
        status = 0

        if sys.platform == 'linux':
            libc = ctypes.cdll.LoadLibrary("libc.so.6")
            prctl = libc.prctl
            result = prctl(36 ,1, 0, 0, 0, 0) # PR_SET_CHILD_SUBREAPER = 36

            if result == -1:
                print("PR_SET_CHILD_SUBREAPER is not supported by your kernel (Linux 3.4 and above)")

            process = subprocess.Popen(command, cwd=instruction.cwd, env=self.env)
            process_pid = process.pid

            def iterate_processes():
                for child in Process(os.getpid()).iter_children():
                    if child.state == 'Z':
                        continue

                    if child.name:
                        yield child

            def hard_sig_handler(signum, _frame):
                for _ in range(3):  # just in case we race a new process.
                    for child in Process(os.getpid()).iter_children():
                        try:
                            os.kill(child.pid, signal.SIGKILL)
                        except ProcessLookupError:
                            pass


            def sig_handler(signum, _frame):
                signal.signal(signal.SIGTERM, hard_sig_handler)
                signal.signal(signal.SIGINT, hard_sig_handler)
                for _ in range(3):  # just in case we race a new process.
                    for child in Process(os.getpid()).iter_children():
                        try:
                            os.kill(child.pid, signal.SIGTERM)
                        except ProcessLookupError:
                            pass

            def is_alive():
                return next(iterate_processes(), None) is not None

            signal.signal(signal.SIGTERM, sig_handler)
            signal.signal(signal.SIGINT, sig_handler)

            def reap_children():
                nonlocal status
                while True:
                    try:
                        child_pid, child_returncode, _resource_usage = os.wait3(os.WNOHANG)
                    except ChildProcessError:
                        raise NoMoreChildren from None  # No processes remain.
                    if child_pid == process_pid:
                        status = child_returncode

                    if child_pid == 0:
                        break

            try:
                # The initial wait loop:
                #  the initial process may have been excluded. Wait for the game
                #  to be considered "started".
                if not is_alive():
                    while not is_alive():
                        reap_children()
                        time.sleep(0.1)
                while is_alive():
                    reap_children()
                    time.sleep(0.1)
                reap_children()
            except NoMoreChildren:
                print("All processes exited")


        else:
            process = subprocess.Popen(command, cwd=instruction.cwd, env=self.env)
            status = process.wait()

        sys.exit(status)
07070100000028000081A400000000000000000000000166E061F9000011D8000000000000000000000000000000000000002100000000nile-1.1.2/nile/utils/process.pyimport os


class InvalidPid(Exception):

    """Exception raised when an operation on a non-existent PID is called"""


class Process:

    """Python abstraction a Linux process"""

    def __init__(self, pid):
        try:
            self.pid = int(pid)
            self.error_cache = []
        except ValueError as err:
            raise InvalidPid("'%s' is not a valid pid" % pid) from err

    def __repr__(self):
        return "Process {}".format(self.pid)

    def __str__(self):
        return "{} ({}:{})".format(self.name, self.pid, self.state)

    def _read_content(self, file_path):
        """Return the contents from a file in /proc"""
        try:
            with open(file_path, encoding='utf-8') as proc_file:
                content = proc_file.read()
        except (ProcessLookupError, FileNotFoundError, PermissionError):
            return ""
        return content

    def get_stat(self, parsed=True):
        stat_filename = "/proc/{}/stat".format(self.pid)
        try:
            with open(stat_filename, encoding='utf-8') as stat_file:
                _stat = stat_file.readline()
        except (ProcessLookupError, FileNotFoundError):
            return None
        if parsed:
            return _stat[_stat.rfind(")") + 1:].split()
        return _stat

    def get_thread_ids(self):
        """Return a list of thread ids opened by process."""
        basedir = "/proc/{}/task/".format(self.pid)
        if os.path.isdir(basedir):
            try:
                return os.listdir(basedir)
            except FileNotFoundError:
                return []
        else:
            return []

    def get_children_pids_of_thread(self, tid):
        """Return pids of child processes opened by thread `tid` of process."""
        children_path = "/proc/{}/task/{}/children".format(self.pid, tid)
        try:
            with open(children_path, encoding='utf-8') as children_file:
                children_content = children_file.read()
        except (FileNotFoundError, ProcessLookupError):
            children_content = ""
        return children_content.strip().split()

    @property
    def name(self):
        """Filename of the executable."""
        _stat = self.get_stat(parsed=False)
        if _stat:
            return _stat[_stat.find("(") + 1:_stat.rfind(")")]
        return None

    @property
    def state(self):
        """One character from the string "RSDZTW" where R is running, S is
        sleeping in an interruptible wait, D is waiting in uninterruptible disk
        sleep, Z is zombie, T is traced or stopped (on a signal), and W is
        paging.
        """
        _stat = self.get_stat()
        if _stat:
            return _stat[0]
        return None

    @property
    def cmdline(self):
        """Return command line used to run the process `pid`."""
        cmdline_path = "/proc/{}/cmdline".format(self.pid)
        _cmdline_content = self._read_content(cmdline_path)
        if _cmdline_content:
            return _cmdline_content.replace("\x00", " ").replace("\\", "/")

    @property
    def cwd(self):
        """Return current working dir of process"""
        cwd_path = "/proc/%d/cwd" % int(self.pid)
        return os.readlink(cwd_path)

    @property
    def environ(self):
        """Return the process' environment variables"""
        environ_path = "/proc/{}/environ".format(self.pid)
        _environ_text = self._read_content(environ_path)
        if not _environ_text:
            return {}
        try:
            return dict([line.split("=", 1) for line in _environ_text.split("\x00") if line])
        except ValueError:
            if environ_path not in self.error_cache:
                self.error_cache.append(environ_path)
            return {}

    @property
    def children(self):
        """Return the child processes of this process"""
        _children = []
        for tid in self.get_thread_ids():
            for child_pid in self.get_children_pids_of_thread(tid):
                _children.append(Process(child_pid))
        return _children

    def iter_children(self):
        """Iterator that yields all the children of a process"""
        for child in self.children:
            yield child
            yield from child.iter_children()

    def wait_for_finish(self):
        """Waits until the process finishes
        This only works if self.pid is a child process of Lutris
        """
        try:
            pid, ret_status = os.waitpid(int(self.pid) * -1, 0)
        except OSError as ex:
            return -1
        return ret_status

07070100000029000081A400000000000000000000000166E061F900000484000000000000000000000000000000000000002000000000nile-1.1.2/nile/utils/search.py# Fuzzy search for installing and for future GUI searching


def calculate_distance(s, t):
    # Initialize matrix of zeros
    rows = len(s) + 1
    cols = len(t) + 1
    distance = [[0 for j in range(cols)] for i in range(rows)]

    # Populate matrix of zeros with the indeces of each character of both strings
    for i in range(1, rows):
        for k in range(1, cols):
            distance[i][0] = i
            distance[0][k] = k

    # Iterate over the matrix to compute the cost of deletions,insertions and/or substitutions
    for col in range(1, cols):
        for row in range(1, rows):
            if s[row - 1] == t[col - 1]:
                cost = 0  # If the characters are the same in the two strings in a given position [i,j] then the cost is 0
            else:
                cost = 2
            distance[row][col] = min(
                distance[row - 1][col] + 1,  # Cost of deletions
                distance[row][col - 1] + 1,  # Cost of insertions
                distance[row - 1][col - 1] + cost,
            )  # Cost of substitutions
    Ratio = ((len(s) + len(t)) - distance[row][col]) / (len(s) + len(t))
    return Ratio
0707010000002A000081A400000000000000000000000166E061F9000008DC000000000000000000000000000000000000002300000000nile-1.1.2/nile/utils/uninstall.pyimport os
import logging
from nile.models import manifest
from nile.utils.config import ConfigType


class Uninstaller:
    def __init__(self, config_manager, arguments):
        self.config = config_manager
        self.arguments = arguments
        self.manifest = None
        self.logger = logging.getLogger("UNINSTALL")

    def uninstall(self):
        game_id = self.arguments.id
        installed_games = self.config.get("installed")

        installed_info = None
        for i, game in enumerate(installed_games):
            if game["id"] == game_id:
                installed_info = game
                installed_games.pop(i)
                break
        if not installed_info:
            self.logger.error("Game isn't installed")
            return
        # Load manifest
        self.manifest = self.load_installed_manifest(game_id)

        for f in self.manifest.packages[0].files:
            # Manifest can contain both kind of slash as a separator on the same entry
            filepath = os.path.join(installed_info["path"], f.path.replace("\\", os.sep).replace("/", os.sep))
            try:
                os.remove(filepath)
            except FileNotFoundError:
                self.logger.warning(f'Missing file "{filepath}" - skipping')

        # Remove empty directories under the installation directory
        for dirpath, dirnames, filenames in os.walk(installed_info["path"], topdown=False):
            for dirname in dirnames:
                full_path = os.path.join(dirpath, dirname)
                if not os.listdir(full_path):
                    os.rmdir(full_path)

        # Remove installation directory if it's empty
        if not os.listdir(installed_info["path"]):
            os.rmdir(installed_info["path"])

        self.config.write("installed", installed_games)
        self.config.remove(f"manifests/{game_id}", cfg_type=ConfigType.RAW)
        self.logger.info("Game removed successfully")

    def load_installed_manifest(self, game_id):
        old_manifest_pb = self.config.get(f"manifests/{game_id}", cfg_type=ConfigType.RAW)
        old_manifest = None
        if old_manifest_pb:
            old_manifest = manifest.Manifest()
            old_manifest.parse(old_manifest_pb)
        return old_manifest
0707010000002B000081A400000000000000000000000166E061F90000058E000000000000000000000000000000000000001A00000000nile-1.1.2/pyproject.toml[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"

[project]
name = "nile"
description = "Unofficial Amazon Games client"
readme = "README.md"
requires-python = ">=3.8"
keywords = ["Amazon", "Amazon Games", "Games"]
license = { text = "GPL-3" }
authors = [
  { name = "imLinguin", email = "lidwinpawel@gmail.com" }
]
classifiers = [
  "Development Status :: 5 - Production/Stable",
  "Intended Audience :: Developers",
  "Environment :: Console",
  "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
  "Topic :: Games/Entertainment",
  "Operating System :: OS Independent",
  "Operating System :: POSIX",
  "Operating System :: POSIX :: BSD",
  "Operating System :: POSIX :: Linux",
  "Operating System :: MacOS :: MacOS X",
  "Operating System :: Microsoft :: Windows",
  "Programming Language :: Python :: 3",
  "Programming Language :: Python :: 3.8",
  "Programming Language :: Python :: 3.9",
  "Programming Language :: Python :: 3.10",
  "Programming Language :: Python :: 3 :: Only",
  "Programming Language :: Python :: Implementation :: CPython"
]
dependencies = [
  "setuptools",
  "requests",
  "protobuf",
  "pycryptodome",
  "zstandard",
  "json5",
  "platformdirs"
]
dynamic = ["version"]

[project.scripts]
nile = "nile.cli:main"

[project.urls]
Issues = "https://github.com/imLinguin/nile/issues"

[tool.setuptools.dynamic]
version = {attr = "nile.version"}
0707010000002C000081A400000000000000000000000166E061F90000003C000000000000000000000000000000000000001C00000000nile-1.1.2/requirements.txtrequests
protobuf
pycryptodome
zstandard
json5
platformdirs
07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!333 blocks
openSUSE Build Service is sponsored by