diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..287a2f0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,162 @@
+### Python template
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..35410ca
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
+# 基于编辑器的 HTTP 客户端请求
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/AppleMusicDecrypt.iml b/.idea/AppleMusicDecrypt.iml
new file mode 100644
index 0000000..d0876a7
--- /dev/null
+++ b/.idea/AppleMusicDecrypt.iml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
new file mode 100644
index 0000000..3726f20
--- /dev/null
+++ b/.idea/encodings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..f4173f5
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..dd4c951
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..ab27a3e
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..b6a6047
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..0ad25db
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,661 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are 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.
+
+ 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.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ 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 Affero 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. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ 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 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 work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero 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 Affero 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 Affero 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 Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ 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 AGPL, see
+.
diff --git a/agent.js b/agent.js
new file mode 100644
index 0000000..0987111
--- /dev/null
+++ b/agent.js
@@ -0,0 +1,113 @@
+'use strict';
+setTimeout(() => {
+ const fairplayCert = "MIIEzjCCA7agAwIBAgIIAXAVjHFZDjgwDQYJKoZIhvcNAQEFBQAwfzELMAkGA1UEBhMCVVMxEzARBgNVBAoMCkFwcGxlIEluYy4xJjAkBgNVBAsMHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MTMwMQYDVQQDDCpBcHBsZSBLZXkgU2VydmljZXMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTIwNzI1MTgwMjU4WhcNMTQwNzI2MTgwMjU4WjAwMQswCQYDVQQGEwJVUzESMBAGA1UECgwJQXBwbGUgSW5jMQ0wCwYDVQQDDARGUFMxMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCqZ9IbMt0J0dTKQN4cUlfeQRY9bcnbnP95HFv9A16Yayh4xQzRLAQqVSmisZtBK2/nawZcDmcs+XapBojRb+jDM4Dzk6/Ygdqo8LoA+BE1zipVyalGLj8Y86hTC9QHX8i05oWNCDIlmabjjWvFBoEOk+ezOAPg8c0SET38x5u+TwIDAQABo4ICHzCCAhswHQYDVR0OBBYEFPP6sfTWpOQ5Sguf5W3Y0oibbEc3MAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUY+RHVMuFcVlGLIOszEQxZGcDLL4wgeIGA1UdIASB2jCB1zCB1AYJKoZIhvdjZAUBMIHGMIHDBggrBgEFBQcCAjCBtgyBs1JlbGlhbmNlIG9uIHRoaXMgY2VydGlmaWNhdGUgYnkgYW55IHBhcnR5IGFzc3VtZXMgYWNjZXB0YW5jZSBvZiB0aGUgdGhlbiBhcHBsaWNhYmxlIHN0YW5kYXJkIHRlcm1zIGFuZCBjb25kaXRpb25zIG9mIHVzZSwgY2VydGlmaWNhdGUgcG9saWN5IGFuZCBjZXJ0aWZpY2F0aW9uIHByYWN0aWNlIHN0YXRlbWVudHMuMDUGA1UdHwQuMCwwKqAooCaGJGh0dHA6Ly9jcmwuYXBwbGUuY29tL2tleXNlcnZpY2VzLmNybDAOBgNVHQ8BAf8EBAMCBSAwFAYLKoZIhvdjZAYNAQUBAf8EAgUAMBsGCyqGSIb3Y2QGDQEGAQH/BAkBAAAAAQAAAAEwKQYLKoZIhvdjZAYNAQMBAf8EFwF+bjsY57ASVFmeehD2bdu6HLGBxeC2MEEGCyqGSIb3Y2QGDQEEAQH/BC8BHrKviHJf/Se/ibc7T0/55Bt1GePzaYBVfgF3ZiNuV93z8P3qsawAqAXzzh9o5DANBgkqhkiG9w0BAQUFAAOCAQEAVGyCtuLYcYb/aPijBCtaemxuV0IokXJn3EgmwYHZynaR6HZmeGRUp9p3f8EXu6XPSekKCCQi+a86hXX9RfnGEjRdvtP+jts5MDSKuUIoaqce8cLX2dpUOZXdf3lR0IQM0kXHb5boNGBsmbTLVifqeMsexfZryGw2hE/4WDOJdGQm1gMJZU4jP1b/HSLNIUhHWAaMeWtcJTPRBucR4urAtvvtOWD88mriZNHG+veYw55b+qA36PSqDPMbku9xTY7fsMa6mxIRmwULQgi8nOk1wNhw3ZO0qUKtaCO3gSqWdloecxpxUQSZCSW7tWPkpXXwDZqegUkij9xMFS1pr37RIg==";
+ const port = 2147483647
+
+ function newStdStringFromBuffer(content) {
+ const size = content.byteLength;
+ const cap = 2 ** Math.ceil(Math.log2(size + 1));
+ const buffer = Memory.alloc(cap);
+ Memory.copy(buffer, content.unwrap(), size);
+
+ const addr = Memory.alloc(Process.pointerSize * 3);
+ addr.writeULong(cap | 0x1);
+ addr.add(Process.pointerSize).writeULong(size);
+ addr.add(Process.pointerSize * 2).writePointer(buffer);
+
+ return { buffer: buffer, str: addr };
+ }
+
+ function newStdString(content) {
+ const size = content.length;
+ const cap = 2 ** Math.ceil(Math.log2(size + 1));
+ const buffer = Memory.alloc(cap);
+ buffer.writeUtf8String(content);
+
+ const addr = Memory.alloc(Process.pointerSize * 3);
+ addr.writeULong(cap | 0x1);
+ addr.add(Process.pointerSize).writeULong(size);
+ addr.add(Process.pointerSize * 2).writePointer(buffer);
+
+ return { buffer: buffer, str: addr };
+ }
+
+
+ const androidappmusic = Process.getModuleByName("libandroidappmusic.so");
+
+ const sessionCtrlPtr = androidappmusic.getExportByName("_ZN21SVFootHillSessionCtrl8instanceEv");
+ const sessionCtrlInstanceFunc = new NativeFunction(sessionCtrlPtr, "pointer", []);
+ const sessionCtrlInstance = sessionCtrlInstanceFunc();
+
+ const getPersistentKeyAddr = androidappmusic.getExportByName("_ZN21SVFootHillSessionCtrl16getPersistentKeyERKNSt6__ndk112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEES8_S8_S8_S8_S8_S8_");
+ const getPersistentKey = new NativeFunction(getPersistentKeyAddr, "void", Array(9).fill("pointer"));
+
+ const decryptContextAddr = androidappmusic.getExportByName("_ZN21SVFootHillSessionCtrl14decryptContextERKNSt6__ndk112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEERKN11SVDecryptor15SVDecryptorTypeERKb");
+ const decryptContext = new NativeFunction(decryptContextAddr, "void", Array(3).fill("pointer"));
+
+ const NfcRKVnxuKZy04KWbdFu71Ou = androidappmusic.getExportByName("NfcRKVnxuKZy04KWbdFu71Ou");
+ const decryptSample = new NativeFunction(NfcRKVnxuKZy04KWbdFu71Ou, 'ulong', ['pointer', 'uint', 'pointer', 'pointer', 'size_t']);
+
+ const kdContextMap = new Map();
+
+ function getkdContext(adam, uri) {
+ const uriStr = String.fromCharCode(...new Uint8Array(uri))
+ if (kdContextMap.has(uriStr)) {
+ return kdContextMap.get(uriStr);
+ }
+
+ const defaultId = newStdStringFromBuffer(adam);
+ const keyUri = newStdStringFromBuffer(uri);
+ const keyFormat = newStdString("com.apple.streamingkeydelivery");
+ const keyFormatVer = newStdString("1");
+ const serverUri = newStdString("https://play.itunes.apple.com/WebObjects/MZPlay.woa/music/fps");
+ const protocolType = newStdString("simplified");
+ const fpsCert = newStdString(fairplayCert);
+ const persistentKey = Memory.alloc(Process.pointerSize * 2);
+ getPersistentKey(persistentKey, sessionCtrlInstance, defaultId.str, keyUri.str, keyFormat.str, keyFormatVer.str, serverUri.str, protocolType.str, fpsCert.str);
+
+ const ptr = persistentKey.readPointer();
+ if (ptr.isNull()) return null;
+
+ const svfootHillPKey = Memory.alloc(Process.pointerSize * 2);
+ decryptContext(svfootHillPKey, sessionCtrlInstance, ptr);
+
+ const ptr2 = svfootHillPKey.readPointer();
+ if (ptr2.isNull()) return null;
+
+ const ap = ptr2.add(0x18).readPointer();
+ if (!ap.isNull()) kdContextMap.set(uriStr, ap);
+ return ap;
+ }
+
+ async function handleConnection(s) {
+ // console.log("new connection!");
+ while (true) {
+ const adamSize = (await s.input.readAll(1)).unwrap().readU8();
+ if (adamSize === 0)
+ break;
+ const adam = await s.input.readAll(adamSize);
+ const uriSize = (await s.input.readAll(1)).unwrap().readU8();
+ const uri = await s.input.readAll(uriSize);
+ const kdContext = getkdContext(adam, uri);
+ // console.log(adam, uri, kdContext)
+ while (true) {
+ const size = (await s.input.readAll(4)).unwrap().readU32();
+ if (size === 0)
+ break;
+ const sample = await s.input.readAll(size);
+ decryptSample(kdContext.readPointer(), 5, sample.unwrap(), sample.unwrap(), sample.byteLength);
+ await s.output.writeAll(sample);
+ }
+ }
+ await s.close();
+ }
+
+ Socket.listen({
+ family: "ipv4",
+ port: port,
+ }).then(async function (listener) {
+ while (true) {
+ handleConnection(await listener.accept());
+ }
+ }).catch(console.log);
+}, 4000);
\ No newline at end of file
diff --git a/assets/storefront_ids.json b/assets/storefront_ids.json
new file mode 100644
index 0000000..217ed0f
--- /dev/null
+++ b/assets/storefront_ids.json
@@ -0,0 +1,652 @@
+[
+ {
+ "name": "Algeria",
+ "code": "DZ",
+ "storefrontId": 143563
+ },
+ {
+ "name": "Angola",
+ "code": "AO",
+ "storefrontId": 143564
+ },
+ {
+ "name": "Anguilla",
+ "code": "AI",
+ "storefrontId": 143538
+ },
+ {
+ "name": "Antigua & Barbuda",
+ "code": "AG",
+ "storefrontId": 143540
+ },
+ {
+ "name": "Argentina",
+ "code": "AR",
+ "storefrontId": 143505
+ },
+ {
+ "name": "Armenia",
+ "code": "AM",
+ "storefrontId": 143524
+ },
+ {
+ "name": "Australia",
+ "code": "AU",
+ "storefrontId": 143460
+ },
+ {
+ "name": "Austria",
+ "code": "AT",
+ "storefrontId": 143445
+ },
+ {
+ "name": "Azerbaijan",
+ "code": "AZ",
+ "storefrontId": 143568
+ },
+ {
+ "name": "Bahrain",
+ "code": "BH",
+ "storefrontId": 143559
+ },
+ {
+ "name": "Bangladesh",
+ "code": "BD",
+ "storefrontId": 143490
+ },
+ {
+ "name": "Barbados",
+ "code": "BB",
+ "storefrontId": 143541
+ },
+ {
+ "name": "Belarus",
+ "code": "BY",
+ "storefrontId": 143565
+ },
+ {
+ "name": "Belgium",
+ "code": "BE",
+ "storefrontId": 143446
+ },
+ {
+ "name": "Belize",
+ "code": "BZ",
+ "storefrontId": 143555
+ },
+ {
+ "name": "Bermuda",
+ "code": "BM",
+ "storefrontId": 143542
+ },
+ {
+ "name": "Bolivia",
+ "code": "BO",
+ "storefrontId": 143556
+ },
+ {
+ "name": "Botswana",
+ "code": "BW",
+ "storefrontId": 143525
+ },
+ {
+ "name": "Brazil",
+ "code": "BR",
+ "storefrontId": 143503
+ },
+ {
+ "name": "British Virgin Islands",
+ "code": "VG",
+ "storefrontId": 143543
+ },
+ {
+ "name": "Brunei",
+ "code": "BN",
+ "storefrontId": 143560
+ },
+ {
+ "name": "Bulgaria",
+ "code": "BG",
+ "storefrontId": 143526
+ },
+ {
+ "name": "Canada",
+ "code": "CA",
+ "storefrontId": 143455
+ },
+ {
+ "name": "Cayman Islands",
+ "code": "KY",
+ "storefrontId": 143544
+ },
+ {
+ "name": "Chile",
+ "code": "CL",
+ "storefrontId": 143483
+ },
+ {
+ "name": "China",
+ "code": "CN",
+ "storefrontId": 143465
+ },
+ {
+ "name": "Colombia",
+ "code": "CO",
+ "storefrontId": 143501
+ },
+ {
+ "name": "Costa Rica",
+ "code": "CR",
+ "storefrontId": 143495
+ },
+ {
+ "name": "Cote D’Ivoire",
+ "code": "CI",
+ "storefrontId": 143527
+ },
+ {
+ "name": "Croatia",
+ "code": "HR",
+ "storefrontId": 143494
+ },
+ {
+ "name": "Cyprus",
+ "code": "CY",
+ "storefrontId": 143557
+ },
+ {
+ "name": "Czech Republic",
+ "code": "CZ",
+ "storefrontId": 143489
+ },
+ {
+ "name": "Denmark",
+ "code": "DK",
+ "storefrontId": 143458
+ },
+ {
+ "name": "Dominica",
+ "code": "DM",
+ "storefrontId": 143545
+ },
+ {
+ "name": "Dominican Rep.",
+ "code": "DO",
+ "storefrontId": 143508
+ },
+ {
+ "name": "Ecuador",
+ "code": "EC",
+ "storefrontId": 143509
+ },
+ {
+ "name": "Egypt",
+ "code": "EG",
+ "storefrontId": 143516
+ },
+ {
+ "name": "El Salvador",
+ "code": "SV",
+ "storefrontId": 143506
+ },
+ {
+ "name": "Estonia",
+ "code": "EE",
+ "storefrontId": 143518
+ },
+ {
+ "name": "Finland",
+ "code": "FI",
+ "storefrontId": 143447
+ },
+ {
+ "name": "France",
+ "code": "FR",
+ "storefrontId": 143442
+ },
+ {
+ "name": "Germany",
+ "code": "DE",
+ "storefrontId": 143443
+ },
+ {
+ "name": "Ghana",
+ "code": "GH",
+ "storefrontId": 143573
+ },
+ {
+ "name": "Greece",
+ "code": "GR",
+ "storefrontId": 143448
+ },
+ {
+ "name": "Grenada",
+ "code": "GD",
+ "storefrontId": 143546
+ },
+ {
+ "name": "Guatemala",
+ "code": "GT",
+ "storefrontId": 143504
+ },
+ {
+ "name": "Guyana",
+ "code": "GY",
+ "storefrontId": 143553
+ },
+ {
+ "name": "Honduras",
+ "code": "HN",
+ "storefrontId": 143510
+ },
+ {
+ "name": "Hong Kong",
+ "code": "HK",
+ "storefrontId": 143463
+ },
+ {
+ "name": "Hungary",
+ "code": "HU",
+ "storefrontId": 143482
+ },
+ {
+ "name": "Iceland",
+ "code": "IS",
+ "storefrontId": 143558
+ },
+ {
+ "name": "India",
+ "code": "IN",
+ "storefrontId": 143467
+ },
+ {
+ "name": "Indonesia",
+ "code": "ID",
+ "storefrontId": 143476
+ },
+ {
+ "name": "Ireland",
+ "code": "IE",
+ "storefrontId": 143449
+ },
+ {
+ "name": "Israel",
+ "code": "IL",
+ "storefrontId": 143491
+ },
+ {
+ "name": "Italy",
+ "code": "IT",
+ "storefrontId": 143450
+ },
+ {
+ "name": "Jamaica",
+ "code": "JM",
+ "storefrontId": 143511
+ },
+ {
+ "name": "Japan",
+ "code": "JP",
+ "storefrontId": 143462
+ },
+ {
+ "name": "Jordan",
+ "code": "JO",
+ "storefrontId": 143528
+ },
+ {
+ "name": "Kazakstan",
+ "code": "KZ",
+ "storefrontId": 143517
+ },
+ {
+ "name": "Kenya",
+ "code": "KE",
+ "storefrontId": 143529
+ },
+ {
+ "name": "Korea, Republic Of",
+ "code": "KR",
+ "storefrontId": 143466
+ },
+ {
+ "name": "Kuwait",
+ "code": "KW",
+ "storefrontId": 143493
+ },
+ {
+ "name": "Latvia",
+ "code": "LV",
+ "storefrontId": 143519
+ },
+ {
+ "name": "Lebanon",
+ "code": "LB",
+ "storefrontId": 143497
+ },
+ {
+ "name": "Liechtenstein",
+ "code": "LI",
+ "storefrontId": 143522
+ },
+ {
+ "name": "Lithuania",
+ "code": "LT",
+ "storefrontId": 143520
+ },
+ {
+ "name": "Luxembourg",
+ "code": "LU",
+ "storefrontId": 143451
+ },
+ {
+ "name": "Macau",
+ "code": "MO",
+ "storefrontId": 143515
+ },
+ {
+ "name": "Macedonia",
+ "code": "MK",
+ "storefrontId": 143530
+ },
+ {
+ "name": "Madagascar",
+ "code": "MG",
+ "storefrontId": 143531
+ },
+ {
+ "name": "Malaysia",
+ "code": "MY",
+ "storefrontId": 143473
+ },
+ {
+ "name": "Maldives",
+ "code": "MV",
+ "storefrontId": 143488
+ },
+ {
+ "name": "Mali",
+ "code": "ML",
+ "storefrontId": 143532
+ },
+ {
+ "name": "Malta",
+ "code": "MT",
+ "storefrontId": 143521
+ },
+ {
+ "name": "Mauritius",
+ "code": "MU",
+ "storefrontId": 143533
+ },
+ {
+ "name": "Mexico",
+ "code": "MX",
+ "storefrontId": 143468
+ },
+ {
+ "name": "Moldova, Republic Of",
+ "code": "MD",
+ "storefrontId": 143523
+ },
+ {
+ "name": "Montserrat",
+ "code": "MS",
+ "storefrontId": 143547
+ },
+ {
+ "name": "Nepal",
+ "code": "NP",
+ "storefrontId": 143484
+ },
+ {
+ "name": "Netherlands",
+ "code": "NL",
+ "storefrontId": 143452
+ },
+ {
+ "name": "New Zealand",
+ "code": "NZ",
+ "storefrontId": 143461
+ },
+ {
+ "name": "Nicaragua",
+ "code": "NI",
+ "storefrontId": 143512
+ },
+ {
+ "name": "Niger",
+ "code": "NE",
+ "storefrontId": 143534
+ },
+ {
+ "name": "Nigeria",
+ "code": "NG",
+ "storefrontId": 143561
+ },
+ {
+ "name": "Norway",
+ "code": "NO",
+ "storefrontId": 143457
+ },
+ {
+ "name": "Oman",
+ "code": "OM",
+ "storefrontId": 143562
+ },
+ {
+ "name": "Pakistan",
+ "code": "PK",
+ "storefrontId": 143477
+ },
+ {
+ "name": "Panama",
+ "code": "PA",
+ "storefrontId": 143485
+ },
+ {
+ "name": "Paraguay",
+ "code": "PY",
+ "storefrontId": 143513
+ },
+ {
+ "name": "Peru",
+ "code": "PE",
+ "storefrontId": 143507
+ },
+ {
+ "name": "Philippines",
+ "code": "PH",
+ "storefrontId": 143474
+ },
+ {
+ "name": "Poland",
+ "code": "PL",
+ "storefrontId": 143478
+ },
+ {
+ "name": "Portugal",
+ "code": "PT",
+ "storefrontId": 143453
+ },
+ {
+ "name": "Qatar",
+ "code": "QA",
+ "storefrontId": 143498
+ },
+ {
+ "name": "Romania",
+ "code": "RO",
+ "storefrontId": 143487
+ },
+ {
+ "name": "Russia",
+ "code": "RU",
+ "storefrontId": 143469
+ },
+ {
+ "name": "Saudi Arabia",
+ "code": "SA",
+ "storefrontId": 143479
+ },
+ {
+ "name": "Senegal",
+ "code": "SN",
+ "storefrontId": 143535
+ },
+ {
+ "name": "Serbia",
+ "code": "RS",
+ "storefrontId": 143500
+ },
+ {
+ "name": "Singapore",
+ "code": "SG",
+ "storefrontId": 143464
+ },
+ {
+ "name": "Slovakia",
+ "code": "SK",
+ "storefrontId": 143496
+ },
+ {
+ "name": "Slovenia",
+ "code": "SI",
+ "storefrontId": 143499
+ },
+ {
+ "name": "South Africa",
+ "code": "ZA",
+ "storefrontId": 143472
+ },
+ {
+ "name": "Spain",
+ "code": "ES",
+ "storefrontId": 143454
+ },
+ {
+ "name": "Sri Lanka",
+ "code": "LK",
+ "storefrontId": 143486
+ },
+ {
+ "name": "St. Kitts & Nevis",
+ "code": "KN",
+ "storefrontId": 143548
+ },
+ {
+ "name": "St. Lucia",
+ "code": "LC",
+ "storefrontId": 143549
+ },
+ {
+ "name": "St. Vincent & The Grenadines",
+ "code": "VC",
+ "storefrontId": 143550
+ },
+ {
+ "name": "Suriname",
+ "code": "SR",
+ "storefrontId": 143554
+ },
+ {
+ "name": "Sweden",
+ "code": "SE",
+ "storefrontId": 143456
+ },
+ {
+ "name": "Switzerland",
+ "code": "CH",
+ "storefrontId": 143459
+ },
+ {
+ "name": "Taiwan",
+ "code": "TW",
+ "storefrontId": 143470
+ },
+ {
+ "name": "Tanzania",
+ "code": "TZ",
+ "storefrontId": 143572
+ },
+ {
+ "name": "Thailand",
+ "code": "TH",
+ "storefrontId": 143475
+ },
+ {
+ "name": "The Bahamas",
+ "code": "BS",
+ "storefrontId": 143539
+ },
+ {
+ "name": "Trinidad & Tobago",
+ "code": "TT",
+ "storefrontId": 143551
+ },
+ {
+ "name": "Tunisia",
+ "code": "TN",
+ "storefrontId": 143536
+ },
+ {
+ "name": "Turkey",
+ "code": "TR",
+ "storefrontId": 143480
+ },
+ {
+ "name": "Turks & Caicos",
+ "code": "TC",
+ "storefrontId": 143552
+ },
+ {
+ "name": "Uganda",
+ "code": "UG",
+ "storefrontId": 143537
+ },
+ {
+ "name": "UK",
+ "code": "GB",
+ "storefrontId": 143444
+ },
+ {
+ "name": "Ukraine",
+ "code": "UA",
+ "storefrontId": 143492
+ },
+ {
+ "name": "United Arab Emirates",
+ "code": "AE",
+ "storefrontId": 143481
+ },
+ {
+ "name": "Uruguay",
+ "code": "UY",
+ "storefrontId": 143514
+ },
+ {
+ "name": "USA",
+ "code": "US",
+ "storefrontId": 143441
+ },
+ {
+ "name": "Uzbekistan",
+ "code": "UZ",
+ "storefrontId": 143566
+ },
+ {
+ "name": "Venezuela",
+ "code": "VE",
+ "storefrontId": 143502
+ },
+ {
+ "name": "Vietnam",
+ "code": "VN",
+ "storefrontId": 143471
+ },
+ {
+ "name": "Yemen",
+ "code": "YE",
+ "storefrontId": 143571
+ }
+]
\ No newline at end of file
diff --git a/config.toml b/config.toml
new file mode 100644
index 0000000..28bb997
--- /dev/null
+++ b/config.toml
@@ -0,0 +1,24 @@
+[language]
+language = "zh-CN"
+languageForGenre = "en_US"
+
+[[devices]]
+host = "127.0.0.1"
+port = 58526
+agentPort = 10020
+fridaPath = "/system/bin/frida-server"
+suMethod = "su -c"
+
+[download]
+atmosConventToM4a = false
+songNameFormat = "{disk}-{tracknum:02d} {title}"
+dirPathFormat = "downloads/{artist}/{album}"
+saveLyrics = true
+saveCover = true
+coverFormat = "jpg"
+afterDownloaded = ""
+
+[metadata]
+embedMetadata = ["title", "artist", "album", "album_artist", "composer",
+"genre", "created", "track", "tracknum", "disk", "lyrics", "cover", "copyright",
+"record_company", "upc", "isrc"]
\ No newline at end of file
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..11e322f
--- /dev/null
+++ b/main.py
@@ -0,0 +1,12 @@
+import asyncio
+
+from src.cmd import NewInteractiveShell
+
+
+if __name__ == '__main__':
+ loop = asyncio.get_event_loop()
+ cmd = NewInteractiveShell(loop)
+ try:
+ loop.run_until_complete(cmd.start())
+ except KeyboardInterrupt:
+ loop.stop()
\ No newline at end of file
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000..35a1826
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,718 @@
+# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
+
+[[package]]
+name = "annotated-types"
+version = "0.6.0"
+description = "Reusable constraint types to use with typing.Annotated"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"},
+ {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"},
+]
+
+[[package]]
+name = "anyio"
+version = "4.3.0"
+description = "High level compatibility layer for multiple asynchronous event loop implementations"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"},
+ {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"},
+]
+
+[package.dependencies]
+idna = ">=2.8"
+sniffio = ">=1.1"
+
+[package.extras]
+doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
+test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
+trio = ["trio (>=0.23)"]
+
+[[package]]
+name = "beautifulsoup4"
+version = "4.12.3"
+description = "Screen-scraping library"
+optional = false
+python-versions = ">=3.6.0"
+files = [
+ {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"},
+ {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"},
+]
+
+[package.dependencies]
+soupsieve = ">1.2"
+
+[package.extras]
+cchardet = ["cchardet"]
+chardet = ["chardet"]
+charset-normalizer = ["charset-normalizer"]
+html5lib = ["html5lib"]
+lxml = ["lxml"]
+
+[[package]]
+name = "certifi"
+version = "2024.2.2"
+description = "Python package for providing Mozilla's CA Bundle."
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"},
+ {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"},
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+description = "Cross-platform colored terminal text."
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+files = [
+ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
+
+[[package]]
+name = "frida"
+version = "16.2.1"
+description = "Dynamic instrumentation toolkit for developers, reverse-engineers, and security researchers"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "frida-16.2.1-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:df1cc56a494aa045bf3b48a6609658b992801b41b929e9013c7319b8301c6450"},
+ {file = "frida-16.2.1-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:2bf69733d56d6d15260f94a4b68551d758c77e3dd36ad5a71df18ba9852e44ce"},
+ {file = "frida-16.2.1-cp37-abi3-manylinux_2_17_aarch64.whl", hash = "sha256:4b4db71b9317086ad188f91de8b00aceb9fd8a4d89c353bd06587eaf6bb410a2"},
+ {file = "frida-16.2.1-cp37-abi3-manylinux_2_17_armv7l.whl", hash = "sha256:e15d612767493b29795522ee3e763b2dfcf8610dab8a1e337b936adc55991c55"},
+ {file = "frida-16.2.1-cp37-abi3-manylinux_2_5_i686.whl", hash = "sha256:51ca64ffd29c6df70429e2d96a6651f3d0a752223f55a0187181370391a28cba"},
+ {file = "frida-16.2.1-cp37-abi3-manylinux_2_5_x86_64.whl", hash = "sha256:2b549a18bfd09e5b67168bc890cfdc53b3ca01eb9fab99b0d06c3ffc530788ec"},
+ {file = "frida-16.2.1-cp37-abi3-win32.whl", hash = "sha256:ee3e63fa16bf494f840bcacaa955a6a2e793a25f84dc3ea5bd92885f8526aa7a"},
+ {file = "frida-16.2.1-cp37-abi3-win_amd64.whl", hash = "sha256:0363340ab678b75045426529b4a7061b32f8095c01ae0e196f8764e9ee404e26"},
+ {file = "frida-16.2.1.tar.gz", hash = "sha256:64a011825ea21a5ed3e3d7589f04c1dec473e1a083beb4c57895dddf32caa7c9"},
+]
+
+[[package]]
+name = "frida-tools"
+version = "12.3.0"
+description = "Frida CLI tools"
+optional = false
+python-versions = "*"
+files = [
+ {file = "frida-tools-12.3.0.tar.gz", hash = "sha256:8edc67d1ae3792ff5b2dc63508cde4d247f92b7d0d7bf153d74a21a6d58dc045"},
+]
+
+[package.dependencies]
+colorama = ">=0.2.7,<1.0.0"
+frida = ">=16.0.9,<17.0.0"
+prompt-toolkit = ">=2.0.0,<4.0.0"
+pygments = ">=2.0.2,<3.0.0"
+
+[[package]]
+name = "h11"
+version = "0.14.0"
+description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
+ {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.5"
+description = "A minimal low-level HTTP client."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"},
+ {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"},
+]
+
+[package.dependencies]
+certifi = "*"
+h11 = ">=0.13,<0.15"
+
+[package.extras]
+asyncio = ["anyio (>=4.0,<5.0)"]
+http2 = ["h2 (>=3,<5)"]
+socks = ["socksio (==1.*)"]
+trio = ["trio (>=0.22.0,<0.26.0)"]
+
+[[package]]
+name = "httpx"
+version = "0.27.0"
+description = "The next generation HTTP client."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"},
+ {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"},
+]
+
+[package.dependencies]
+anyio = "*"
+certifi = "*"
+httpcore = "==1.*"
+idna = "*"
+sniffio = "*"
+
+[package.extras]
+brotli = ["brotli", "brotlicffi"]
+cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
+http2 = ["h2 (>=3,<5)"]
+socks = ["socksio (==1.*)"]
+
+[[package]]
+name = "idna"
+version = "3.7"
+description = "Internationalized Domain Names in Applications (IDNA)"
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
+ {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
+]
+
+[[package]]
+name = "loguru"
+version = "0.7.2"
+description = "Python logging made (stupidly) simple"
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb"},
+ {file = "loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"},
+]
+
+[package.dependencies]
+colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""}
+win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""}
+
+[package.extras]
+dev = ["Sphinx (==7.2.5)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.2.2)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.4.1)", "mypy (==v1.5.1)", "pre-commit (==3.4.0)", "pytest (==6.1.2)", "pytest (==7.4.0)", "pytest-cov (==2.12.1)", "pytest-cov (==4.1.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.0.0)", "sphinx-autobuild (==2021.3.14)", "sphinx-rtd-theme (==1.3.0)", "tox (==3.27.1)", "tox (==4.11.0)"]
+
+[[package]]
+name = "lxml"
+version = "5.2.1"
+description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "lxml-5.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1f7785f4f789fdb522729ae465adcaa099e2a3441519df750ebdccc481d961a1"},
+ {file = "lxml-5.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6cc6ee342fb7fa2471bd9b6d6fdfc78925a697bf5c2bcd0a302e98b0d35bfad3"},
+ {file = "lxml-5.2.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:794f04eec78f1d0e35d9e0c36cbbb22e42d370dda1609fb03bcd7aeb458c6377"},
+ {file = "lxml-5.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817d420c60a5183953c783b0547d9eb43b7b344a2c46f69513d5952a78cddf3"},
+ {file = "lxml-5.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2213afee476546a7f37c7a9b4ad4d74b1e112a6fafffc9185d6d21f043128c81"},
+ {file = "lxml-5.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b070bbe8d3f0f6147689bed981d19bbb33070225373338df755a46893528104a"},
+ {file = "lxml-5.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e02c5175f63effbd7c5e590399c118d5db6183bbfe8e0d118bdb5c2d1b48d937"},
+ {file = "lxml-5.2.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:3dc773b2861b37b41a6136e0b72a1a44689a9c4c101e0cddb6b854016acc0aa8"},
+ {file = "lxml-5.2.1-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:d7520db34088c96cc0e0a3ad51a4fd5b401f279ee112aa2b7f8f976d8582606d"},
+ {file = "lxml-5.2.1-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:bcbf4af004f98793a95355980764b3d80d47117678118a44a80b721c9913436a"},
+ {file = "lxml-5.2.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a2b44bec7adf3e9305ce6cbfa47a4395667e744097faed97abb4728748ba7d47"},
+ {file = "lxml-5.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1c5bb205e9212d0ebddf946bc07e73fa245c864a5f90f341d11ce7b0b854475d"},
+ {file = "lxml-5.2.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2c9d147f754b1b0e723e6afb7ba1566ecb162fe4ea657f53d2139bbf894d050a"},
+ {file = "lxml-5.2.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:3545039fa4779be2df51d6395e91a810f57122290864918b172d5dc7ca5bb433"},
+ {file = "lxml-5.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a91481dbcddf1736c98a80b122afa0f7296eeb80b72344d7f45dc9f781551f56"},
+ {file = "lxml-5.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2ddfe41ddc81f29a4c44c8ce239eda5ade4e7fc305fb7311759dd6229a080052"},
+ {file = "lxml-5.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a7baf9ffc238e4bf401299f50e971a45bfcc10a785522541a6e3179c83eabf0a"},
+ {file = "lxml-5.2.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:31e9a882013c2f6bd2f2c974241bf4ba68c85eba943648ce88936d23209a2e01"},
+ {file = "lxml-5.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0a15438253b34e6362b2dc41475e7f80de76320f335e70c5528b7148cac253a1"},
+ {file = "lxml-5.2.1-cp310-cp310-win32.whl", hash = "sha256:6992030d43b916407c9aa52e9673612ff39a575523c5f4cf72cdef75365709a5"},
+ {file = "lxml-5.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:da052e7962ea2d5e5ef5bc0355d55007407087392cf465b7ad84ce5f3e25fe0f"},
+ {file = "lxml-5.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:70ac664a48aa64e5e635ae5566f5227f2ab7f66a3990d67566d9907edcbbf867"},
+ {file = "lxml-5.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1ae67b4e737cddc96c99461d2f75d218bdf7a0c3d3ad5604d1f5e7464a2f9ffe"},
+ {file = "lxml-5.2.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f18a5a84e16886898e51ab4b1d43acb3083c39b14c8caeb3589aabff0ee0b270"},
+ {file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6f2c8372b98208ce609c9e1d707f6918cc118fea4e2c754c9f0812c04ca116d"},
+ {file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:394ed3924d7a01b5bd9a0d9d946136e1c2f7b3dc337196d99e61740ed4bc6fe1"},
+ {file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d077bc40a1fe984e1a9931e801e42959a1e6598edc8a3223b061d30fbd26bbc"},
+ {file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:764b521b75701f60683500d8621841bec41a65eb739b8466000c6fdbc256c240"},
+ {file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:3a6b45da02336895da82b9d472cd274b22dc27a5cea1d4b793874eead23dd14f"},
+ {file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:5ea7b6766ac2dfe4bcac8b8595107665a18ef01f8c8343f00710b85096d1b53a"},
+ {file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:e196a4ff48310ba62e53a8e0f97ca2bca83cdd2fe2934d8b5cb0df0a841b193a"},
+ {file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:200e63525948e325d6a13a76ba2911f927ad399ef64f57898cf7c74e69b71095"},
+ {file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dae0ed02f6b075426accbf6b2863c3d0a7eacc1b41fb40f2251d931e50188dad"},
+ {file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:ab31a88a651039a07a3ae327d68ebdd8bc589b16938c09ef3f32a4b809dc96ef"},
+ {file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:df2e6f546c4df14bc81f9498bbc007fbb87669f1bb707c6138878c46b06f6510"},
+ {file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5dd1537e7cc06efd81371f5d1a992bd5ab156b2b4f88834ca852de4a8ea523fa"},
+ {file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9b9ec9c9978b708d488bec36b9e4c94d88fd12ccac3e62134a9d17ddba910ea9"},
+ {file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8e77c69d5892cb5ba71703c4057091e31ccf534bd7f129307a4d084d90d014b8"},
+ {file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a8d5c70e04aac1eda5c829a26d1f75c6e5286c74743133d9f742cda8e53b9c2f"},
+ {file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c94e75445b00319c1fad60f3c98b09cd63fe1134a8a953dcd48989ef42318534"},
+ {file = "lxml-5.2.1-cp311-cp311-win32.whl", hash = "sha256:4951e4f7a5680a2db62f7f4ab2f84617674d36d2d76a729b9a8be4b59b3659be"},
+ {file = "lxml-5.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:5c670c0406bdc845b474b680b9a5456c561c65cf366f8db5a60154088c92d102"},
+ {file = "lxml-5.2.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:abc25c3cab9ec7fcd299b9bcb3b8d4a1231877e425c650fa1c7576c5107ab851"},
+ {file = "lxml-5.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6935bbf153f9a965f1e07c2649c0849d29832487c52bb4a5c5066031d8b44fd5"},
+ {file = "lxml-5.2.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d793bebb202a6000390a5390078e945bbb49855c29c7e4d56a85901326c3b5d9"},
+ {file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd5562927cdef7c4f5550374acbc117fd4ecc05b5007bdfa57cc5355864e0a4"},
+ {file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0e7259016bc4345a31af861fdce942b77c99049d6c2107ca07dc2bba2435c1d9"},
+ {file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:530e7c04f72002d2f334d5257c8a51bf409db0316feee7c87e4385043be136af"},
+ {file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59689a75ba8d7ffca577aefd017d08d659d86ad4585ccc73e43edbfc7476781a"},
+ {file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f9737bf36262046213a28e789cc82d82c6ef19c85a0cf05e75c670a33342ac2c"},
+ {file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:3a74c4f27167cb95c1d4af1c0b59e88b7f3e0182138db2501c353555f7ec57f4"},
+ {file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:68a2610dbe138fa8c5826b3f6d98a7cfc29707b850ddcc3e21910a6fe51f6ca0"},
+ {file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f0a1bc63a465b6d72569a9bba9f2ef0334c4e03958e043da1920299100bc7c08"},
+ {file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c2d35a1d047efd68027817b32ab1586c1169e60ca02c65d428ae815b593e65d4"},
+ {file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:79bd05260359170f78b181b59ce871673ed01ba048deef4bf49a36ab3e72e80b"},
+ {file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:865bad62df277c04beed9478fe665b9ef63eb28fe026d5dedcb89b537d2e2ea6"},
+ {file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:44f6c7caff88d988db017b9b0e4ab04934f11e3e72d478031efc7edcac6c622f"},
+ {file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:71e97313406ccf55d32cc98a533ee05c61e15d11b99215b237346171c179c0b0"},
+ {file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:057cdc6b86ab732cf361f8b4d8af87cf195a1f6dc5b0ff3de2dced242c2015e0"},
+ {file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f3bbbc998d42f8e561f347e798b85513ba4da324c2b3f9b7969e9c45b10f6169"},
+ {file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:491755202eb21a5e350dae00c6d9a17247769c64dcf62d8c788b5c135e179dc4"},
+ {file = "lxml-5.2.1-cp312-cp312-win32.whl", hash = "sha256:8de8f9d6caa7f25b204fc861718815d41cbcf27ee8f028c89c882a0cf4ae4134"},
+ {file = "lxml-5.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:f2a9efc53d5b714b8df2b4b3e992accf8ce5bbdfe544d74d5c6766c9e1146a3a"},
+ {file = "lxml-5.2.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:70a9768e1b9d79edca17890175ba915654ee1725975d69ab64813dd785a2bd5c"},
+ {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c38d7b9a690b090de999835f0443d8aa93ce5f2064035dfc48f27f02b4afc3d0"},
+ {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5670fb70a828663cc37552a2a85bf2ac38475572b0e9b91283dc09efb52c41d1"},
+ {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:958244ad566c3ffc385f47dddde4145088a0ab893504b54b52c041987a8c1863"},
+ {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6241d4eee5f89453307c2f2bfa03b50362052ca0af1efecf9fef9a41a22bb4f"},
+ {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2a66bf12fbd4666dd023b6f51223aed3d9f3b40fef06ce404cb75bafd3d89536"},
+ {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:9123716666e25b7b71c4e1789ec829ed18663152008b58544d95b008ed9e21e9"},
+ {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:0c3f67e2aeda739d1cc0b1102c9a9129f7dc83901226cc24dd72ba275ced4218"},
+ {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:5d5792e9b3fb8d16a19f46aa8208987cfeafe082363ee2745ea8b643d9cc5b45"},
+ {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:88e22fc0a6684337d25c994381ed8a1580a6f5ebebd5ad41f89f663ff4ec2885"},
+ {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:21c2e6b09565ba5b45ae161b438e033a86ad1736b8c838c766146eff8ceffff9"},
+ {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_2_s390x.whl", hash = "sha256:afbbdb120d1e78d2ba8064a68058001b871154cc57787031b645c9142b937a62"},
+ {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:627402ad8dea044dde2eccde4370560a2b750ef894c9578e1d4f8ffd54000461"},
+ {file = "lxml-5.2.1-cp36-cp36m-win32.whl", hash = "sha256:e89580a581bf478d8dcb97d9cd011d567768e8bc4095f8557b21c4d4c5fea7d0"},
+ {file = "lxml-5.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:59565f10607c244bc4c05c0c5fa0c190c990996e0c719d05deec7030c2aa8289"},
+ {file = "lxml-5.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:857500f88b17a6479202ff5fe5f580fc3404922cd02ab3716197adf1ef628029"},
+ {file = "lxml-5.2.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56c22432809085b3f3ae04e6e7bdd36883d7258fcd90e53ba7b2e463efc7a6af"},
+ {file = "lxml-5.2.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a55ee573116ba208932e2d1a037cc4b10d2c1cb264ced2184d00b18ce585b2c0"},
+ {file = "lxml-5.2.1-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:6cf58416653c5901e12624e4013708b6e11142956e7f35e7a83f1ab02f3fe456"},
+ {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:64c2baa7774bc22dd4474248ba16fe1a7f611c13ac6123408694d4cc93d66dbd"},
+ {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:74b28c6334cca4dd704e8004cba1955af0b778cf449142e581e404bd211fb619"},
+ {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:7221d49259aa1e5a8f00d3d28b1e0b76031655ca74bb287123ef56c3db92f213"},
+ {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3dbe858ee582cbb2c6294dc85f55b5f19c918c2597855e950f34b660f1a5ede6"},
+ {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:04ab5415bf6c86e0518d57240a96c4d1fcfc3cb370bb2ac2a732b67f579e5a04"},
+ {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:6ab833e4735a7e5533711a6ea2df26459b96f9eec36d23f74cafe03631647c41"},
+ {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f443cdef978430887ed55112b491f670bba6462cea7a7742ff8f14b7abb98d75"},
+ {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:9e2addd2d1866fe112bc6f80117bcc6bc25191c5ed1bfbcf9f1386a884252ae8"},
+ {file = "lxml-5.2.1-cp37-cp37m-win32.whl", hash = "sha256:f51969bac61441fd31f028d7b3b45962f3ecebf691a510495e5d2cd8c8092dbd"},
+ {file = "lxml-5.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:b0b58fbfa1bf7367dde8a557994e3b1637294be6cf2169810375caf8571a085c"},
+ {file = "lxml-5.2.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3e183c6e3298a2ed5af9d7a356ea823bccaab4ec2349dc9ed83999fd289d14d5"},
+ {file = "lxml-5.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:804f74efe22b6a227306dd890eecc4f8c59ff25ca35f1f14e7482bbce96ef10b"},
+ {file = "lxml-5.2.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:08802f0c56ed150cc6885ae0788a321b73505d2263ee56dad84d200cab11c07a"},
+ {file = "lxml-5.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f8c09ed18ecb4ebf23e02b8e7a22a05d6411911e6fabef3a36e4f371f4f2585"},
+ {file = "lxml-5.2.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3d30321949861404323c50aebeb1943461a67cd51d4200ab02babc58bd06a86"},
+ {file = "lxml-5.2.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:b560e3aa4b1d49e0e6c847d72665384db35b2f5d45f8e6a5c0072e0283430533"},
+ {file = "lxml-5.2.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:058a1308914f20784c9f4674036527e7c04f7be6fb60f5d61353545aa7fcb739"},
+ {file = "lxml-5.2.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:adfb84ca6b87e06bc6b146dc7da7623395db1e31621c4785ad0658c5028b37d7"},
+ {file = "lxml-5.2.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:417d14450f06d51f363e41cace6488519038f940676ce9664b34ebf5653433a5"},
+ {file = "lxml-5.2.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a2dfe7e2473f9b59496247aad6e23b405ddf2e12ef0765677b0081c02d6c2c0b"},
+ {file = "lxml-5.2.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bf2e2458345d9bffb0d9ec16557d8858c9c88d2d11fed53998512504cd9df49b"},
+ {file = "lxml-5.2.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:58278b29cb89f3e43ff3e0c756abbd1518f3ee6adad9e35b51fb101c1c1daaec"},
+ {file = "lxml-5.2.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:64641a6068a16201366476731301441ce93457eb8452056f570133a6ceb15fca"},
+ {file = "lxml-5.2.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:78bfa756eab503673991bdcf464917ef7845a964903d3302c5f68417ecdc948c"},
+ {file = "lxml-5.2.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:11a04306fcba10cd9637e669fd73aa274c1c09ca64af79c041aa820ea992b637"},
+ {file = "lxml-5.2.1-cp38-cp38-win32.whl", hash = "sha256:66bc5eb8a323ed9894f8fa0ee6cb3e3fb2403d99aee635078fd19a8bc7a5a5da"},
+ {file = "lxml-5.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:9676bfc686fa6a3fa10cd4ae6b76cae8be26eb5ec6811d2a325636c460da1806"},
+ {file = "lxml-5.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cf22b41fdae514ee2f1691b6c3cdeae666d8b7fa9434de445f12bbeee0cf48dd"},
+ {file = "lxml-5.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ec42088248c596dbd61d4ae8a5b004f97a4d91a9fd286f632e42e60b706718d7"},
+ {file = "lxml-5.2.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd53553ddad4a9c2f1f022756ae64abe16da1feb497edf4d9f87f99ec7cf86bd"},
+ {file = "lxml-5.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feaa45c0eae424d3e90d78823f3828e7dc42a42f21ed420db98da2c4ecf0a2cb"},
+ {file = "lxml-5.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddc678fb4c7e30cf830a2b5a8d869538bc55b28d6c68544d09c7d0d8f17694dc"},
+ {file = "lxml-5.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:853e074d4931dbcba7480d4dcab23d5c56bd9607f92825ab80ee2bd916edea53"},
+ {file = "lxml-5.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc4691d60512798304acb9207987e7b2b7c44627ea88b9d77489bbe3e6cc3bd4"},
+ {file = "lxml-5.2.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:beb72935a941965c52990f3a32d7f07ce869fe21c6af8b34bf6a277b33a345d3"},
+ {file = "lxml-5.2.1-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:6588c459c5627fefa30139be4d2e28a2c2a1d0d1c265aad2ba1935a7863a4913"},
+ {file = "lxml-5.2.1-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:588008b8497667f1ddca7c99f2f85ce8511f8f7871b4a06ceede68ab62dff64b"},
+ {file = "lxml-5.2.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b6787b643356111dfd4032b5bffe26d2f8331556ecb79e15dacb9275da02866e"},
+ {file = "lxml-5.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7c17b64b0a6ef4e5affae6a3724010a7a66bda48a62cfe0674dabd46642e8b54"},
+ {file = "lxml-5.2.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:27aa20d45c2e0b8cd05da6d4759649170e8dfc4f4e5ef33a34d06f2d79075d57"},
+ {file = "lxml-5.2.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:d4f2cc7060dc3646632d7f15fe68e2fa98f58e35dd5666cd525f3b35d3fed7f8"},
+ {file = "lxml-5.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff46d772d5f6f73564979cd77a4fffe55c916a05f3cb70e7c9c0590059fb29ef"},
+ {file = "lxml-5.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:96323338e6c14e958d775700ec8a88346014a85e5de73ac7967db0367582049b"},
+ {file = "lxml-5.2.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:52421b41ac99e9d91934e4d0d0fe7da9f02bfa7536bb4431b4c05c906c8c6919"},
+ {file = "lxml-5.2.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:7a7efd5b6d3e30d81ec68ab8a88252d7c7c6f13aaa875009fe3097eb4e30b84c"},
+ {file = "lxml-5.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0ed777c1e8c99b63037b91f9d73a6aad20fd035d77ac84afcc205225f8f41188"},
+ {file = "lxml-5.2.1-cp39-cp39-win32.whl", hash = "sha256:644df54d729ef810dcd0f7732e50e5ad1bd0a135278ed8d6bcb06f33b6b6f708"},
+ {file = "lxml-5.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:9ca66b8e90daca431b7ca1408cae085d025326570e57749695d6a01454790e95"},
+ {file = "lxml-5.2.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9b0ff53900566bc6325ecde9181d89afadc59c5ffa39bddf084aaedfe3b06a11"},
+ {file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd6037392f2d57793ab98d9e26798f44b8b4da2f2464388588f48ac52c489ea1"},
+ {file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b9c07e7a45bb64e21df4b6aa623cb8ba214dfb47d2027d90eac197329bb5e94"},
+ {file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3249cc2989d9090eeac5467e50e9ec2d40704fea9ab72f36b034ea34ee65ca98"},
+ {file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f42038016852ae51b4088b2862126535cc4fc85802bfe30dea3500fdfaf1864e"},
+ {file = "lxml-5.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:533658f8fbf056b70e434dff7e7aa611bcacb33e01f75de7f821810e48d1bb66"},
+ {file = "lxml-5.2.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:622020d4521e22fb371e15f580d153134bfb68d6a429d1342a25f051ec72df1c"},
+ {file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efa7b51824aa0ee957ccd5a741c73e6851de55f40d807f08069eb4c5a26b2baa"},
+ {file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c6ad0fbf105f6bcc9300c00010a2ffa44ea6f555df1a2ad95c88f5656104817"},
+ {file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e233db59c8f76630c512ab4a4daf5a5986da5c3d5b44b8e9fc742f2a24dbd460"},
+ {file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6a014510830df1475176466b6087fc0c08b47a36714823e58d8b8d7709132a96"},
+ {file = "lxml-5.2.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:d38c8f50ecf57f0463399569aa388b232cf1a2ffb8f0a9a5412d0db57e054860"},
+ {file = "lxml-5.2.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5aea8212fb823e006b995c4dda533edcf98a893d941f173f6c9506126188860d"},
+ {file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff097ae562e637409b429a7ac958a20aab237a0378c42dabaa1e3abf2f896e5f"},
+ {file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f5d65c39f16717a47c36c756af0fb36144069c4718824b7533f803ecdf91138"},
+ {file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3d0c3dd24bb4605439bf91068598d00c6370684f8de4a67c2992683f6c309d6b"},
+ {file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e32be23d538753a8adb6c85bd539f5fd3b15cb987404327c569dfc5fd8366e85"},
+ {file = "lxml-5.2.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cc518cea79fd1e2f6c90baafa28906d4309d24f3a63e801d855e7424c5b34144"},
+ {file = "lxml-5.2.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a0af35bd8ebf84888373630f73f24e86bf016642fb8576fba49d3d6b560b7cbc"},
+ {file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8aca2e3a72f37bfc7b14ba96d4056244001ddcc18382bd0daa087fd2e68a354"},
+ {file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ca1e8188b26a819387b29c3895c47a5e618708fe6f787f3b1a471de2c4a94d9"},
+ {file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c8ba129e6d3b0136a0f50345b2cb3db53f6bda5dd8c7f5d83fbccba97fb5dcb5"},
+ {file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e998e304036198b4f6914e6a1e2b6f925208a20e2042563d9734881150c6c246"},
+ {file = "lxml-5.2.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d3be9b2076112e51b323bdf6d5a7f8a798de55fb8d95fcb64bd179460cdc0704"},
+ {file = "lxml-5.2.1.tar.gz", hash = "sha256:3f7765e69bbce0906a7c74d5fe46d2c7a7596147318dbc08e4a2431f3060e306"},
+]
+
+[package.extras]
+cssselect = ["cssselect (>=0.7)"]
+html-clean = ["lxml-html-clean"]
+html5 = ["html5lib"]
+htmlsoup = ["BeautifulSoup4"]
+source = ["Cython (>=3.0.10)"]
+
+[[package]]
+name = "m3u8"
+version = "4.1.0"
+description = "Python m3u8 parser"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "m3u8-4.1.0-py3-none-any.whl", hash = "sha256:981daed09f57b7590721b6437278e49f2c36c1bceaa8fbe48f585e1745571d17"},
+ {file = "m3u8-4.1.0.tar.gz", hash = "sha256:3b9d7e5bafbaae89f2464cb16f397887d8decf6b1b48d8de58711414dc1c7b45"},
+]
+
+[[package]]
+name = "prompt-toolkit"
+version = "3.0.43"
+description = "Library for building powerful interactive command lines in Python"
+optional = false
+python-versions = ">=3.7.0"
+files = [
+ {file = "prompt_toolkit-3.0.43-py3-none-any.whl", hash = "sha256:a11a29cb3bf0a28a387fe5122cdb649816a957cd9261dcedf8c9f1fef33eacf6"},
+ {file = "prompt_toolkit-3.0.43.tar.gz", hash = "sha256:3527b7af26106cbc65a040bcc84839a3566ec1b051bb0bfe953631e704b0ff7d"},
+]
+
+[package.dependencies]
+wcwidth = "*"
+
+[[package]]
+name = "pure-python-adb"
+version = "0.3.0.dev0"
+description = "Pure python implementation of the adb client"
+optional = false
+python-versions = "*"
+files = [
+ {file = "pure-python-adb-0.3.0.dev0.tar.gz", hash = "sha256:0ecc89d780160cfe03260ba26df2c471a05263b2cad0318363573ee8043fb94d"},
+]
+
+[package.extras]
+async = ["aiofiles (>=0.4.0)"]
+
+[[package]]
+name = "pydantic"
+version = "2.7.1"
+description = "Data validation using Python type hints"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pydantic-2.7.1-py3-none-any.whl", hash = "sha256:e029badca45266732a9a79898a15ae2e8b14840b1eabbb25844be28f0b33f3d5"},
+ {file = "pydantic-2.7.1.tar.gz", hash = "sha256:e9dbb5eada8abe4d9ae5f46b9939aead650cd2b68f249bb3a8139dbe125803cc"},
+]
+
+[package.dependencies]
+annotated-types = ">=0.4.0"
+pydantic-core = "2.18.2"
+typing-extensions = ">=4.6.1"
+
+[package.extras]
+email = ["email-validator (>=2.0.0)"]
+
+[[package]]
+name = "pydantic-core"
+version = "2.18.2"
+description = "Core functionality for Pydantic validation and serialization"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pydantic_core-2.18.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:9e08e867b306f525802df7cd16c44ff5ebbe747ff0ca6cf3fde7f36c05a59a81"},
+ {file = "pydantic_core-2.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f0a21cbaa69900cbe1a2e7cad2aa74ac3cf21b10c3efb0fa0b80305274c0e8a2"},
+ {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0680b1f1f11fda801397de52c36ce38ef1c1dc841a0927a94f226dea29c3ae3d"},
+ {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:95b9d5e72481d3780ba3442eac863eae92ae43a5f3adb5b4d0a1de89d42bb250"},
+ {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fcf5cd9c4b655ad666ca332b9a081112cd7a58a8b5a6ca7a3104bc950f2038"},
+ {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b5155ff768083cb1d62f3e143b49a8a3432e6789a3abee8acd005c3c7af1c74"},
+ {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553ef617b6836fc7e4df130bb851e32fe357ce36336d897fd6646d6058d980af"},
+ {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89ed9eb7d616ef5714e5590e6cf7f23b02d0d539767d33561e3675d6f9e3857"},
+ {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:75f7e9488238e920ab6204399ded280dc4c307d034f3924cd7f90a38b1829563"},
+ {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ef26c9e94a8c04a1b2924149a9cb081836913818e55681722d7f29af88fe7b38"},
+ {file = "pydantic_core-2.18.2-cp310-none-win32.whl", hash = "sha256:182245ff6b0039e82b6bb585ed55a64d7c81c560715d1bad0cbad6dfa07b4027"},
+ {file = "pydantic_core-2.18.2-cp310-none-win_amd64.whl", hash = "sha256:e23ec367a948b6d812301afc1b13f8094ab7b2c280af66ef450efc357d2ae543"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:219da3f096d50a157f33645a1cf31c0ad1fe829a92181dd1311022f986e5fbe3"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc1cfd88a64e012b74e94cd00bbe0f9c6df57049c97f02bb07d39e9c852e19a4"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05b7133a6e6aeb8df37d6f413f7705a37ab4031597f64ab56384c94d98fa0e90"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:224c421235f6102e8737032483f43c1a8cfb1d2f45740c44166219599358c2cd"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b14d82cdb934e99dda6d9d60dc84a24379820176cc4a0d123f88df319ae9c150"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2728b01246a3bba6de144f9e3115b532ee44bd6cf39795194fb75491824a1413"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:470b94480bb5ee929f5acba6995251ada5e059a5ef3e0dfc63cca287283ebfa6"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:997abc4df705d1295a42f95b4eec4950a37ad8ae46d913caeee117b6b198811c"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75250dbc5290e3f1a0f4618db35e51a165186f9034eff158f3d490b3fed9f8a0"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4456f2dca97c425231d7315737d45239b2b51a50dc2b6f0c2bb181fce6207664"},
+ {file = "pydantic_core-2.18.2-cp311-none-win32.whl", hash = "sha256:269322dcc3d8bdb69f054681edff86276b2ff972447863cf34c8b860f5188e2e"},
+ {file = "pydantic_core-2.18.2-cp311-none-win_amd64.whl", hash = "sha256:800d60565aec896f25bc3cfa56d2277d52d5182af08162f7954f938c06dc4ee3"},
+ {file = "pydantic_core-2.18.2-cp311-none-win_arm64.whl", hash = "sha256:1404c69d6a676245199767ba4f633cce5f4ad4181f9d0ccb0577e1f66cf4c46d"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:fb2bd7be70c0fe4dfd32c951bc813d9fe6ebcbfdd15a07527796c8204bd36242"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6132dd3bd52838acddca05a72aafb6eab6536aa145e923bb50f45e78b7251043"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d904828195733c183d20a54230c0df0eb46ec746ea1a666730787353e87182"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9bd70772c720142be1020eac55f8143a34ec9f82d75a8e7a07852023e46617f"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b8ed04b3582771764538f7ee7001b02e1170223cf9b75dff0bc698fadb00cf3"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6dac87ddb34aaec85f873d737e9d06a3555a1cc1a8e0c44b7f8d5daeb89d86f"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ca4ae5a27ad7a4ee5170aebce1574b375de390bc01284f87b18d43a3984df72"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:886eec03591b7cf058467a70a87733b35f44707bd86cf64a615584fd72488b7c"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ca7b0c1f1c983e064caa85f3792dd2fe3526b3505378874afa84baf662e12241"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b4356d3538c3649337df4074e81b85f0616b79731fe22dd11b99499b2ebbdf3"},
+ {file = "pydantic_core-2.18.2-cp312-none-win32.whl", hash = "sha256:8b172601454f2d7701121bbec3425dd71efcb787a027edf49724c9cefc14c038"},
+ {file = "pydantic_core-2.18.2-cp312-none-win_amd64.whl", hash = "sha256:b1bd7e47b1558ea872bd16c8502c414f9e90dcf12f1395129d7bb42a09a95438"},
+ {file = "pydantic_core-2.18.2-cp312-none-win_arm64.whl", hash = "sha256:98758d627ff397e752bc339272c14c98199c613f922d4a384ddc07526c86a2ec"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:9fdad8e35f278b2c3eb77cbdc5c0a49dada440657bf738d6905ce106dc1de439"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1d90c3265ae107f91a4f279f4d6f6f1d4907ac76c6868b27dc7fb33688cfb347"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390193c770399861d8df9670fb0d1874f330c79caaca4642332df7c682bf6b91"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:82d5d4d78e4448683cb467897fe24e2b74bb7b973a541ea1dcfec1d3cbce39fb"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4774f3184d2ef3e14e8693194f661dea5a4d6ca4e3dc8e39786d33a94865cefd"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4d938ec0adf5167cb335acb25a4ee69a8107e4984f8fbd2e897021d9e4ca21b"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0e8b1be28239fc64a88a8189d1df7fad8be8c1ae47fcc33e43d4be15f99cc70"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:868649da93e5a3d5eacc2b5b3b9235c98ccdbfd443832f31e075f54419e1b96b"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:78363590ef93d5d226ba21a90a03ea89a20738ee5b7da83d771d283fd8a56761"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:852e966fbd035a6468fc0a3496589b45e2208ec7ca95c26470a54daed82a0788"},
+ {file = "pydantic_core-2.18.2-cp38-none-win32.whl", hash = "sha256:6a46e22a707e7ad4484ac9ee9f290f9d501df45954184e23fc29408dfad61350"},
+ {file = "pydantic_core-2.18.2-cp38-none-win_amd64.whl", hash = "sha256:d91cb5ea8b11607cc757675051f61b3d93f15eca3cefb3e6c704a5d6e8440f4e"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:ae0a8a797a5e56c053610fa7be147993fe50960fa43609ff2a9552b0e07013e8"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:042473b6280246b1dbf530559246f6842b56119c2926d1e52b631bdc46075f2a"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a388a77e629b9ec814c1b1e6b3b595fe521d2cdc625fcca26fbc2d44c816804"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25add29b8f3b233ae90ccef2d902d0ae0432eb0d45370fe315d1a5cf231004b"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f459a5ce8434614dfd39bbebf1041952ae01da6bed9855008cb33b875cb024c0"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eff2de745698eb46eeb51193a9f41d67d834d50e424aef27df2fcdee1b153845"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8309f67285bdfe65c372ea3722b7a5642680f3dba538566340a9d36e920b5f0"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f93a8a2e3938ff656a7c1bc57193b1319960ac015b6e87d76c76bf14fe0244b4"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:22057013c8c1e272eb8d0eebc796701167d8377441ec894a8fed1af64a0bf399"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cfeecd1ac6cc1fb2692c3d5110781c965aabd4ec5d32799773ca7b1456ac636b"},
+ {file = "pydantic_core-2.18.2-cp39-none-win32.whl", hash = "sha256:0d69b4c2f6bb3e130dba60d34c0845ba31b69babdd3f78f7c0c8fae5021a253e"},
+ {file = "pydantic_core-2.18.2-cp39-none-win_amd64.whl", hash = "sha256:d9319e499827271b09b4e411905b24a426b8fb69464dfa1696258f53a3334641"},
+ {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a1874c6dd4113308bd0eb568418e6114b252afe44319ead2b4081e9b9521fe75"},
+ {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:ccdd111c03bfd3666bd2472b674c6899550e09e9f298954cfc896ab92b5b0e6d"},
+ {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e18609ceaa6eed63753037fc06ebb16041d17d28199ae5aba0052c51449650a9"},
+ {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e5c584d357c4e2baf0ff7baf44f4994be121e16a2c88918a5817331fc7599d7"},
+ {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43f0f463cf89ace478de71a318b1b4f05ebc456a9b9300d027b4b57c1a2064fb"},
+ {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e1b395e58b10b73b07b7cf740d728dd4ff9365ac46c18751bf8b3d8cca8f625a"},
+ {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0098300eebb1c837271d3d1a2cd2911e7c11b396eac9661655ee524a7f10587b"},
+ {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:36789b70d613fbac0a25bb07ab3d9dba4d2e38af609c020cf4d888d165ee0bf3"},
+ {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3f9a801e7c8f1ef8718da265bba008fa121243dfe37c1cea17840b0944dfd72c"},
+ {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3a6515ebc6e69d85502b4951d89131ca4e036078ea35533bb76327f8424531ce"},
+ {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20aca1e2298c56ececfd8ed159ae4dde2df0781988c97ef77d5c16ff4bd5b400"},
+ {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:223ee893d77a310a0391dca6df00f70bbc2f36a71a895cecd9a0e762dc37b349"},
+ {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2334ce8c673ee93a1d6a65bd90327588387ba073c17e61bf19b4fd97d688d63c"},
+ {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:cbca948f2d14b09d20268cda7b0367723d79063f26c4ffc523af9042cad95592"},
+ {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b3ef08e20ec49e02d5c6717a91bb5af9b20f1805583cb0adfe9ba2c6b505b5ae"},
+ {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6fdc8627910eed0c01aed6a390a252fe3ea6d472ee70fdde56273f198938374"},
+ {file = "pydantic_core-2.18.2.tar.gz", hash = "sha256:2e29d20810dfc3043ee13ac7d9e25105799817683348823f305ab3f349b9386e"},
+]
+
+[package.dependencies]
+typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
+
+[[package]]
+name = "pygments"
+version = "2.17.2"
+description = "Pygments is a syntax highlighting package written in Python."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"},
+ {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"},
+]
+
+[package.extras]
+plugins = ["importlib-metadata"]
+windows-terminal = ["colorama (>=0.4.6)"]
+
+[[package]]
+name = "regex"
+version = "2023.12.25"
+description = "Alternative regular expression module, to replace re."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "regex-2023.12.25-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0694219a1d54336fd0445ea382d49d36882415c0134ee1e8332afd1529f0baa5"},
+ {file = "regex-2023.12.25-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b014333bd0217ad3d54c143de9d4b9a3ca1c5a29a6d0d554952ea071cff0f1f8"},
+ {file = "regex-2023.12.25-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d865984b3f71f6d0af64d0d88f5733521698f6c16f445bb09ce746c92c97c586"},
+ {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e0eabac536b4cc7f57a5f3d095bfa557860ab912f25965e08fe1545e2ed8b4c"},
+ {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c25a8ad70e716f96e13a637802813f65d8a6760ef48672aa3502f4c24ea8b400"},
+ {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9b6d73353f777630626f403b0652055ebfe8ff142a44ec2cf18ae470395766e"},
+ {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9cc99d6946d750eb75827cb53c4371b8b0fe89c733a94b1573c9dd16ea6c9e4"},
+ {file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88d1f7bef20c721359d8675f7d9f8e414ec5003d8f642fdfd8087777ff7f94b5"},
+ {file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cb3fe77aec8f1995611f966d0c656fdce398317f850d0e6e7aebdfe61f40e1cd"},
+ {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7aa47c2e9ea33a4a2a05f40fcd3ea36d73853a2aae7b4feab6fc85f8bf2c9704"},
+ {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:df26481f0c7a3f8739fecb3e81bc9da3fcfae34d6c094563b9d4670b047312e1"},
+ {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c40281f7d70baf6e0db0c2f7472b31609f5bc2748fe7275ea65a0b4601d9b392"},
+ {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:d94a1db462d5690ebf6ae86d11c5e420042b9898af5dcf278bd97d6bda065423"},
+ {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ba1b30765a55acf15dce3f364e4928b80858fa8f979ad41f862358939bdd1f2f"},
+ {file = "regex-2023.12.25-cp310-cp310-win32.whl", hash = "sha256:150c39f5b964e4d7dba46a7962a088fbc91f06e606f023ce57bb347a3b2d4630"},
+ {file = "regex-2023.12.25-cp310-cp310-win_amd64.whl", hash = "sha256:09da66917262d9481c719599116c7dc0c321ffcec4b1f510c4f8a066f8768105"},
+ {file = "regex-2023.12.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1b9d811f72210fa9306aeb88385b8f8bcef0dfbf3873410413c00aa94c56c2b6"},
+ {file = "regex-2023.12.25-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d902a43085a308cef32c0d3aea962524b725403fd9373dea18110904003bac97"},
+ {file = "regex-2023.12.25-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d166eafc19f4718df38887b2bbe1467a4f74a9830e8605089ea7a30dd4da8887"},
+ {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7ad32824b7f02bb3c9f80306d405a1d9b7bb89362d68b3c5a9be53836caebdb"},
+ {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:636ba0a77de609d6510235b7f0e77ec494d2657108f777e8765efc060094c98c"},
+ {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fda75704357805eb953a3ee15a2b240694a9a514548cd49b3c5124b4e2ad01b"},
+ {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f72cbae7f6b01591f90814250e636065850c5926751af02bb48da94dfced7baa"},
+ {file = "regex-2023.12.25-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db2a0b1857f18b11e3b0e54ddfefc96af46b0896fb678c85f63fb8c37518b3e7"},
+ {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7502534e55c7c36c0978c91ba6f61703faf7ce733715ca48f499d3dbbd7657e0"},
+ {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e8c7e08bb566de4faaf11984af13f6bcf6a08f327b13631d41d62592681d24fe"},
+ {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:283fc8eed679758de38fe493b7d7d84a198b558942b03f017b1f94dda8efae80"},
+ {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:f44dd4d68697559d007462b0a3a1d9acd61d97072b71f6d1968daef26bc744bd"},
+ {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:67d3ccfc590e5e7197750fcb3a2915b416a53e2de847a728cfa60141054123d4"},
+ {file = "regex-2023.12.25-cp311-cp311-win32.whl", hash = "sha256:68191f80a9bad283432385961d9efe09d783bcd36ed35a60fb1ff3f1ec2efe87"},
+ {file = "regex-2023.12.25-cp311-cp311-win_amd64.whl", hash = "sha256:7d2af3f6b8419661a0c421584cfe8aaec1c0e435ce7e47ee2a97e344b98f794f"},
+ {file = "regex-2023.12.25-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8a0ccf52bb37d1a700375a6b395bff5dd15c50acb745f7db30415bae3c2b0715"},
+ {file = "regex-2023.12.25-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c3c4a78615b7762740531c27cf46e2f388d8d727d0c0c739e72048beb26c8a9d"},
+ {file = "regex-2023.12.25-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ad83e7545b4ab69216cef4cc47e344d19622e28aabec61574b20257c65466d6a"},
+ {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7a635871143661feccce3979e1727c4e094f2bdfd3ec4b90dfd4f16f571a87a"},
+ {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d498eea3f581fbe1b34b59c697512a8baef88212f92e4c7830fcc1499f5b45a5"},
+ {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:43f7cd5754d02a56ae4ebb91b33461dc67be8e3e0153f593c509e21d219c5060"},
+ {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51f4b32f793812714fd5307222a7f77e739b9bc566dc94a18126aba3b92b98a3"},
+ {file = "regex-2023.12.25-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba99d8077424501b9616b43a2d208095746fb1284fc5ba490139651f971d39d9"},
+ {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4bfc2b16e3ba8850e0e262467275dd4d62f0d045e0e9eda2bc65078c0110a11f"},
+ {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8c2c19dae8a3eb0ea45a8448356ed561be843b13cbc34b840922ddf565498c1c"},
+ {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:60080bb3d8617d96f0fb7e19796384cc2467447ef1c491694850ebd3670bc457"},
+ {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b77e27b79448e34c2c51c09836033056a0547aa360c45eeeb67803da7b0eedaf"},
+ {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:518440c991f514331f4850a63560321f833979d145d7d81186dbe2f19e27ae3d"},
+ {file = "regex-2023.12.25-cp312-cp312-win32.whl", hash = "sha256:e2610e9406d3b0073636a3a2e80db05a02f0c3169b5632022b4e81c0364bcda5"},
+ {file = "regex-2023.12.25-cp312-cp312-win_amd64.whl", hash = "sha256:cc37b9aeebab425f11f27e5e9e6cf580be7206c6582a64467a14dda211abc232"},
+ {file = "regex-2023.12.25-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:da695d75ac97cb1cd725adac136d25ca687da4536154cdc2815f576e4da11c69"},
+ {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d126361607b33c4eb7b36debc173bf25d7805847346dd4d99b5499e1fef52bc7"},
+ {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4719bb05094d7d8563a450cf8738d2e1061420f79cfcc1fa7f0a44744c4d8f73"},
+ {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5dd58946bce44b53b06d94aa95560d0b243eb2fe64227cba50017a8d8b3cd3e2"},
+ {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22a86d9fff2009302c440b9d799ef2fe322416d2d58fc124b926aa89365ec482"},
+ {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2aae8101919e8aa05ecfe6322b278f41ce2994c4a430303c4cd163fef746e04f"},
+ {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e692296c4cc2873967771345a876bcfc1c547e8dd695c6b89342488b0ea55cd8"},
+ {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:263ef5cc10979837f243950637fffb06e8daed7f1ac1e39d5910fd29929e489a"},
+ {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d6f7e255e5fa94642a0724e35406e6cb7001c09d476ab5fce002f652b36d0c39"},
+ {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:88ad44e220e22b63b0f8f81f007e8abbb92874d8ced66f32571ef8beb0643b2b"},
+ {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:3a17d3ede18f9cedcbe23d2daa8a2cd6f59fe2bf082c567e43083bba3fb00347"},
+ {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d15b274f9e15b1a0b7a45d2ac86d1f634d983ca40d6b886721626c47a400bf39"},
+ {file = "regex-2023.12.25-cp37-cp37m-win32.whl", hash = "sha256:ed19b3a05ae0c97dd8f75a5d8f21f7723a8c33bbc555da6bbe1f96c470139d3c"},
+ {file = "regex-2023.12.25-cp37-cp37m-win_amd64.whl", hash = "sha256:a6d1047952c0b8104a1d371f88f4ab62e6275567d4458c1e26e9627ad489b445"},
+ {file = "regex-2023.12.25-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b43523d7bc2abd757119dbfb38af91b5735eea45537ec6ec3a5ec3f9562a1c53"},
+ {file = "regex-2023.12.25-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:efb2d82f33b2212898f1659fb1c2e9ac30493ac41e4d53123da374c3b5541e64"},
+ {file = "regex-2023.12.25-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b7fca9205b59c1a3d5031f7e64ed627a1074730a51c2a80e97653e3e9fa0d415"},
+ {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086dd15e9435b393ae06f96ab69ab2d333f5d65cbe65ca5a3ef0ec9564dfe770"},
+ {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e81469f7d01efed9b53740aedd26085f20d49da65f9c1f41e822a33992cb1590"},
+ {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34e4af5b27232f68042aa40a91c3b9bb4da0eeb31b7632e0091afc4310afe6cb"},
+ {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9852b76ab558e45b20bf1893b59af64a28bd3820b0c2efc80e0a70a4a3ea51c1"},
+ {file = "regex-2023.12.25-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff100b203092af77d1a5a7abe085b3506b7eaaf9abf65b73b7d6905b6cb76988"},
+ {file = "regex-2023.12.25-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cc038b2d8b1470364b1888a98fd22d616fba2b6309c5b5f181ad4483e0017861"},
+ {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:094ba386bb5c01e54e14434d4caabf6583334090865b23ef58e0424a6286d3dc"},
+ {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5cd05d0f57846d8ba4b71d9c00f6f37d6b97d5e5ef8b3c3840426a475c8f70f4"},
+ {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:9aa1a67bbf0f957bbe096375887b2505f5d8ae16bf04488e8b0f334c36e31360"},
+ {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:98a2636994f943b871786c9e82bfe7883ecdaba2ef5df54e1450fa9869d1f756"},
+ {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:37f8e93a81fc5e5bd8db7e10e62dc64261bcd88f8d7e6640aaebe9bc180d9ce2"},
+ {file = "regex-2023.12.25-cp38-cp38-win32.whl", hash = "sha256:d78bd484930c1da2b9679290a41cdb25cc127d783768a0369d6b449e72f88beb"},
+ {file = "regex-2023.12.25-cp38-cp38-win_amd64.whl", hash = "sha256:b521dcecebc5b978b447f0f69b5b7f3840eac454862270406a39837ffae4e697"},
+ {file = "regex-2023.12.25-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f7bc09bc9c29ebead055bcba136a67378f03d66bf359e87d0f7c759d6d4ffa31"},
+ {file = "regex-2023.12.25-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e14b73607d6231f3cc4622809c196b540a6a44e903bcfad940779c80dffa7be7"},
+ {file = "regex-2023.12.25-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9eda5f7a50141291beda3edd00abc2d4a5b16c29c92daf8d5bd76934150f3edc"},
+ {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc6bb9aa69aacf0f6032c307da718f61a40cf970849e471254e0e91c56ffca95"},
+ {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:298dc6354d414bc921581be85695d18912bea163a8b23cac9a2562bbcd5088b1"},
+ {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f4e475a80ecbd15896a976aa0b386c5525d0ed34d5c600b6d3ebac0a67c7ddf"},
+ {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:531ac6cf22b53e0696f8e1d56ce2396311254eb806111ddd3922c9d937151dae"},
+ {file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22f3470f7524b6da61e2020672df2f3063676aff444db1daa283c2ea4ed259d6"},
+ {file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:89723d2112697feaa320c9d351e5f5e7b841e83f8b143dba8e2d2b5f04e10923"},
+ {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0ecf44ddf9171cd7566ef1768047f6e66975788258b1c6c6ca78098b95cf9a3d"},
+ {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:905466ad1702ed4acfd67a902af50b8db1feeb9781436372261808df7a2a7bca"},
+ {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:4558410b7a5607a645e9804a3e9dd509af12fb72b9825b13791a37cd417d73a5"},
+ {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7e316026cc1095f2a3e8cc012822c99f413b702eaa2ca5408a513609488cb62f"},
+ {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3b1de218d5375cd6ac4b5493e0b9f3df2be331e86520f23382f216c137913d20"},
+ {file = "regex-2023.12.25-cp39-cp39-win32.whl", hash = "sha256:11a963f8e25ab5c61348d090bf1b07f1953929c13bd2309a0662e9ff680763c9"},
+ {file = "regex-2023.12.25-cp39-cp39-win_amd64.whl", hash = "sha256:e693e233ac92ba83a87024e1d32b5f9ab15ca55ddd916d878146f4e3406b5c91"},
+ {file = "regex-2023.12.25.tar.gz", hash = "sha256:29171aa128da69afdf4bde412d5bedc335f2ca8fcfe4489038577d05f16181e5"},
+]
+
+[[package]]
+name = "six"
+version = "1.16.0"
+description = "Python 2 and 3 compatibility utilities"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+files = [
+ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
+ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
+]
+
+[[package]]
+name = "sniffio"
+version = "1.3.1"
+description = "Sniff out which async library your code is running under"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
+ {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
+]
+
+[[package]]
+name = "soupsieve"
+version = "2.5"
+description = "A modern CSS selector implementation for Beautiful Soup."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"},
+ {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"},
+]
+
+[[package]]
+name = "tenacity"
+version = "8.2.3"
+description = "Retry code until it succeeds"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "tenacity-8.2.3-py3-none-any.whl", hash = "sha256:ce510e327a630c9e1beaf17d42e6ffacc88185044ad85cf74c0a8887c6a0f88c"},
+ {file = "tenacity-8.2.3.tar.gz", hash = "sha256:5398ef0d78e63f40007c1fb4c0bff96e1911394d2fa8d194f77619c05ff6cc8a"},
+]
+
+[package.extras]
+doc = ["reno", "sphinx", "tornado (>=4.5)"]
+
+[[package]]
+name = "typing-extensions"
+version = "4.11.0"
+description = "Backported and Experimental Type Hints for Python 3.8+"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"},
+ {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"},
+]
+
+[[package]]
+name = "wcwidth"
+version = "0.2.13"
+description = "Measures the displayed width of unicode strings in a terminal"
+optional = false
+python-versions = "*"
+files = [
+ {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"},
+ {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"},
+]
+
+[[package]]
+name = "win32-setctime"
+version = "1.1.0"
+description = "A small Python utility to set file creation time on Windows"
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"},
+ {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"},
+]
+
+[package.extras]
+dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"]
+
+[metadata]
+lock-version = "2.0"
+python-versions = "^3.11"
+content-hash = "d1e738dcf8fd6798c036097d82214e2ad9a2a3e881a33721792a490a8e4850dc"
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..1bd1f54
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,26 @@
+[tool.poetry]
+name = "applemusicdecrypt"
+version = "0.1.0"
+description = ""
+authors = ["WorldObservationLog "]
+readme = "README.md"
+
+[tool.poetry.dependencies]
+python = "^3.11"
+httpx = "^0.27.0"
+regex = "^2023.12.25"
+pydantic = "^2.7.0"
+loguru = "^0.7.2"
+six = "^1.16.0"
+lxml = "^5.2.1"
+beautifulsoup4 = "^4.12.3"
+m3u8 = "^4.1.0"
+frida-tools = "^12.3.0"
+pure-python-adb = "^0.3.0.dev0"
+frida = "^16.2.1"
+tenacity = "^8.2.3"
+prompt-toolkit = "^3.0.43"
+
+[build-system]
+requires = ["poetry-core"]
+build-backend = "poetry.core.masonry.api"
diff --git a/src/__init__.py b/src/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/adb.py b/src/adb.py
new file mode 100644
index 0000000..7e7940d
--- /dev/null
+++ b/src/adb.py
@@ -0,0 +1,150 @@
+import asyncio
+import json
+import subprocess
+from typing import Optional
+
+import frida
+import regex
+from loguru import logger
+from ppadb.client import Client as AdbClient
+from ppadb.device import Device as AdbDevice
+
+from src.exceptions import FridaNotExistException, ADBConnectException, FailedGetAuthParamException
+from src.types import AuthParams
+
+
+class Device:
+ host: str
+ client: AdbClient
+ device: AdbDevice
+ fridaPath: str
+ fridaPort: int
+ fridaDevice: frida.core.Device = None
+ fridaSession: frida.core.Session = None
+ pid: int
+ authParams: AuthParams = None
+ suMethod: str
+ decryptLock: asyncio.Lock
+
+ def __init__(self, host="127.0.0.1", port=5037,
+ frida_path="/data/local/tmp/frida-server-16.2.1-android-x86_64", su_method: str = "su -c"):
+ self.client = AdbClient(host, port)
+ self.fridaPath = frida_path
+ self.suMethod = su_method
+ self.host = host
+ self.decryptLock = asyncio.Lock()
+
+ def connect(self, host: str, port: int):
+ try:
+ status = self.client.remote_connect(host, port)
+ except RuntimeError:
+ subprocess.run("adb devices", stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ status = self.client.remote_connect(host, port)
+ if not status:
+ raise ADBConnectException
+ self.device = self.client.device(f"{host}:{port}")
+
+ def _execute_command(self, cmd: str, su: bool = False) -> Optional[str]:
+ if su:
+ cmd = cmd.replace("\"", "\\\"")
+ output = self.device.shell(f"{self.suMethod} \"{cmd}\"")
+ else:
+ output = self.device.shell(cmd, timeout=30)
+ if not output:
+ return ""
+ return output
+
+ def _if_frida_running(self) -> bool:
+ logger.debug("checking if frida-server running")
+ output = self._execute_command("ps -e | grep frida")
+ if not output or "frida" not in output:
+ return False
+ return True
+
+ def _start_remote_frida(self):
+ logger.debug("starting remote frida")
+ output = f"(ls {self.fridaPath} && echo True) || echo False"
+ if not output or "True" not in output:
+ raise FridaNotExistException
+ permission = self._execute_command(f"ls -l {self.fridaPath}")
+ if not permission or "x" not in permission[:10]:
+ self._execute_command(f"chmod +x {self.fridaPath}", True)
+ self._execute_command(f"{self.fridaPath} &", True)
+
+ def _start_forward(self, local_port: int, remote_port: int):
+ self.device.forward(f"tcp:{local_port}", f"tcp:{remote_port}")
+
+ def _inject_frida(self, frida_port):
+ logger.debug("injecting agent script")
+ self.fridaPort = frida_port
+ with open("agent.js", "r") as f:
+ agent = f.read().replace("2147483647", str(frida_port))
+ if not self.fridaDevice:
+ frida.get_device_manager().add_remote_device(self.device.serial)
+ self.fridaDevice = frida.get_device_manager().get_device(self.device.serial)
+ self.pid = self.fridaDevice.spawn("com.apple.android.music")
+ self.fridaSession = self.fridaDevice.attach(self.pid)
+ script: frida.core.Script = self.fridaSession.create_script(agent)
+ script.load()
+ self.fridaDevice.resume(self.pid)
+
+ def restart_inject_frida(self):
+ self.fridaSession.detach()
+ self._kill_apple_music()
+ self._inject_frida(self.fridaPort)
+
+ def _kill_apple_music(self):
+ self._execute_command(f"kill -9 {self.pid}", su=True)
+
+ def start_inject_frida(self, frida_port):
+ if not self._if_frida_running():
+ self._start_remote_frida()
+ self._start_forward(frida_port, frida_port)
+ self._inject_frida(frida_port)
+
+ def _get_dsid(self) -> str:
+ logger.debug("getting dsid")
+ dsid = self._execute_command(
+ "sqlite3 /data/data/com.apple.android.music/files/mpl_db/cookies.sqlitedb \"select value from cookies where name='X-Dsid';\"", True)
+ if not dsid:
+ raise FailedGetAuthParamException
+ return dsid.strip()
+
+ def _get_account_token(self, dsid: str) -> str:
+ logger.debug("getting account token")
+ account_token = self._execute_command(
+ f"sqlite3 /data/data/com.apple.android.music/files/mpl_db/cookies.sqlitedb \"select value from cookies where name='mz_at_ssl-{dsid}';\"", True)
+ if not account_token:
+ raise FailedGetAuthParamException
+ return account_token.strip()
+
+ def _get_access_token(self) -> str:
+ logger.debug("getting access token")
+ prefs = self._execute_command("cat /data/data/com.apple.android.music/shared_prefs/preferences.xml", True)
+ match = regex.search(r"eyJr[^<]*", prefs)
+ if not match:
+ raise FailedGetAuthParamException
+ return match[0]
+
+ def _get_storefront(self) -> str | None:
+ logger.debug("getting storefront")
+ storefront_id = self._execute_command(
+ "sqlite3 /data/data/com.apple.android.music/files/mpl_db/accounts.sqlitedb \"select storeFront from account;\"", True)
+ if not storefront_id:
+ raise FailedGetAuthParamException
+ with open("assets/storefront_ids.json") as f:
+ storefront_ids = json.load(f)
+ for storefront_mapping in storefront_ids:
+ if storefront_mapping["storefrontId"] == int(storefront_id.split("-")[0]):
+ return storefront_mapping["code"]
+ return None
+
+ def get_auth_params(self):
+ if not self.authParams:
+ dsid = self._get_dsid()
+ token = self._get_account_token(dsid)
+ access_token = self._get_access_token()
+ storefront = self._get_storefront()
+ self.authParams = AuthParams(dsid=dsid, accountToken=token,
+ accountAccessToken=access_token, storefront=storefront)
+ return self.authParams
diff --git a/src/api.py b/src/api.py
new file mode 100644
index 0000000..652cf0d
--- /dev/null
+++ b/src/api.py
@@ -0,0 +1,103 @@
+import asyncio
+import logging
+from ssl import SSLError
+
+import httpcore
+import httpx
+import regex
+
+from tenacity import retry, retry_if_exception_type, stop_after_attempt, before_sleep_log
+from loguru import logger
+
+from src.models import *
+
+client = httpx.AsyncClient()
+lock = asyncio.Semaphore(1)
+user_agent_browser = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
+user_agent_itunes = "iTunes/12.11.3 (Windows; Microsoft Windows 10 x64 Professional Edition (Build 19041); x64) AppleWebKit/7611.1022.4001.1 (dt:2)"
+user_agent_app = "Music/5.7 Android/10 model/Pixel6GR1YH build/1234 (dt:66)"
+
+
+@retry(retry=retry_if_exception_type((httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError)), stop=stop_after_attempt(5),
+ before_sleep=before_sleep_log(logger, logging.WARN))
+async def get_token():
+ req = await client.get("https://beta.music.apple.com")
+ index_js_uri = regex.findall(r"/assets/index-legacy-[^/]+\.js", req.text)[0]
+ js_req = await client.get("https://beta.music.apple.com" + index_js_uri)
+ token = regex.search(r'eyJh([^"]*)', js_req.text)[0]
+ return token
+
+
+@retry(retry=retry_if_exception_type((httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError)), stop=stop_after_attempt(5),
+ before_sleep=before_sleep_log(logger, logging.WARN))
+async def download_song(url: str) -> bytes:
+ async with lock:
+ return (await client.get(url)).content
+
+
+@retry(retry=retry_if_exception_type((httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError)), stop=stop_after_attempt(5),
+ before_sleep=before_sleep_log(logger, logging.WARN))
+async def get_meta(album_id: str, token: str, storefront: str):
+ if "pl." in album_id:
+ mtype = "playlists"
+ else:
+ mtype = "albums"
+ req = await client.get(f"https://amp-api.music.apple.com/v1/catalog/{storefront}/{mtype}/{album_id}",
+ params={"omit[resource]": "autos", "include": "tracks,artists,record-labels",
+ "include[songs]": "artists", "fields[artists]": "name",
+ "fields[albums:albums]": "artistName,artwork,name,releaseDate,url",
+ "fields[record-labels]": "name"},
+ headers={"Authorization": f"Bearer {token}", "User-Agent": user_agent_browser,
+ "Origin": "https://music.apple.com"})
+ if mtype == "albums":
+ return AlbumMeta.model_validate(req.json())
+ else:
+ result = PlaylistMeta.model_validate(req.json())
+ result.data[0].attributes.artistName = "Apple Music"
+ if result.data[0].relationships.tracks.next:
+ page = 0
+ while True:
+ page += 100
+ page_req = await client.get(
+ f"https://amp-api.music.apple.com/v1/catalog/{storefront}/{mtype}/{album_id}/tracks?offset={page}",
+ headers={"Authorization": f"Bearer {token}", "User-Agent": user_agent_browser,
+ "Origin": "https://music.apple.com"})
+ page_result = TracksMeta.model_validate(page_req.json())
+ result.data[0].relationships.tracks.data.extend(page_result.data)
+ if not page_result.next:
+ break
+ return result
+
+
+@retry(retry=retry_if_exception_type((httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError)), stop=stop_after_attempt(5),
+ before_sleep=before_sleep_log(logger, logging.WARN))
+async def get_cover(url: str, cover_format: str):
+ formatted_url = regex.sub('bb.jpg', f'bb.{cover_format}', url)
+ req = await client.get(formatted_url.replace("{w}x{h}", "10000x10000"),
+ headers={"User-Agent": user_agent_browser})
+ return req.content
+
+
+@retry(retry=retry_if_exception_type((httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError)), stop=stop_after_attempt(5),
+ before_sleep=before_sleep_log(logger, logging.WARN))
+async def get_info_from_adam(adam_id: str, token: str, storefront: str):
+ req = await client.get(f"https://amp-api.music.apple.com/v1/catalog/{storefront}/songs/{adam_id}",
+ params={"extend": "extendedAssetUrls", "include": "albums"},
+ headers={"Authorization": f"Bearer {token}", "User-Agent": user_agent_itunes,
+ "Origin": "https://music.apple.com"})
+ song_data_obj = SongData.model_validate(req.json())
+ for data in song_data_obj.data:
+ if data.id == adam_id:
+ return data
+ return None
+
+
+@retry(retry=retry_if_exception_type((httpx.TimeoutException, httpcore.ConnectError, SSLError, FileNotFoundError)), stop=stop_after_attempt(5),
+ before_sleep=before_sleep_log(logger, logging.WARN))
+async def get_song_lyrics(song_id: str, storefront: str, token: str, dsid: str, account_token: str) -> str:
+ req = await client.get(f"https://amp-api.music.apple.com/v1/catalog/{storefront}/songs/{song_id}/lyrics",
+ headers={"Authorization": f"Bearer {token}", "User-Agent": user_agent_app,
+ "X-Dsid": dsid},
+ cookies={f"mz_at_ssl-{dsid}": account_token})
+ result = SongLyrics.model_validate(req.json())
+ return result.data[0].attributes.ttml
diff --git a/src/cmd.py b/src/cmd.py
new file mode 100644
index 0000000..26c4124
--- /dev/null
+++ b/src/cmd.py
@@ -0,0 +1,104 @@
+import argparse
+import asyncio
+import random
+import sys
+from asyncio import Task
+
+from loguru import logger
+from prompt_toolkit import PromptSession, print_formatted_text, ANSI
+from prompt_toolkit.patch_stdout import patch_stdout
+
+from src.adb import Device
+from src.api import get_token
+from src.config import Config
+from src.rip import rip_song, rip_album
+from src.types import GlobalAuthParams
+from src.url import AppleMusicURL, URLType
+
+
+class NewInteractiveShell:
+ loop: asyncio.AbstractEventLoop
+ config: Config
+ tasks: list[Task] = []
+ devices: list[Device] = []
+ storefront_device_mapping: dict[str, list[Device]] = {}
+ anonymous_access_token: str
+ parser: argparse.ArgumentParser
+
+ def __init__(self, loop: asyncio.AbstractEventLoop):
+ self.loop = loop
+ self.config = Config.load_from_config()
+ self.anonymous_access_token = loop.run_until_complete(get_token())
+
+ self.parser = argparse.ArgumentParser(exit_on_error=False)
+ subparser = self.parser.add_subparsers()
+ download_parser = subparser.add_parser("download")
+ download_parser.add_argument("url", type=str)
+ download_parser.add_argument("-c", "--codec",
+ choices=["alac", "ec3", "aac", "aac-binaural", "aac-downmix"], default="alac")
+ download_parser.add_argument("-f", "--force", type=bool, default=False)
+ subparser.add_parser("exit")
+
+ logger.remove()
+ logger.add(lambda msg: print_formatted_text(ANSI(msg), end=""), colorize=True, level="INFO")
+
+ for device_info in self.config.devices:
+ device = Device(frida_path=device_info.fridaPath)
+ device.connect(device_info.host, device_info.port)
+ logger.info(f"Device {device_info.host}:{device_info.port} has connected")
+ self.devices.append(device)
+ auth_params = device.get_auth_params()
+ if not self.storefront_device_mapping.get(auth_params.storefront.lower()):
+ self.storefront_device_mapping.update({auth_params.storefront.lower(): []})
+ self.storefront_device_mapping[auth_params.storefront.lower()].append(device)
+ device.start_inject_frida(device_info.agentPort)
+
+ async def command_parser(self, cmd: str):
+ if not cmd.strip():
+ return
+ cmds = cmd.split(" ")
+ try:
+ args = self.parser.parse_args(cmds)
+ except argparse.ArgumentError:
+ logger.warning(f"Unknown command: {cmd}")
+ return
+ match cmds[0]:
+ case "download":
+ await self.do_download(args.url, args.codec, args.force)
+ case "exit":
+ self.loop.stop()
+ sys.exit()
+
+ async def do_download(self, raw_url: str, codec: str, force_download: bool):
+ url = AppleMusicURL.parse_url(raw_url)
+ devices = self.storefront_device_mapping.get(url.storefront)
+ if not devices:
+ logger.error(f"No device is available to decrypt the specified region: {url.storefront}")
+ available_devices = [device for device in devices if not device.decryptLock.locked()]
+ if not available_devices:
+ available_device: Device = random.choice(devices)
+ else:
+ available_device: Device = random.choice(available_devices)
+ global_auth_param = GlobalAuthParams.from_auth_params_and_token(available_device.get_auth_params(), self.anonymous_access_token)
+ match url.type:
+ case URLType.Song:
+ self.loop.create_task(rip_song(url, global_auth_param, codec, self.config, available_device, force_download))
+ case URLType.Album:
+ self.loop.create_task(rip_album(url, global_auth_param, codec, self.config, available_device))
+
+ async def handle_command(self):
+ session = PromptSession("> ")
+
+ while True:
+ try:
+ command = await session.prompt_async()
+ await self.command_parser(command)
+ except (EOFError, KeyboardInterrupt):
+ return
+
+ async def start(self):
+ with patch_stdout():
+ try:
+ await self.handle_command()
+ finally:
+ logger.info("Existing shell")
diff --git a/src/config.py b/src/config.py
new file mode 100644
index 0000000..49a88d7
--- /dev/null
+++ b/src/config.py
@@ -0,0 +1,42 @@
+import tomllib
+
+from pydantic import BaseModel
+
+
+class Language(BaseModel):
+ language: str
+ languageForGenre: str
+
+
+class Device(BaseModel):
+ host: str
+ port: int
+ agentPort: int
+ fridaPath: str
+
+
+class Download(BaseModel):
+ atmosConventToM4a: bool
+ songNameFormat: str
+ dirPathFormat: str
+ saveLyrics: bool
+ saveCover: bool
+ coverFormat: str
+ afterDownloaded: str
+
+
+class Metadata(BaseModel):
+ embedMetadata: list[str]
+
+
+class Config(BaseModel):
+ language: Language
+ devices: list[Device]
+ download: Download
+ metadata: Metadata
+
+ @classmethod
+ def load_from_config(cls, config_file: str = "config.toml"):
+ with open(config_file, "r") as f:
+ config = tomllib.loads(f.read())
+ return cls.parse_obj(config)
diff --git a/src/decrypt.py b/src/decrypt.py
new file mode 100644
index 0000000..4f43c15
--- /dev/null
+++ b/src/decrypt.py
@@ -0,0 +1,48 @@
+import asyncio
+import logging
+import sys
+
+from prompt_toolkit.shortcuts import ProgressBar
+from loguru import logger
+from tenacity import retry, retry_if_exception_type, stop_after_attempt, before_sleep_log
+
+from src.adb import Device
+from src.exceptions import DecryptException
+from src.models.song_data import Datum
+from src.mp4 import SongInfo, SampleInfo
+from src.types import defaultId, prefetchKey
+
+
+async def decrypt(info: SongInfo, keys: list[str], manifest: Datum, device: Device) -> bytes:
+ async with device.decryptLock:
+ logger.info(f"Decrypting song: {manifest.attributes.artistName} - {manifest.attributes.name}")
+ reader, writer = await asyncio.open_connection(device.host, device.fridaPort)
+ decrypted = bytes()
+ last_index = 255
+ for sample in info.samples:
+ if last_index != sample.descIndex:
+ if len(decrypted) != 0:
+ writer.write(bytes([0, 0, 0, 0]))
+ key_uri = keys[sample.descIndex]
+ track_id = manifest.id
+ if key_uri == prefetchKey:
+ track_id = defaultId
+ writer.write(bytes([len(track_id)]))
+ writer.write(track_id.encode("utf-8"))
+ writer.write(bytes([len(key_uri)]))
+ writer.write(key_uri.encode("utf-8"))
+ last_index = sample.descIndex
+ result = await decrypt_sample(writer, reader, sample)
+ decrypted += result
+ writer.write(bytes([0, 0, 0, 0]))
+ writer.close()
+ return decrypted
+
+
+async def decrypt_sample(writer: asyncio.StreamWriter, reader: asyncio.StreamReader, sample: SampleInfo) -> bytes:
+ writer.write(len(sample.data).to_bytes(4, byteorder="little", signed=False))
+ writer.write(sample.data)
+ result = await reader.read(len(sample.data))
+ if not result:
+ raise DecryptException
+ return result
diff --git a/src/exceptions.py b/src/exceptions.py
new file mode 100644
index 0000000..b6697c6
--- /dev/null
+++ b/src/exceptions.py
@@ -0,0 +1,18 @@
+class FridaNotExistException(Exception):
+ ...
+
+
+class ADBConnectException(Exception):
+ ...
+
+
+class FailedGetAuthParamException(Exception):
+ ...
+
+
+class DecryptException(Exception):
+ ...
+
+
+class NotTimeSyncedLyricsException(Exception):
+ ...
diff --git a/src/metadata.py b/src/metadata.py
new file mode 100644
index 0000000..dbd969b
--- /dev/null
+++ b/src/metadata.py
@@ -0,0 +1,58 @@
+from pydantic import BaseModel
+
+from src.api import get_cover
+from src.models.song_data import Datum
+from src.utils import ttml_convent_to_lrc
+
+
+class SongMetadata(BaseModel):
+ title: str
+ artist: str
+ album_artist: str
+ album: str
+ composer: str
+ genre: str
+ created: str
+ track: str
+ tracknum: int
+ disk: int
+ lyrics: str
+ cover: bytes = None
+ cover_url: str
+ copyright: str
+ record_company: str
+ upc: str
+ isrc: str
+
+ def to_itags_params(self, embed_metadata: list[str], cover_format: str):
+ tags = []
+ for key, value in self.model_dump().items():
+ if key in embed_metadata and value:
+ if key == "cover":
+ continue
+ if key == "lyrics":
+ lrc = ttml_convent_to_lrc(value)
+ tags.append(f"{key}={lrc}")
+ continue
+ tags.append(f"{key}={value}")
+ return ":".join(tags)
+
+ @classmethod
+ def parse_from_song_data(cls, song_data: Datum):
+ return cls(title=song_data.attributes.name, artist=song_data.attributes.artistName,
+ album_artist=song_data.relationships.albums.data[0].attributes.artistName,
+ album=song_data.attributes.albumName, composer=song_data.attributes.composerName,
+ genre=song_data.attributes.genreNames[0], created=song_data.attributes.releaseDate,
+ track=song_data.attributes.name, tracknum=song_data.attributes.trackNumber,
+ disk=song_data.attributes.discNumber, lyrics="", cover_url=song_data.attributes.artwork.url,
+ copyright=song_data.relationships.albums.data[0].attributes.copyright,
+ record_company=song_data.relationships.albums.data[0].attributes.recordLabel,
+ upc=song_data.relationships.albums.data[0].attributes.upc,
+ isrc=song_data.attributes.isrc
+ )
+
+ def set_lyrics(self, lyrics: str):
+ self.lyrics = lyrics
+
+ async def get_cover(self, cover_format: str):
+ self.cover = await get_cover(self.cover_url, cover_format)
\ No newline at end of file
diff --git a/src/models/__init__.py b/src/models/__init__.py
new file mode 100644
index 0000000..b16ec36
--- /dev/null
+++ b/src/models/__init__.py
@@ -0,0 +1,5 @@
+from src.models.album_meta import AlbumMeta
+from src.models.playlist_meta import PlaylistMeta
+from src.models.tracks_meta import TracksMeta
+from src.models.song_data import SongData
+from src.models.song_lyrics import SongLyrics
diff --git a/src/models/album_meta.py b/src/models/album_meta.py
new file mode 100644
index 0000000..dca36bf
--- /dev/null
+++ b/src/models/album_meta.py
@@ -0,0 +1,160 @@
+from __future__ import annotations
+
+from typing import List
+
+from pydantic import BaseModel, Field
+
+
+class Artwork(BaseModel):
+ width: int
+ url: str
+ height: int
+ textColor3: str
+ textColor2: str
+ textColor4: str
+ textColor1: str
+ bgColor: str
+ hasP3: bool
+
+
+class PlayParams(BaseModel):
+ id: str
+ kind: str
+
+
+class Attributes(BaseModel):
+ copyright: str
+ genreNames: List[str]
+ releaseDate: str
+ upc: str
+ isMasteredForItunes: bool
+ artwork: Artwork
+ url: str
+ playParams: PlayParams
+ recordLabel: str
+ isCompilation: bool
+ trackCount: int
+ isPrerelease: bool
+ audioTraits: List[str]
+ isSingle: bool
+ name: str
+ artistName: str
+ isComplete: bool
+
+
+class Artwork1(BaseModel):
+ width: int
+ url: str
+ height: int
+ textColor3: str
+ textColor2: str
+ textColor4: str
+ textColor1: str
+ bgColor: str
+ hasP3: bool
+
+
+class PlayParams1(BaseModel):
+ id: str
+ kind: str
+
+
+class Preview(BaseModel):
+ url: str
+
+
+class Attributes1(BaseModel):
+ hasTimeSyncedLyrics: bool
+ albumName: str
+ genreNames: List[str]
+ trackNumber: int
+ durationInMillis: int
+ releaseDate: str
+ isVocalAttenuationAllowed: bool
+ isMasteredForItunes: bool
+ isrc: str
+ artwork: Artwork1
+ composerName: str
+ audioLocale: str
+ playParams: PlayParams1
+ url: str
+ discNumber: int
+ hasCredits: bool
+ isAppleDigitalMaster: bool
+ hasLyrics: bool
+ audioTraits: List[str]
+ name: str
+ previews: List[Preview]
+ artistName: str
+
+
+class Attributes2(BaseModel):
+ name: str
+
+
+class Datum2(BaseModel):
+ id: str
+ type: str
+ href: str
+ attributes: Attributes2
+
+
+class Artists(BaseModel):
+ href: str
+ data: List[Datum2]
+
+
+class Relationships1(BaseModel):
+ artists: Artists
+
+
+class Datum1(BaseModel):
+ id: str
+ type: str
+ href: str
+ attributes: Attributes1
+ relationships: Relationships1
+
+
+class Tracks(BaseModel):
+ href: str
+ data: List[Datum1]
+
+
+class Attributes3(BaseModel):
+ name: str
+
+
+class Datum3(BaseModel):
+ id: str
+ type: str
+ href: str
+ attributes: Attributes3
+
+
+class Artists1(BaseModel):
+ href: str
+ data: List[Datum3]
+
+
+class RecordLabels(BaseModel):
+ href: str
+ data: List
+
+
+class Relationships(BaseModel):
+ tracks: Tracks
+ artists: Artists1
+ record_labels: RecordLabels = Field(..., alias='record-labels')
+
+
+class Datum(BaseModel):
+ id: str
+ type: str
+ href: str
+ attributes: Attributes
+ relationships: Relationships
+
+
+class AlbumMeta(BaseModel):
+ data: List[Datum]
diff --git a/src/models/playlist_meta.py b/src/models/playlist_meta.py
new file mode 100644
index 0000000..3d58837
--- /dev/null
+++ b/src/models/playlist_meta.py
@@ -0,0 +1,147 @@
+from __future__ import annotations
+
+from typing import List, Optional
+
+from pydantic import BaseModel
+
+
+class Description(BaseModel):
+ standard: str
+ short: str
+
+
+class Artwork(BaseModel):
+ width: int
+ url: str
+ height: int
+ textColor3: str
+ textColor2: str
+ textColor4: str
+ textColor1: str
+ bgColor: str
+ hasP3: bool
+
+
+class PlayParams(BaseModel):
+ id: str
+ kind: str
+ versionHash: str
+
+
+class EditorialNotes(BaseModel):
+ name: str
+ standard: str
+ short: str
+
+
+class Attributes(BaseModel):
+ lastModifiedDate: str
+ supportsSing: bool
+ description: Description
+ artwork: Artwork
+ playParams: PlayParams
+ url: str
+ hasCollaboration: bool
+ curatorName: str
+ audioTraits: List
+ name: str
+ isChart: bool
+ playlistType: str
+ editorialNotes: EditorialNotes
+ artistName: Optional[str] = None
+
+
+class Artwork1(BaseModel):
+ width: int
+ url: str
+ height: int
+ textColor3: str
+ textColor2: str
+ textColor4: str
+ textColor1: str
+ bgColor: str
+ hasP3: bool
+
+
+class PlayParams1(BaseModel):
+ id: str
+ kind: str
+
+
+class Preview(BaseModel):
+ url: str
+
+
+class Attributes1(BaseModel):
+ albumName: str
+ hasTimeSyncedLyrics: bool
+ genreNames: List[str]
+ trackNumber: int
+ releaseDate: str
+ durationInMillis: int
+ isVocalAttenuationAllowed: bool
+ isMasteredForItunes: bool
+ isrc: str
+ artwork: Artwork1
+ composerName: str
+ audioLocale: str
+ url: str
+ playParams: PlayParams1
+ discNumber: int
+ hasCredits: bool
+ hasLyrics: bool
+ isAppleDigitalMaster: bool
+ audioTraits: List[str]
+ name: str
+ previews: List[Preview]
+ artistName: str
+
+
+class Attributes2(BaseModel):
+ name: str
+
+
+class Datum2(BaseModel):
+ id: str
+ type: str
+ href: str
+ attributes: Attributes2
+
+
+class Artists(BaseModel):
+ href: str
+ data: List[Datum2]
+
+
+class Relationships1(BaseModel):
+ artists: Artists
+
+
+class Datum1(BaseModel):
+ id: str
+ type: str
+ href: str
+ attributes: Attributes1
+ relationships: Relationships1
+
+
+class Tracks(BaseModel):
+ href: str
+ next: Optional[str] = None
+ data: List[Datum1]
+
+
+class Relationships(BaseModel):
+ tracks: Tracks
+
+
+class Datum(BaseModel):
+ id: str
+ type: str
+ href: str
+ attributes: Attributes
+ relationships: Relationships
+
+
+class PlaylistMeta(BaseModel):
+ data: List[Datum]
diff --git a/src/models/song_data.py b/src/models/song_data.py
new file mode 100644
index 0000000..99f514b
--- /dev/null
+++ b/src/models/song_data.py
@@ -0,0 +1,137 @@
+from __future__ import annotations
+
+from typing import List
+
+from pydantic import BaseModel
+
+
+class Artwork(BaseModel):
+ width: int
+ url: str
+ height: int
+ textColor3: str
+ textColor2: str
+ textColor4: str
+ textColor1: str
+ bgColor: str
+ hasP3: bool
+
+
+class PlayParams(BaseModel):
+ id: str
+ kind: str
+
+
+class Preview(BaseModel):
+ url: str
+
+
+class ExtendedAssetUrls(BaseModel):
+ plus: str
+ lightweight: str
+ superLightweight: str
+ lightweightPlus: str
+ enhancedHls: str
+
+
+class Attributes(BaseModel):
+ hasTimeSyncedLyrics: bool
+ albumName: str
+ genreNames: List[str]
+ trackNumber: int
+ durationInMillis: int
+ releaseDate: str
+ isVocalAttenuationAllowed: bool
+ isMasteredForItunes: bool
+ isrc: str
+ artwork: Artwork
+ composerName: str
+ audioLocale: str
+ url: str
+ playParams: PlayParams
+ discNumber: int
+ hasCredits: bool
+ isAppleDigitalMaster: bool
+ hasLyrics: bool
+ audioTraits: List[str]
+ name: str
+ previews: List[Preview]
+ artistName: str
+ extendedAssetUrls: ExtendedAssetUrls
+
+
+class Artwork1(BaseModel):
+ width: int
+ url: str
+ height: int
+ textColor3: str
+ textColor2: str
+ textColor4: str
+ textColor1: str
+ bgColor: str
+ hasP3: bool
+
+
+class PlayParams1(BaseModel):
+ id: str
+ kind: str
+
+
+class Attributes1(BaseModel):
+ copyright: str
+ genreNames: List[str]
+ releaseDate: str
+ isMasteredForItunes: bool
+ upc: str
+ artwork: Artwork1
+ url: str
+ playParams: PlayParams1
+ recordLabel: str
+ isCompilation: bool
+ trackCount: int
+ isPrerelease: bool
+ audioTraits: List[str]
+ isSingle: bool
+ name: str
+ artistName: str
+ isComplete: bool
+
+
+class Datum1(BaseModel):
+ id: str
+ type: str
+ href: str
+ attributes: Attributes1
+
+
+class Albums(BaseModel):
+ href: str
+ data: List[Datum1]
+
+
+class Datum2(BaseModel):
+ id: str
+ type: str
+ href: str
+
+
+class Artists(BaseModel):
+ href: str
+ data: List[Datum2]
+
+
+class Relationships(BaseModel):
+ albums: Albums
+ artists: Artists
+
+
+class Datum(BaseModel):
+ id: str
+ type: str
+ href: str
+ attributes: Attributes
+ relationships: Relationships
+
+
+class SongData(BaseModel):
+ data: List[Datum]
diff --git a/src/models/song_lyrics.py b/src/models/song_lyrics.py
new file mode 100644
index 0000000..45b920c
--- /dev/null
+++ b/src/models/song_lyrics.py
@@ -0,0 +1,25 @@
+from typing import List
+
+from pydantic import BaseModel
+
+
+class PlayParams(BaseModel):
+ id: str
+ kind: str
+ catalogId: str
+ displayType: int
+
+
+class Attributes(BaseModel):
+ ttml: str
+ playParams: PlayParams
+
+
+class Datum(BaseModel):
+ id: str
+ type: str
+ attributes: Attributes
+
+
+class SongLyrics(BaseModel):
+ data: List[Datum]
diff --git a/src/models/tracks_meta.py b/src/models/tracks_meta.py
new file mode 100644
index 0000000..3987b31
--- /dev/null
+++ b/src/models/tracks_meta.py
@@ -0,0 +1,63 @@
+from __future__ import annotations
+
+from typing import List, Optional
+
+from pydantic import BaseModel
+
+
+class Artwork(BaseModel):
+ width: int
+ url: str
+ height: int
+ textColor3: str
+ textColor2: str
+ textColor4: str
+ textColor1: str
+ bgColor: str
+ hasP3: bool
+
+
+class PlayParams(BaseModel):
+ id: str
+ kind: str
+
+
+class Preview(BaseModel):
+ url: str
+
+
+class Attributes(BaseModel):
+ hasTimeSyncedLyrics: bool
+ albumName: str
+ genreNames: List[str]
+ trackNumber: int
+ releaseDate: str
+ durationInMillis: int
+ isVocalAttenuationAllowed: bool
+ isMasteredForItunes: bool
+ isrc: str
+ artwork: Artwork
+ composerName: Optional[str] = None
+ audioLocale: str
+ url: str
+ playParams: PlayParams
+ discNumber: int
+ hasCredits: bool
+ isAppleDigitalMaster: bool
+ hasLyrics: bool
+ audioTraits: List[str]
+ name: str
+ previews: List[Preview]
+ artistName: str
+
+
+class Datum(BaseModel):
+ id: str
+ type: str
+ href: str
+ attributes: Attributes
+
+
+class TracksMeta(BaseModel):
+ next: Optional[str] = None
+ data: List[Datum]
diff --git a/src/mp4.py b/src/mp4.py
new file mode 100644
index 0000000..b269873
--- /dev/null
+++ b/src/mp4.py
@@ -0,0 +1,165 @@
+import subprocess
+import uuid
+from io import BytesIO
+from pathlib import Path
+from tempfile import TemporaryDirectory
+from typing import Tuple
+
+import m3u8
+import regex
+from bs4 import BeautifulSoup
+
+from src.metadata import SongMetadata
+from src.types import *
+from src.utils import find_best_codec
+
+
+async def extract_media(m3u8_url: str, codec: str) -> Tuple[str, list[str], str]:
+ parsed_m3u8 = m3u8.load(m3u8_url)
+ specifyPlaylist = find_best_codec(parsed_m3u8, codec)
+ selected_codec = specifyPlaylist.media[0].group_id
+ if not specifyPlaylist:
+ raise
+ stream = m3u8.load(specifyPlaylist.absolute_uri)
+ skds = [key.uri for key in stream.keys if regex.match('(skd?://[^"]*)', key.uri)]
+ keys = [prefetchKey]
+ key_suffix = CodecKeySuffix.KeySuffixDefault
+ match codec:
+ case Codec.ALAC:
+ key_suffix = CodecKeySuffix.KeySuffixAlac
+ case Codec.EC3:
+ key_suffix = CodecKeySuffix.KeySuffixAtmos
+ case Codec.AAC:
+ key_suffix = CodecKeySuffix.KeySuffixAAC
+ case Codec.AAC_BINAURAL:
+ key_suffix = CodecKeySuffix.KeySuffixAACBinaural
+ case Codec.AAC_DOWNMIX:
+ key_suffix = CodecKeySuffix.KeySuffixAACDownmix
+ for key in skds:
+ if key.endswith(key_suffix) or key.endswith(CodecKeySuffix.KeySuffixDefault):
+ keys.append(key)
+ return stream.segment_map[0].absolute_uri, keys, selected_codec
+
+
+def extract_song(raw_song: bytes, codec: str) -> SongInfo:
+ tmp_dir = TemporaryDirectory()
+ mp4_name = uuid.uuid4().hex
+ raw_mp4 = Path(tmp_dir.name) / Path(f"{mp4_name}.mp4")
+ with open(raw_mp4.absolute(), "wb") as f:
+ f.write(raw_song)
+ nhml_name = (Path(tmp_dir.name) / Path(mp4_name).with_suffix('.nhml')).absolute()
+ media_name = (Path(tmp_dir.name) / Path(mp4_name).with_suffix('.media')).absolute()
+ subprocess.run(f"gpac -i {raw_mp4.absolute()} nhmlw:pckp=true -o {nhml_name}",
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ xml_name = (Path(tmp_dir.name) / Path(mp4_name).with_suffix('.xml')).absolute()
+ subprocess.run(f"mp4box -diso {raw_mp4.absolute()} -out {xml_name}",
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ decoder_params = None
+
+ with open(xml_name, "r") as f:
+ info_xml = BeautifulSoup(f.read(), "xml")
+ with open(nhml_name, "r") as f:
+ raw_nhml = f.read()
+ nhml = BeautifulSoup(raw_nhml, "xml")
+ with open(media_name, "rb") as f:
+ media = BytesIO(f.read())
+
+ if codec == Codec.ALAC:
+ alac_atom_name = (Path(tmp_dir.name) / Path(mp4_name).with_suffix('.atom')).absolute()
+ subprocess.run(f"mp4extract moov/trak/mdia/minf/stbl/stsd/enca[0]/alac {raw_mp4.absolute()} {alac_atom_name}",
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ with open(alac_atom_name, "rb") as f:
+ decoder_params = f.read()
+
+ samples = []
+ moofs = info_xml.find_all("MovieFragmentBox")
+ nhnt_sample_number = 0
+ nhnt_samples = {}
+ for sample in nhml.find_all("NHNTSample"):
+ nhnt_samples.update({int(sample.get("number")): sample})
+ for i, moof in enumerate(moofs):
+ tfhd = moof.TrackFragmentBox.TrackFragmentHeaderBox
+ index = 0 if not tfhd.get("SampleDescriptionIndex") else int(tfhd.get("SampleDescriptionIndex")) - 1
+ truns = moof.TrackFragmentBox.find_all("TrackRunBox")
+ for trun in truns:
+ for sample_number in range(int(trun.get("SampleCount"))):
+ nhnt_sample_number += 1
+ nhnt_sample = nhnt_samples[nhnt_sample_number]
+ sample_data = media.read(int(nhnt_sample.get("dataLength")))
+ duration = int(nhnt_sample.get("duration"))
+ samples.append(SampleInfo(descIndex=index, data=sample_data, duration=int(duration)))
+ tmp_dir.cleanup()
+ return SongInfo(codec=codec, raw=raw_song, samples=samples, nhml=raw_nhml, decoderParams=decoder_params)
+
+
+def encapsulate(song_info: SongInfo, decrypted_media: bytes, atmos_convent: bool) -> bytes:
+ tmp_dir = TemporaryDirectory()
+ name = uuid.uuid4().hex
+ media = Path(tmp_dir.name) / Path(name).with_suffix(".media")
+ with open(media.absolute(), "wb") as f:
+ f.write(decrypted_media)
+ if song_info.codec == Codec.EC3 and not atmos_convent:
+ song_name = Path(tmp_dir.name) / Path(name).with_suffix(".ec3")
+ else:
+ song_name = Path(tmp_dir.name) / Path(name).with_suffix(".m4a")
+ match song_info.codec:
+ case Codec.ALAC:
+ nhml_name = Path(tmp_dir.name) / Path(f"{name}.nhml")
+ with open(nhml_name.absolute(), "w", encoding="utf-8") as f:
+ nhml_xml = BeautifulSoup(song_info.nhml, features="xml")
+ nhml_xml.NHNTStream["baseMediaFile"] = media.name
+ f.write(str(nhml_xml))
+ subprocess.run(f"gpac -i {nhml_name.absolute()} nhmlr -o {song_name.absolute()}",
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ alac_params_atom_name = Path(tmp_dir.name) / Path(f"{name}.atom")
+ with open(alac_params_atom_name.absolute(), "wb") as f:
+ f.write(song_info.decoderParams)
+ final_m4a_name = Path(tmp_dir.name) / Path(f"{name}_final.m4a")
+ subprocess.run(
+ f"mp4edit --insert moov/trak/mdia/minf/stbl/stsd/alac:{alac_params_atom_name.absolute()} {song_name.absolute()} {final_m4a_name.absolute()}",
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ song_name = final_m4a_name
+ case Codec.EC3:
+ if not atmos_convent:
+ with open(song_name.absolute(), "wb") as f:
+ f.write(decrypted_media)
+ subprocess.run(f"gpac -i {media.absolute()} -o {song_name.absolute()}",
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ case Codec.AAC_BINAURAL | Codec.AAC_DOWNMIX | Codec.AAC:
+ nhml_name = Path(tmp_dir.name) / Path(f"{name}.nhml")
+ with open(nhml_name.absolute(), "w", encoding="utf-8") as f:
+ nhml_xml = BeautifulSoup(song_info.nhml, features="xml")
+ nhml_xml.NHNTStream["baseMediaFile"] = media.name
+ del nhml_xml.NHNTStream["streamType"]
+ del nhml_xml.NHNTStream["objectTypeIndication"]
+ del nhml_xml.NHNTStream["specificInfoFile"]
+ nhml_xml.NHNTStream["mediaType"] = "soun"
+ nhml_xml.NHNTStream["mediaSubType"] = "mp4a"
+ f.write(str(nhml_xml))
+ subprocess.run(f"gpac -i {nhml_name.absolute()} nhmlr -o {song_name.absolute()}",
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ with open(song_name.absolute(), "rb") as f:
+ final_song = f.read()
+ tmp_dir.cleanup()
+ return final_song
+
+
+def write_metadata(song: bytes, metadata: SongMetadata, embed_metadata: list[str], cover_format: str) -> bytes:
+ tmp_dir = TemporaryDirectory()
+ name = uuid.uuid4().hex
+ song_name = Path(tmp_dir.name) / Path(f"{name}.m4a")
+ with open(song_name.absolute(), "wb") as f:
+ f.write(song)
+ absolute_cover_path = ""
+ if "cover" in embed_metadata:
+ cover_path = Path(tmp_dir.name) / Path(f"cover.{cover_format}")
+ absolute_cover_path = cover_path.absolute()
+ with open(cover_path.absolute(), "wb") as f:
+ f.write(metadata.cover)
+ subprocess.run(["mp4box", "-time", "0", "-mtime", "0", "-keep-utc", "-name", f"1={metadata.title}", "-itags",
+ ":".join(["tool=\"\"", f"cover={absolute_cover_path}", metadata.to_itags_params(embed_metadata, cover_format)]),
+ song_name.absolute()], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ with open(song_name.absolute(), "rb") as f:
+ embed_song = f.read()
+ tmp_dir.cleanup()
+ return embed_song
diff --git a/src/rip.py b/src/rip.py
new file mode 100644
index 0000000..7138ca5
--- /dev/null
+++ b/src/rip.py
@@ -0,0 +1,61 @@
+import asyncio
+
+from loguru import logger
+
+from src.api import get_info_from_adam, get_song_lyrics, get_meta, download_song
+from src.config import Config, Device
+from src.decrypt import decrypt
+from src.metadata import SongMetadata
+from src.mp4 import extract_media, extract_song, encapsulate, write_metadata
+from src.save import save
+from src.types import GlobalAuthParams, Codec
+from src.url import Song, Album, URLType
+from src.utils import check_song_exists
+
+
+@logger.catch
+async def rip_song(song: Song, auth_params: GlobalAuthParams, codec: str, config: Config, device: Device,
+ force_save: bool = False):
+ logger.debug(f"Task of song id {song.id} was created")
+ token = auth_params.anonymousAccessToken
+ song_data = await get_info_from_adam(song.id, token, song.storefront)
+ song_metadata = SongMetadata.parse_from_song_data(song_data)
+ logger.info(f"Ripping song: {song_metadata.artist} - {song_metadata.title}")
+ if not force_save and check_song_exists(song_metadata, config.download, codec):
+ logger.info(f"Song: {song_metadata.artist} - {song_metadata.title} already exists")
+ return
+ await song_metadata.get_cover(config.download.coverFormat)
+ if song_data.attributes.hasTimeSyncedLyrics:
+ lyrics = await get_song_lyrics(song.id, song.storefront, auth_params.accountAccessToken,
+ auth_params.dsid, auth_params.accountToken)
+ song_metadata.lyrics = lyrics
+ song_uri, keys, selected_codec = await extract_media(song_data.attributes.extendedAssetUrls.enhancedHls, codec)
+ logger.info(f"Selected codec: {selected_codec} for song: {song_metadata.artist} - {song_metadata.title}")
+ logger.info(f"Downloading song: {song_metadata.artist} - {song_metadata.title}")
+ raw_song = await download_song(song_uri)
+ song_info = extract_song(raw_song, codec)
+ decrypted_song = await decrypt(song_info, keys, song_data, device)
+ song = encapsulate(song_info, decrypted_song, config.download.atmosConventToM4a)
+ if codec != Codec.EC3 or (codec == Codec.EC3 and config.download.atmosConventToM4a):
+ song = write_metadata(song, song_metadata, config.metadata.embedMetadata, config.download.coverFormat)
+ save(song, codec, song_metadata, config.download)
+ logger.info(f"Song {song_metadata.artist} - {song_metadata.title} saved!")
+
+
+async def rip_album(album: Album, auth_params: GlobalAuthParams, codec: str, config: Config, device: Device,
+ force_save: bool = False):
+ album_info = await get_meta(album.id, auth_params.anonymousAccessToken, album.storefront)
+ logger.info(f"Ripping Album: {album_info.data[0].attributes.artistName} - {album_info.data[0].attributes.name}")
+ async with asyncio.TaskGroup() as tg:
+ for track in album_info.data[0].relationships.tracks.data:
+ song = Song(id=track.id, storefront=album.storefront, url="", type=URLType.Song)
+ tg.create_task(rip_song(song, auth_params, codec, config, device, force_save))
+ logger.info(f"Album: {album_info.data[0].attributes.artistName} - {album_info.data[0].attributes.name} finished ripping")
+
+
+async def rip_playlist():
+ pass
+
+
+async def rip_artist():
+ pass
diff --git a/src/save.py b/src/save.py
new file mode 100644
index 0000000..75719b1
--- /dev/null
+++ b/src/save.py
@@ -0,0 +1,29 @@
+import os
+from pathlib import Path
+
+from src.config import Download
+from src.metadata import SongMetadata
+from src.types import Codec
+from src.utils import ttml_convent_to_lrc, get_valid_filename
+
+
+def save(song: bytes, codec: str, metadata: SongMetadata, config: Download):
+ song_name = get_valid_filename(config.songNameFormat.format(**metadata.model_dump()))
+ dir_path = Path(config.dirPathFormat.format(**metadata.model_dump()))
+ if not dir_path.exists() or not dir_path.is_dir():
+ os.makedirs(dir_path.absolute())
+ if codec == Codec.EC3 and not config.atmosConventToM4a:
+ song_path = dir_path / Path(song_name).with_suffix(".ec3")
+ else:
+ song_path = dir_path / Path(song_name).with_suffix(".m4a")
+ with open(song_path.absolute(), "wb") as f:
+ f.write(song)
+ if config.saveCover:
+ cover_path = dir_path / Path(f"cover.{config.coverFormat}")
+ with open(cover_path.absolute(), "wb") as f:
+ f.write(metadata.cover)
+ if config.saveLyrics and metadata.lyrics:
+ lrc_path = dir_path / Path(song_name).with_suffix(".lrc")
+ with open(lrc_path.absolute(), "w", encoding="utf-8") as f:
+ f.write(ttml_convent_to_lrc(metadata.lyrics))
+ return song_path.absolute()
\ No newline at end of file
diff --git a/src/types.py b/src/types.py
new file mode 100644
index 0000000..26a4791
--- /dev/null
+++ b/src/types.py
@@ -0,0 +1,68 @@
+from typing import Optional
+
+from pydantic import BaseModel
+
+defaultId = "0"
+prefetchKey = "skd://itunes.apple.com/P000000000/s1/e1"
+
+
+class SampleInfo(BaseModel):
+ data: bytes
+ duration: int
+ descIndex: int
+
+
+class SongInfo(BaseModel):
+ codec: str
+ raw: bytes
+ samples: list[SampleInfo]
+ nhml: str
+ decoderParams: Optional[bytes] = None
+
+
+class Codec:
+ ALAC = "alac"
+ EC3 = "ec3"
+ AAC_BINAURAL = "aac-binaural"
+ AAC_DOWNMIX = "aac-downmix"
+ AAC = "aac"
+
+
+class CodecKeySuffix:
+ KeySuffixAtmos = "c24"
+ KeySuffixAlac = "c23"
+ KeySuffixAAC = "c22"
+ KeySuffixAACDownmix = "c24"
+ KeySuffixAACBinaural = "c24"
+ KeySuffixDefault = "c6"
+
+
+class CodecRegex:
+ RegexCodecAtmos = "audio-atmos-\\d{4}$"
+ RegexCodecAlac = "audio-alac-stereo-\\d{5}-\\d{2}$"
+ RegexCodecBinaural = "audio-stereo-\\d{3}-binaural$"
+ RegexCodecDownmix = "audio-stereo-\\d{3}-downmix$"
+ RegexCodecAAC = "audio-stereo-\\d{3}$"
+
+ @classmethod
+ def get_pattern_by_codec(cls, codec: str):
+ codec_pattern_mapping = {Codec.ALAC: cls.RegexCodecAlac, Codec.EC3: cls.RegexCodecAtmos,
+ Codec.AAC_DOWNMIX: cls.RegexCodecDownmix, Codec.AAC_BINAURAL: cls.RegexCodecBinaural,
+ Codec.AAC: cls.RegexCodecAAC}
+ return codec_pattern_mapping.get(codec)
+
+
+class AuthParams(BaseModel):
+ dsid: str
+ accountToken: str
+ accountAccessToken: str
+ storefront: str
+
+
+class GlobalAuthParams(AuthParams):
+ anonymousAccessToken: str
+
+ @classmethod
+ def from_auth_params_and_token(cls, auth_params: AuthParams, token: str):
+ return cls(dsid=auth_params.dsid, accountToken=auth_params.accountToken, anonymousAccessToken=token,
+ accountAccessToken=auth_params.accountAccessToken, storefront=auth_params.storefront)
\ No newline at end of file
diff --git a/src/url.py b/src/url.py
new file mode 100644
index 0000000..1dbc6d4
--- /dev/null
+++ b/src/url.py
@@ -0,0 +1,62 @@
+from urllib.parse import urlparse, parse_qs
+
+from pydantic import BaseModel
+
+
+class URLType:
+ Song = "song"
+ Album = "album"
+ Playlist = "playlist"
+ Artist = "artist"
+
+
+class AppleMusicURL(BaseModel):
+ url: str
+ storefront: str
+ type: str
+ id: str
+
+ @classmethod
+ def parse_url(cls, url: str):
+ parsed_url = urlparse(url)
+ paths = parsed_url.path.split("/")
+ storefront = paths[1]
+ url_type = paths[2]
+ match url_type:
+ case URLType.Song:
+ url_id = paths[4]
+ return Song(url=url, storefront=storefront, id=url_id, type=URLType.Song)
+ case URLType.Album:
+ if not parsed_url.query:
+ url_id = paths[4]
+ return Album(url=url, storefront=storefront, id=url_id, type=URLType.Album)
+ else:
+ url_query = parse_qs(parsed_url.query)
+ if url_query.get("i"):
+ url_id = url_query.get("i")[0]
+ return Song(url=url, storefront=storefront, id=url_id, type=URLType.Song)
+ else:
+ url_id = paths[4]
+ return Album(url=url, storefront=storefront, id=url_id, type=URLType.Album)
+ case URLType.Artist:
+ url_id = paths[4]
+ return Artist(url=url, storefront=storefront, id=url_id, type=URLType.Artist)
+ case URLType.Playlist:
+ url_id = paths[4]
+ return Playlist(url=url, storefront=storefront, id=url_id, type=URLType.Playlist)
+
+
+class Song(AppleMusicURL):
+ ...
+
+
+class Album(AppleMusicURL):
+ ...
+
+
+class Playlist(AppleMusicURL):
+ ...
+
+
+class Artist(AppleMusicURL):
+ ...
diff --git a/src/utils.py b/src/utils.py
new file mode 100644
index 0000000..29b4b0e
--- /dev/null
+++ b/src/utils.py
@@ -0,0 +1,117 @@
+import asyncio
+import time
+from itertools import islice
+from pathlib import Path
+
+import m3u8
+import regex
+from bs4 import BeautifulSoup
+
+from src.config import Download
+from src.exceptions import NotTimeSyncedLyricsException
+
+from src.types import *
+
+
+def check_url(url):
+ pattern = regex.compile(
+ r'^(?:https:\/\/(?:beta\.music|music)\.apple\.com\/(\w{2})(?:\/album|\/album\/.+))\/(?:id)?(\d[^\D]+)(?:$|\?)')
+ result = regex.findall(pattern, url)
+ return result[0][0], result[0][1]
+
+
+def check_playlist_url(url):
+ pattern = regex.compile(
+ r'^(?:https:\/\/(?:beta\.music|music)\.apple\.com\/(\w{2})(?:\/playlist|\/playlist\/.+))\/(?:id)?(pl\.[\w-]+)(?:$|\?)')
+ result = regex.findall(pattern, url)
+ return result[0][0], result[0][1]
+
+
+def byte_length(i):
+ return (i.bit_length() + 7) // 8
+
+
+def find_best_codec(parsed_m3u8: m3u8.M3U8, codec: str) -> Optional[m3u8.Playlist]:
+ available_medias = [playlist for playlist in parsed_m3u8.playlists
+ if regex.match(CodecRegex.get_pattern_by_codec(codec), playlist.stream_info.audio)]
+ if not available_medias:
+ return None
+ available_medias.sort(key=lambda x: x.stream_info.average_bandwidth, reverse=True)
+ return available_medias[0]
+
+
+def chunk(it, size):
+ it = iter(it)
+ return iter(lambda: tuple(islice(it, size)), ())
+
+
+def timeit(func):
+ async def process(func, *args, **params):
+ if asyncio.iscoroutinefunction(func):
+ print('this function is a coroutine: {}'.format(func.__name__))
+ return await func(*args, **params)
+ else:
+ print('this is not a coroutine')
+ return func(*args, **params)
+
+ async def helper(*args, **params):
+ print('{}.time'.format(func.__name__))
+ start = time.time()
+ result = await process(func, *args, **params)
+
+ # Test normal function route...
+ # result = await process(lambda *a, **p: print(*a, **p), *args, **params)
+
+ print('>>>', time.time() - start)
+ return result
+
+ return helper
+
+
+def get_digit_from_string(text: str) -> int:
+ return int(''.join(filter(str.isdigit, text)))
+
+
+def ttml_convent_to_lrc(ttml: str) -> str:
+ b = BeautifulSoup(ttml, features="xml")
+ lrc_lines = []
+ for item in b.tt.body.children:
+ for lyric in item.children:
+ h, m, s, ms = 0, 0, 0, 0
+ lyric_time: str = lyric.get("begin")
+ if not lyric_time:
+ raise NotTimeSyncedLyricsException
+ match lyric_time.count(":"):
+ case 0:
+ split_time = lyric_time.split(".")
+ s, ms = get_digit_from_string(split_time[0]), get_digit_from_string(split_time[1])
+ case 1:
+ split_time = lyric_time.split(":")
+ s_ms = split_time[-1]
+ del split_time[-1]
+ split_time.extend(s_ms.split("."))
+ m, s, ms = (get_digit_from_string(split_time[0]), get_digit_from_string(split_time[1]),
+ get_digit_from_string(split_time[2]))
+ case 2:
+ split_time = lyric_time.split(":")
+ s_ms = split_time[-1]
+ del split_time[-1]
+ split_time.extend(s_ms.split("."))
+ h, m, s, ms = (get_digit_from_string(split_time[0]), get_digit_from_string(split_time[1]),
+ get_digit_from_string(split_time[2]), get_digit_from_string(split_time[3]))
+ lrc_lines.append(
+ f"[{str(m + h * 60).rjust(2, '0')}:{str(s).rjust(2, '0')}.{str(int(ms / 10)).rjust(2, '0')}]{lyric.text}")
+ return "\n".join(lrc_lines)
+
+
+def check_song_exists(metadata, config: Download, codec: str):
+ song_name = get_valid_filename(config.songNameFormat.format(**metadata.model_dump()))
+ dir_path = Path(config.dirPathFormat.format(**metadata.model_dump()))
+ if not config.atmosConventToM4a and codec == Codec.EC3:
+ return (Path(dir_path) / Path(song_name).with_suffix(".ec3")).exists()
+ else:
+ return (Path(dir_path) / Path(song_name).with_suffix(".m4a")).exists()
+
+
+def get_valid_filename(filename: str):
+ return "".join(i for i in filename if i not in "\/:*?<>|")