commit 6d365210055d4b460d1c3c3596ed1d41a4605a8c Author: WorldObservationLog Date: Sat May 4 15:58:59 2024 +0800 Initial commit 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 "\/:*?<>|")