From 1a878b536bd4aff22f972976620ab4b48770465b Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Mon, 11 Oct 2021 02:12:22 +0200 Subject: [PATCH] Add initial files --- .github/workflows/rust.yml | 24 ++ .gitignore | 4 + .vscode/launch.json | 47 +++ Cargo.toml | 45 ++ LICENSE | 674 ++++++++++++++++++++++++++++++ README.md | 59 +++ assets/icon.afdesign | Bin 0 -> 63255 bytes assets/icon.ico | Bin 0 -> 21599 bytes assets/icon.svg | 1 + build.rs | 9 + mp3lame.lib | Bin 0 -> 52020 bytes src/converter.rs | 160 ++++++++ src/downloader.rs | 811 +++++++++++++++++++++++++++++++++++++ src/error.rs | 134 ++++++ src/main.rs | 161 ++++++++ src/settings.rs | 62 +++ src/spotify.rs | 168 ++++++++ src/tag/id3.rs | 85 ++++ src/tag/mod.rs | 57 +++ src/tag/ogg.rs | 68 ++++ 20 files changed, 2569 insertions(+) create mode 100644 .github/workflows/rust.yml create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 assets/icon.afdesign create mode 100644 assets/icon.ico create mode 100644 assets/icon.svg create mode 100644 build.rs create mode 100644 mp3lame.lib create mode 100644 src/converter.rs create mode 100644 src/downloader.rs create mode 100644 src/error.rs create mode 100644 src/main.rs create mode 100644 src/settings.rs create mode 100644 src/spotify.rs create mode 100644 src/tag/id3.rs create mode 100644 src/tag/mod.rs create mode 100644 src/tag/ogg.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..4886b5a --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,24 @@ +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +name: "Build project" + +env: + CARGO_TERM_COLOR: always + +jobs: + build_and_test: + name: DownOnSpot + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - run: sudo apt install -y libasound2-dev libmp3lame-dev + - uses: actions-rs/toolchain@v1 + with: + override: true + toolchain: nightly + - run: cargo build --release --all-features \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e8c7448 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +settings.json +Cargo.lock +libmp3lame.dll \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..fd02e6b --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,47 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'down_on_spot'", + "cargo": { + "args": [ + "build", + "--bin=down_on_spot", + "--package=down_on_spot" + ], + "filter": { + "name": "down_on_spot", + "kind": "bin" + } + }, + "args": [ + "https://open.spotify.com/track/2Ju1xUOXSS1C6GOvlTHXUp?si=60e759a084d2470d" + ], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'down_on_spot'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=down_on_spot", + "--package=down_on_spot" + ], + "filter": { + "name": "down_on_spot", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c0af5a3 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,45 @@ +cargo-features = ["strip"] + +[profile.release] +strip = true +opt-level = "z" +lto = true +codegen-units = 1 +panic = "abort" + +[package] +name = "down_on_spot" +version = "0.0.1" +edition = "2018" +authors = ["exttex", "oSumAtrIX"] +build = "build.rs" + +[target.'cfg(windows)'.build-dependencies] +winres = "0.1.12" + +[dependencies] +log = "0.4.14" +url = "2.2" +id3 = "0.6" +dirs = "3.0" +chrono = "0.4" +lewton = "0.10" +futures = "0.3" +reqwest = "0.11" +colored = "2" +lame = "0.1.3" +aspotify = "0.7" +librespot = "0.2" +async-std = { version = "1.10.0", features = ["attributes", "tokio1"] } +serde_json = "1.0" +async-stream = "0.3" +oggvorbismeta = "0.1" +sanitize-filename = "0.3" +serde = { version = "1.0.130", features = ["derive"] } +tokio = { version = "1.12", features = ["fs"] } + +[package.metadata.winres] +OriginalFilename = "DownOnSpot.exe" +FileDescription = "Download songs from spotify with rust" +ProductName = "DownOnSpot" +ProductVersion = "0.0.1" \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6ceba7a --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +
+ +# DownOnSpot + +### A Spotify downloader written in Rust + +drawing +
+ +## Disclaimer + +```text +DownOnSpot was not developed for piracy, but educational and private usage. +It may be illegal to use this in your country! +I am not responsible in any way for the usage of this app. +``` + +## Features + +- Multi-threaded +- Download 320 kbit/s audio from spotify, directly +- Download playlists and albums +- Convert to mp3 +- Simple usage over CLI + +## Building + +To build this project you will need `Nightly Rust`. You can install it by following [rustup.rs](https://rustup.rs) instructions. + +```bash +git clone https://github.com/oSumAtrIX/DownOnSpot.git +cd DownOnSpot +cargo build --release +``` + +If you get a linker error, you might need to download the [standard libmp3lame](https://www.rarewares.org/mp3-lame-libraries.php#libmp3lame) library. + +## Usage/Examples + +To install and use DownOnSpot, edit the configuration file which is being created in the same directory as your shell on first launch. + +```bash +$ down_on_spot.exe +Settings could not be loaded, because of the following error: IO: NotFound No such file or directory. (os error 2)... +..but default settings have been created successfully. Edit them and run the program again. + +$ down_on_spot.exe +Usage: +down_on_spot.exe (track_url | album_url | playlist_url) +``` + +## Authors + +- [@oSumAtrIX](https://osumatrix.me/#github) +- [@exttex](https://git.freezer.life/exttex) + +## License + +[GPL3](https://choosealicense.com/licenses/agpl-3.0/) diff --git a/assets/icon.afdesign b/assets/icon.afdesign new file mode 100644 index 0000000000000000000000000000000000000000..bcf991f2766c1cdd065f4d4325ad248affcd53b6 GIT binary patch literal 63255 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 Y|8{DDMvfgxFaQ7m07*qoM6N<$f?-nIbpQYW literal 0 HcmV?d00001 diff --git a/assets/icon.svg b/assets/icon.svg new file mode 100644 index 0000000..06607d0 --- /dev/null +++ b/assets/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..f6a0d28 --- /dev/null +++ b/build.rs @@ -0,0 +1,9 @@ +fn main() { + #[cfg(windows)] + { + winres::WindowsResource::new() + .set_icon("assets/icon.ico") + .compile() + .unwrap(); + } +} diff --git a/mp3lame.lib b/mp3lame.lib new file mode 100644 index 0000000000000000000000000000000000000000..7824f1ba2f7b4281b056c091c7751429171f8cda GIT binary patch literal 52020 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 X#+4Zp`C6;O6Hfdc$xd{D)V%)%HXUKV literal 0 HcmV?d00001 diff --git a/src/converter.rs b/src/converter.rs new file mode 100644 index 0000000..08563a7 --- /dev/null +++ b/src/converter.rs @@ -0,0 +1,160 @@ +use lewton::inside_ogg::OggStreamReader; +use std::io::{Error, ErrorKind, Read, Seek}; + +use crate::downloader::{AudioFormat, Quality}; +use crate::error::SpotifyError; +use crate::error::SpotifyError::{LameConverterError, InvalidFormat}; + +/// Converts audio to MP3 +pub enum AudioConverter { + OGG { + decoder: OggStreamReader, + lame: lame::Lame, + lame_end: bool, + }, +} + +unsafe impl Send for AudioConverter {} + +impl AudioConverter { + /// Wrap reader + pub fn new( + read: Box<(dyn Read + Send + 'static)>, + format: AudioFormat, + quality: Quality, + ) -> Result { + // Create encoder + let bitrate = match quality { + Quality::Q320 => 320, + Quality::Q256 => 256, + Quality::Q160 => 160, + Quality::Q96 => 96, + }; + + let mut lame = lame::Lame::new().unwrap(); + + match lame.set_channels(2) { + Ok(_) => {} + Err(_) => return Err(LameConverterError("Channels".to_string())) + }; + + match lame.set_quality(0) { + Ok(_) => {} + Err(_) => return Err(LameConverterError("Quality".to_string())) + }; + match lame.set_kilobitrate(bitrate) { + Ok(_) => {} + Err(_) => return Err(LameConverterError("Bitrate".to_string())) + }; + + match format { + AudioFormat::AAC => todo!(), + AudioFormat::MP4 => todo!(), + // Lewton decoder + AudioFormat::OGG => { + let decoder = OggStreamReader::new(ReadWrap::new(Box::new(read)))?; + let sample_rate = decoder.ident_hdr.audio_sample_rate; + // Init lame + match lame.set_sample_rate(sample_rate) { + Ok(_) => {} + Err(_) => return Err(LameConverterError("Sample rate".to_string())) + }; + match lame.init_params() { + Ok(_) => {} + Err(_) => return Err(LameConverterError("Init".to_string())) + }; + + Ok(AudioConverter::OGG { + lame, + decoder, + lame_end: false, + }) + } + AudioFormat::MP3 => panic!("No reencoding allowd!"), + _ => Err(InvalidFormat), + } + } +} + +impl Read for AudioConverter { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + match self { + AudioConverter::OGG { + decoder, + lame, + lame_end, + } => { + match decoder.read_dec_packet() { + Ok(packet) => match packet { + Some(data) => { + // 0 sized packets aren't EOF + if data[0].is_empty() { + return self.read(buf); + } + + let result = match lame.encode(&data[0], &data[1], buf) { + Ok(size) => { + if size == 0 { + return self.read(buf); + } + size + } + Err(_e) => { + drop(lame); + return Err(Error::new( + ErrorKind::InvalidData, + format!("Lame error: {:?}", _e), + )); + } + }; + Ok(result as usize) + } + None => { + if *lame_end { + return Ok(0); + } + // Flush buffer + drop(lame); + *lame_end = true; + Ok(0) + } + }, + Err(e) => { + // Close lame + if !*lame_end { + drop(lame); + *lame_end = true; + } + warn!("Lawton error: {}, calling EOF", e); + Ok(0) + } + } + } + } + } +} + +pub struct ReadWrap { + source: Box<(dyn Read + Send + 'static)>, +} + +impl ReadWrap { + pub fn new(read: Box<(dyn Read + Send + 'static)>) -> ReadWrap { + ReadWrap { + source: Box::new(read), + } + } +} + +impl Read for ReadWrap { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.source.read(buf) + } +} + +/// Fake seek for Rodio +impl Seek for ReadWrap { + fn seek(&mut self, _pos: std::io::SeekFrom) -> std::io::Result { + Ok(0) + } +} diff --git a/src/downloader.rs b/src/downloader.rs new file mode 100644 index 0000000..b238bac --- /dev/null +++ b/src/downloader.rs @@ -0,0 +1,811 @@ +use async_std::channel::{bounded, Receiver, Sender}; +use async_stream::try_stream; +use chrono::NaiveDate; +use futures::stream::FuturesUnordered; +use futures::{pin_mut, select, FutureExt, Stream, StreamExt}; +use librespot::audio::{AudioDecrypt, AudioFile}; +use librespot::core::audio_key::AudioKey; +use librespot::core::session::Session; +use librespot::core::spotify_id::SpotifyId; +use librespot::metadata::{FileFormat, Metadata, Track}; +use sanitize_filename::sanitize; +use serde::{Deserialize, Serialize}; +use std::io::Read; +use std::path::{Path, PathBuf}; +use tokio::fs::File; +use tokio::io::AsyncWriteExt; + +use crate::converter::AudioConverter; +use crate::error::SpotifyError; +use crate::spotify::{Spotify, SpotifyItem}; +use crate::tag::{Field, TagWrap}; + +/// Wrapper for use with UI +#[derive(Debug, Clone)] +pub struct Downloader { + rx: Receiver, + tx: Sender, + + spotify: Spotify, +} +impl Downloader { + /// Create new instance + pub fn new(config: DownloaderConfig, spotify: Spotify) -> Downloader { + let (tx_0, rx_0) = bounded(1); + let (tx_1, rx_1) = bounded(1); + + let tx_clone = tx_1.clone(); + let spotify_clone = spotify.clone(); + tokio::spawn(async move { + communication_thread(config, spotify_clone, rx_1, tx_0, tx_clone).await + }); + Downloader { + rx: rx_0, + tx: tx_1, + spotify, + } + } + /// Add item to download queue + pub async fn add_to_queue(&self, download: Download) { + self.tx + .send(Message::AddToQueue(vec![download])) + .await + .unwrap(); + } + + /// Add multiple items to queue + pub async fn add_to_queue_multiple(&self, downloads: Vec) { + self.tx.send(Message::AddToQueue(downloads)).await.unwrap(); + } + + /// Add URL or URI to queue + pub async fn add_uri(&self, uri: &str) -> Result<(), SpotifyError> { + let uri = Spotify::parse_uri(uri)?; + let item = self.spotify.resolve_uri(&uri).await?; + match item { + SpotifyItem::Track(t) => self.add_to_queue(t.into()).await, + SpotifyItem::Album(a) => { + let tracks = self.spotify.full_album(&a.id).await?; + let queue: Vec = tracks.into_iter().map(|t| t.into()).collect(); + self.add_to_queue_multiple(queue).await; + } + SpotifyItem::Playlist(p) => { + let tracks = self.spotify.full_playlist(&p.id).await?; + let queue: Vec = tracks.into_iter().map(|t| t.into()).collect(); + self.add_to_queue_multiple(queue).await; + } + // Unsupported + SpotifyItem::Other(u) => { + error!("Unsupported URI: {}", u); + return Err(SpotifyError::Unavailable); + } + }; + Ok(()) + } + + /// Get all downloads + pub async fn get_downloads(&self) -> Vec { + self.tx.send(Message::GetDownloads).await.unwrap(); + let Response::Downloads(d) = self.rx.recv().await.unwrap(); + d + } +} + +async fn communication_thread( + config: DownloaderConfig, + spotify: Spotify, + rx: Receiver, + tx: Sender, + self_tx: Sender, +) { + // Downloader + let downloader = DownloaderInternal::new(spotify.clone(), self_tx.clone()); + let downloader_tx = downloader.tx.clone(); + tokio::spawn(async move { + downloader.download_loop().await; + }); + let mut waiting_for_job = false; + let mut queue: Vec = vec![]; + + // Receive messages + while let Ok(msg) = rx.recv().await { + match msg { + // Send job to worker thread + Message::GetJob => { + if let Some(d) = queue.iter_mut().find(|i| i.state == DownloadState::None) { + d.state = DownloadState::Lock; + downloader_tx + .send(DownloaderMessage::Job(d.clone().into(), config.clone())) + .await + .unwrap(); + waiting_for_job = false; + } else { + waiting_for_job = true; + } + } + // Update state of download + Message::UpdateState(id, state) => { + let i = queue.iter().position(|i| i.id == id).unwrap(); + queue[i].state = state.clone(); + if state == DownloadState::Done { + queue.remove(i); + } + } + Message::AddToQueue(download) => { + // Assign new IDs and reset state + let mut id = queue.iter().map(|i| i.id).max().unwrap_or(0); + let downloads: Vec = download + .into_iter() + .map(|mut d| { + d.id = id; + d.state = DownloadState::None; + id += 1; + d + }) + .collect(); + queue.extend(downloads); + // Update worker threads if locked + if waiting_for_job { + let d = queue + .iter_mut() + .find(|i| i.state == DownloadState::None) + .unwrap(); + d.state = DownloadState::Lock; + downloader_tx + .send(DownloaderMessage::Job(d.clone().into(), config.clone())) + .await + .unwrap(); + waiting_for_job = false; + } + } + Message::GetDownloads => { + tx.send(Response::Downloads(queue.clone())).await.ok(); + } + } + } +} + +/// Spotify downloader +pub struct DownloaderInternal { + spotify: Spotify, + pub tx: Sender, + rx: Receiver, + event_tx: Sender, +} + +pub enum DownloaderMessage { + Job(DownloadJob, DownloaderConfig), +} + +impl DownloaderInternal { + /// Create new instance + pub fn new(spotify: Spotify, event_tx: Sender) -> DownloaderInternal { + let (tx, rx) = bounded(1); + DownloaderInternal { + spotify, + tx, + rx, + event_tx, + } + } + + /// Downloader loop + pub async fn download_loop(&self) { + let mut queue = vec![]; + let mut tasks = FuturesUnordered::new(); + let mut job_future = Box::pin(self.get_job()).fuse(); + + loop { + select! { + job = job_future => { + if let Some((job, config)) = job { + if tasks.len() < config.concurrent_downloads { + tasks.push(self.download_job_wrapper(job.clone(), config).boxed()) + } else { + queue.push((job, config)); + } + } + job_future = Box::pin(self.get_job()).fuse(); + }, + // Task finished + () = tasks.select_next_some() => { + if let Some((job, config)) = queue.first() { + tasks.push(self.download_job_wrapper(job.clone(), config.clone()).boxed()); + queue.remove(0); + } + } + }; + } + } + + // Get job from parent + async fn get_job(&self) -> Option<(DownloadJob, DownloaderConfig)> { + self.event_tx.send(Message::GetJob).await.unwrap(); + match self.rx.recv().await.ok()? { + DownloaderMessage::Job(job, config) => Some((job, config)), + } + } + + /// Wrapper for download_job for error handling + async fn download_job_wrapper(&self, job: DownloadJob, config: DownloaderConfig) { + let track_id = job.track_id.clone(); + let id = job.id; + match self.download_job(job, config).await { + Ok(_) => {} + Err(e) => { + error!("Download job for track {} failed. {}", track_id, e); + self.event_tx + .send(Message::UpdateState( + id, + DownloadState::Error(e.to_string()), + )) + .await + .unwrap(); + } + } + } + + // Wrapper for downloading and tagging + async fn download_job( + &self, + job: DownloadJob, + config: DownloaderConfig, + ) -> Result<(), SpotifyError> { + // Fetch metadata + let track = self + .spotify + .spotify + .tracks() + .get_track(&job.track_id, None) + .await? + .data; + let album = self + .spotify + .spotify + .albums() + .get_album(&track.album.id.ok_or(SpotifyError::Unavailable)?, None) + .await? + .data; + // Generate path + let mut filename = config.filename_template.to_owned(); + let tags: Vec<(&str, String)> = vec![ + ("%title%", sanitize(&track.name)), + ( + "%artist%", + sanitize( + &track + .artists + .iter() + .map(|a| a.name.as_str()) + .collect::>() + .first() + .unwrap_or(&""), + ), + ), + ( + "%artists%", + sanitize( + &track + .artists + .iter() + .map(|a| a.name.as_str()) + .collect::>() + .join(", "), + ), + ), + ("%track%", track.track_number.to_string()), + ("%0track%", format!("{:02}", track.track_number)), + ("%disc%", track.disc_number.to_string()), + ("%0disc%", format!("{:02}", track.disc_number)), + ("%id%", job.track_id.to_string()), + ("%album%", sanitize(&track.album.name)), + ( + "%albumArtist%", + sanitize( + &track + .album + .artists + .iter() + .map(|a| a.name.as_str()) + .collect::>() + .first() + .unwrap_or(&""), + ), + ), + ( + "%albumArtists%", + sanitize( + &track + .album + .artists + .iter() + .map(|a| a.name.as_str()) + .collect::>() + .join(", "), + ), + ), + ]; + for (t, v) in tags { + filename = filename.replace(t, &v); + } + let path = config.path.join(filename); + tokio::fs::create_dir_all(path.parent().unwrap()).await?; + + // Download + let (path, format) = DownloaderInternal::download_track( + &self.spotify.session, + &job.track_id, + path, + config.clone(), + self.event_tx.clone(), + job.id, + ) + .await?; + // Post processing + self.event_tx + .send(Message::UpdateState(job.id, DownloadState::Post)) + .await + .ok(); + + // Download cover + let mut cover = None; + if let Some(image) = track.album.images.first() { + match DownloaderInternal::download_cover(&image.url).await { + Ok(c) => cover = Some(c), + Err(e) => warn!("Failed downloading cover! {}", e), + } + } + + let tags = vec![ + (Field::Title, vec![track.name.to_string()]), + (Field::Album, vec![track.album.name.to_string()]), + ( + Field::Artist, + track + .artists + .iter() + .map(|a| a.name.to_string()) + .collect::>(), + ), + ( + Field::AlbumArtist, + track + .album + .artists + .iter() + .map(|a| a.name.to_string()) + .collect::>(), + ), + (Field::TrackNumber, vec![track.track_number.to_string()]), + (Field::DiscNumber, vec![track.disc_number.to_string()]), + (Field::Genre, album.genres.clone()), + (Field::Label, vec![album.label.to_string()]), + ]; + let date = album.release_date; + // Write tags + let config = config.clone(); + tokio::task::spawn_blocking(move || { + DownloaderInternal::write_tags(path, format, tags, date, cover, config) + }) + .await??; + + // Done + self.event_tx + .send(Message::UpdateState(job.id, DownloadState::Done)) + .await + .ok(); + Ok(()) + } + + /// Download cover, returns mime and data + async fn download_cover(url: &str) -> Result<(String, Vec), SpotifyError> { + let res = reqwest::get(url).await?; + let mime = res + .headers() + .get("content-type") + .ok_or(SpotifyError::Error("Missing cover mime!".into()))? + .to_str() + .unwrap() + .to_string(); + let data = res.bytes().await?.to_vec(); + Ok((mime, data)) + } + + /// Write tags to file ( BLOCKING ) + fn write_tags( + path: impl AsRef, + format: AudioFormat, + tags: Vec<(Field, Vec)>, + date: NaiveDate, + cover: Option<(String, Vec)>, + config: DownloaderConfig, + ) -> Result<(), SpotifyError> { + let mut tag_wrap = TagWrap::new(path, format)?; + // Format specific + match &mut tag_wrap { + TagWrap::ID3(id3) => id3.use_id3_v24(config.id3v24), + _ => {} + } + + let tag = tag_wrap.get_tag(); + tag.set_separator(&config.separator); + for (field, value) in tags { + tag.set_field(field, value); + } + tag.set_release_date(date); + // Cover + if let Some((mime, data)) = cover { + tag.add_cover(&mime, data); + } + tag.save()?; + Ok(()) + } + + /// Download track by id + async fn download_track( + session: &Session, + id: &str, + path: impl AsRef, + config: DownloaderConfig, + tx: Sender, + job_id: i64, + ) -> Result<(PathBuf, AudioFormat), SpotifyError> { + let id = SpotifyId::from_base62(id)?; + let track = Track::get(session, id).await?; + // Fallback if unavailable + if !track.available { + for alt in track.alternatives { + let t = Track::get(session, alt).await?; + if t.available { + break; + } + } + return Err(SpotifyError::Unavailable); + } + // Quality fallback + let mut quality = config.quality; + let (mut file_id, mut file_format) = (None, None); + 'outer: loop { + for format in quality.get_file_formats() { + if let Some(f) = track.files.get(&format) { + info!("{} Using {:?} format.", id.to_base62(), format); + file_id = Some(f); + file_format = Some(format); + break 'outer; + } + } + // Fallback to worser quality + match quality.fallback() { + Some(q) => quality = q, + None => break, + } + warn!("{} Falling back to: {:?}", id.to_base62(), quality); + } + let file_id = file_id.ok_or(SpotifyError::Unavailable)?; + let file_format = file_format.unwrap(); + + // Path with extension + let mut audio_format: AudioFormat = file_format.into(); + let path = format!( + "{}.{}", + path.as_ref().to_str().unwrap(), + match config.convert_to_mp3 { + true => "mp3".to_string(), + false => audio_format.extension(), + } + ); + let path = Path::new(&path).to_owned(); + let path_clone = path.clone(); + + let key = session.audio_key().request(track.id, *file_id).await?; + let encrypted = AudioFile::open(session, *file_id, 1024 * 1024, true).await?; + let size = encrypted.get_stream_loader_controller().len(); + // Download + let s = match config.convert_to_mp3 { + true => { + let s = DownloaderInternal::download_track_convert_stream( + path_clone, + encrypted, + key, + audio_format.clone(), + quality, + ) + .boxed(); + audio_format = AudioFormat::MP3; + s + } + false => DownloaderInternal::download_track_stream(path_clone, encrypted, key).boxed(), + }; + pin_mut!(s); + // Read progress + let mut read = 0; + while let Some(result) = s.next().await { + match result { + Ok(r) => { + read += r; + tx.send(Message::UpdateState( + job_id, + DownloadState::Downloading(read, size), + )) + .await + .ok(); + } + Err(e) => { + tokio::fs::remove_file(path).await.ok(); + return Err(e); + } + } + } + + info!("Done downloading: {}", track.id.to_base62()); + Ok((path, audio_format)) + } + + fn download_track_stream( + path: impl AsRef, + encrypted: AudioFile, + key: AudioKey, + ) -> impl Stream> { + try_stream! { + let mut file = File::create(path).await?; + let mut decrypted = AudioDecrypt::new(key, encrypted); + // Skip (i guess encrypted shit) + let mut skip: [u8; 0xa7] = [0; 0xa7]; + let mut decrypted = tokio::task::spawn_blocking(move || { + match decrypted.read_exact(&mut skip) { + Ok(_) => Ok(decrypted), + Err(e) => Err(e) + } + }).await??; + // Custom reader loop for decrypting + loop { + // Blocking reader + let (d, read, mut buf) = tokio::task::spawn_blocking(move || { + let mut buf = vec![0; 1024 * 64]; + match decrypted.read(&mut buf) { + Ok(r) => Ok((decrypted, r, buf)), + Err(e) => Err(e) + } + }).await??; + decrypted = d; + if read == 0 { + break; + } + file.write_all(&mut buf[0..read]).await?; + yield read; + } + } + } + + /// Download and convert to MP3 + fn download_track_convert_stream( + path: impl AsRef, + encrypted: AudioFile, + key: AudioKey, + format: AudioFormat, + quality: Quality, + ) -> impl Stream> { + try_stream! { + let mut file = File::create(path).await?; + let mut decrypted = AudioDecrypt::new(key, encrypted); + // Skip (i guess encrypted shit) + let mut skip: [u8; 0xa7] = [0; 0xa7]; + let decrypted = tokio::task::spawn_blocking(move || { + match decrypted.read_exact(&mut skip) { + Ok(_) => Ok(decrypted), + Err(e) => Err(e) + } + }).await??; + // Convertor + let mut decrypted = tokio::task::spawn_blocking(move || { + AudioConverter::new(Box::new(decrypted), format, quality) + }).await??; + + // Custom reader loop for decrypting + loop { + // Blocking reader + let (d, read, mut buf) = tokio::task::spawn_blocking(move || { + let mut buf = vec![0; 1024 * 64]; + match decrypted.read(&mut buf) { + Ok(r) => Ok((decrypted, r, buf)), + Err(e) => Err(e) + } + }).await??; + decrypted = d; + if read == 0 { + break; + } + file.write_all(&mut buf[0..read]).await?; + yield read; + } + } + } +} + +#[derive(Debug, Clone)] +pub enum AudioFormat { + OGG, + AAC, + MP3, + MP4, + Unknown, +} + +impl AudioFormat { + /// Get extension + pub fn extension(&self) -> String { + match self { + AudioFormat::OGG => "ogg", + AudioFormat::AAC => "m4a", + AudioFormat::MP3 => "mp3", + AudioFormat::MP4 => "mp4", + AudioFormat::Unknown => "", + } + .to_string() + } +} + +impl From for AudioFormat { + fn from(f: FileFormat) -> Self { + match f { + FileFormat::OGG_VORBIS_96 => Self::OGG, + FileFormat::OGG_VORBIS_160 => Self::OGG, + FileFormat::OGG_VORBIS_320 => Self::OGG, + FileFormat::MP3_256 => Self::MP3, + FileFormat::MP3_320 => Self::MP3, + FileFormat::MP3_160 => Self::MP3, + FileFormat::MP3_96 => Self::MP3, + FileFormat::MP3_160_ENC => Self::MP3, + FileFormat::MP4_128_DUAL => Self::MP4, + FileFormat::OTHER3 => Self::Unknown, + FileFormat::AAC_160 => Self::AAC, + FileFormat::AAC_320 => Self::AAC, + FileFormat::MP4_128 => Self::MP4, + FileFormat::OTHER5 => Self::Unknown, + } + } +} + +impl Quality { + /// Get librespot FileFormat + pub fn get_file_formats(&self) -> Vec { + match self { + Self::Q320 => vec![ + FileFormat::OGG_VORBIS_320, + FileFormat::AAC_320, + FileFormat::MP3_320, + ], + Self::Q256 => vec![FileFormat::MP3_256], + Self::Q160 => vec![ + FileFormat::OGG_VORBIS_160, + FileFormat::AAC_160, + FileFormat::MP3_160, + ], + Self::Q96 => vec![FileFormat::OGG_VORBIS_96, FileFormat::MP3_96], + } + } + + /// Fallback to lower quality + pub fn fallback(&self) -> Option { + match self { + Self::Q320 => Some(Quality::Q256), + Self::Q256 => Some(Quality::Q160), + Self::Q160 => Some(Quality::Q96), + Self::Q96 => None, + } + } +} + +#[derive(Debug, Clone)] +pub struct DownloadJob { + pub id: i64, + pub track_id: String, +} + +#[derive(Debug, Clone)] +pub enum Message { + // Send job to worker + GetJob, + // Update state of download + UpdateState(i64, DownloadState), + AddToQueue(Vec), + // Get all downloads to UI + GetDownloads, +} + +#[derive(Debug, Clone)] +pub enum Response { + Downloads(Vec), +} + +#[derive(Debug, Clone)] +pub struct Download { + pub id: i64, + pub track_id: String, + pub title: String, + pub subtitle: String, + pub state: DownloadState, +} + +impl Into for aspotify::Track { + fn into(self) -> Download { + Download { + id: 0, + track_id: self.id.unwrap(), + title: self.name, + subtitle: self + .artists + .first() + .map(|a| a.name.to_owned()) + .unwrap_or_default(), + state: DownloadState::None, + } + } +} + +impl Into for aspotify::TrackSimplified { + fn into(self) -> Download { + Download { + id: 0, + track_id: self.id.unwrap(), + title: self.name, + subtitle: self + .artists + .first() + .map(|a| a.name.to_owned()) + .unwrap_or_default(), + state: DownloadState::None, + } + } +} + +impl Into for Download { + fn into(self) -> DownloadJob { + DownloadJob { + id: self.id, + track_id: self.track_id, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DownloadState { + None, + Lock, + Downloading(usize, usize), + Post, + Done, + Error(String), +} + +/// Bitrate of music +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Copy)] +pub enum Quality { + Q320, + Q256, + Q160, + Q96, +} + +impl ToString for Quality { + fn to_string(&self) -> String { + match self { + Quality::Q320 => "320kbps", + Quality::Q256 => "256kbps", + Quality::Q160 => "160kbps", + Quality::Q96 => "96kbps", + } + .to_string() + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DownloaderConfig { + pub concurrent_downloads: usize, + pub quality: Quality, + pub path: PathBuf, + pub filename_template: String, + pub id3v24: bool, + pub convert_to_mp3: bool, + pub separator: String, +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..11c8a43 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,134 @@ +use std::fmt; + +#[derive(Debug, Clone)] +pub enum SpotifyError { + Error(String), + IoError(std::io::ErrorKind, String), + MercuryError, + AuthenticationError, + Unavailable, + SpotifyIdError, + ChannelError, + AudioKeyError, + LameConverterError(String), + JoinError, + ASpotify(String), + Serde(String, usize, usize), + InvalidUri, + ParseError(url::ParseError), + ID3Error(String, String), + Reqwest(String), + InvalidFormat, +} + +impl std::error::Error for SpotifyError {} +impl fmt::Display for SpotifyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + SpotifyError::Error(e) => write!(f, "Error: {}", e), + SpotifyError::MercuryError => write!(f, "Mercury Error"), + SpotifyError::IoError(kind, err) => write!(f, "IO: {:?} {}", kind, err), + SpotifyError::AuthenticationError => write!(f, "Authentication Error"), + SpotifyError::Unavailable => write!(f, "Unavailable!"), + SpotifyError::SpotifyIdError => write!(f, "Invalid Spotify ID"), + SpotifyError::ChannelError => write!(f, "Channel Error"), + SpotifyError::AudioKeyError => write!(f, "Audio Key Error"), + SpotifyError::LameConverterError(e) => write!(f, "Lame error: {}", e), + SpotifyError::JoinError => write!(f, "Tokio Join Error"), + SpotifyError::ASpotify(e) => write!(f, "Spotify Error: {}", e), + SpotifyError::Serde(e, l, c) => write!(f, "Serde Error @{}:{} {}", l, c, e), + SpotifyError::InvalidUri => write!(f, "Invalid URI"), + SpotifyError::ParseError(e) => write!(f, "Parse Error: {}", e), + SpotifyError::ID3Error(k, e) => write!(f, "ID3 Error: {} {}", k, e), + SpotifyError::Reqwest(e) => write!(f, "Reqwest Error: {}", e), + SpotifyError::InvalidFormat => write!(f, "Invalid Format!"), + } + } +} +impl From for SpotifyError { + fn from(e: std::io::Error) -> Self { + Self::IoError(e.kind(), e.to_string()) + } +} +impl From> for SpotifyError { + fn from(e: Box) -> Self { + Self::Error(e.to_string()) + } +} + +impl From for SpotifyError { + fn from(_: librespot::core::mercury::MercuryError) -> Self { + Self::MercuryError + } +} + +impl From for SpotifyError { + fn from(e: librespot::core::session::SessionError) -> Self { + match e { + librespot::core::session::SessionError::IoError(e) => e.into(), + librespot::core::session::SessionError::AuthenticationError(_) => { + SpotifyError::AuthenticationError + } + } + } +} + +impl From for SpotifyError { + fn from(_: librespot::core::spotify_id::SpotifyIdError) -> Self { + Self::SpotifyIdError + } +} + +impl From for SpotifyError { + fn from(_: librespot::core::channel::ChannelError) -> Self { + Self::ChannelError + } +} + +impl From for SpotifyError { + fn from(_: librespot::core::audio_key::AudioKeyError) -> Self { + Self::AudioKeyError + } +} + +impl From for SpotifyError { + fn from(_: tokio::task::JoinError) -> Self { + Self::JoinError + } +} + +impl From for SpotifyError { + fn from(e: aspotify::Error) -> Self { + Self::ASpotify(e.to_string()) + } +} + +impl From for SpotifyError { + fn from(e: serde_json::Error) -> Self { + Self::Serde(e.to_string(), e.line(), e.column()) + } +} + +impl From for SpotifyError { + fn from(e: url::ParseError) -> Self { + Self::ParseError(e) + } +} + +impl From for SpotifyError { + fn from(e: id3::Error) -> Self { + Self::ID3Error(e.kind.to_string(), e.description.to_string()) + } +} + +impl From for SpotifyError { + fn from(e: reqwest::Error) -> Self { + Self::Reqwest(e.to_string()) + } +} + +impl From for SpotifyError { + fn from(e: lewton::VorbisError) -> Self { + SpotifyError::Error(format!("Lewton: {}", e)) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..d821ad7 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,161 @@ +#[macro_use] +extern crate log; + +mod converter; +mod downloader; +mod error; +mod settings; +mod spotify; +mod tag; + +use async_std::{task, task::block_on}; +use colored::Colorize; +use downloader::{DownloadState, Downloader}; +use settings::Settings; +use spotify::Spotify; +use std::{ + env, + ffi::OsStr, + path::Path, + time::{Duration, Instant}, +}; + +fn main() { + block_on(start()); +} + +async fn start() { + let settings = match Settings::load().await { + Ok(settings) => { + println!( + "{} {}.", + "Settings successfully loaded. Continuing with spotify account:".green(), + settings.username + ); + settings + } + Err(_e) => { + println!( + "{} {}...", + "Settings could not be loaded, because of the following error:".red(), + _e + ); + let default_settings = + Settings::new("username", "password", "client_id", "secret").unwrap(); + match default_settings.save().await { + Ok(_) => { + println!( + "{}", + "..but default settings have been created successfully. Edit them and run the program again.".green() + ); + } + Err(_e) => { + println!( + "{} {}", + "..and default settings could not be written:".red(), + _e + ); + } + }; + return; + } + }; + + let args: Vec = env::args().collect(); + if args.len() > 1 { + let spotify = match Spotify::new( + &settings.username, + &settings.password, + &settings.client_id, + &settings.client_secret, + ) + .await + { + Ok(spotify) => { + println!("{}", "Login succeeded.".green()); + spotify + } + Err(_e) => { + println!( + "{} {}", + "Login failed, possibly due to invalid credentials or settings:".red(), + _e + ); + return; + } + }; + + let downloader = Downloader::new(settings.downloader, spotify); + + match downloader.add_uri(&args[1]).await { + Ok(_) => {} + Err(_e) => { + error!("{} {}", "Adding url failed:".red(), _e) + } + } + + let refresh = Duration::from_secs(settings.refresh_ui_seconds); + let now = Instant::now(); + let mut time_elapsed: u64; + + 'outer: loop { + print!("{esc}[2J{esc}[1;1H", esc = 27 as char); + let mut exit_flag: i8 = 1; + + for download in downloader.get_downloads().await { + let state = download.state; + + let progress: String; + + if state != DownloadState::Done { + exit_flag &= 0; + progress = match state { + DownloadState::Downloading(r, t) => { + let p = r as f32 / t as f32 * 100.0; + if p > 100.0 { + "100%".to_string() + } else { + format!("{}%", p as i8) + } + } + DownloadState::Post => "Postprocessing... ".to_string(), + DownloadState::None => "Preparing... ".to_string(), + DownloadState::Lock => "Holding... ".to_string(), + DownloadState::Error(e) => { + exit_flag |= 1; + format!("{} ", e) + }, + DownloadState::Done => { + exit_flag |= 1; + "Impossible state".to_string() + } + }; + } else { + progress = "Done.".to_string(); + } + + println!("{:<19}| {}", progress, download.title); + } + time_elapsed = now.elapsed().as_secs(); + if exit_flag == 1 { + break 'outer; + } + + println!("\nElapsed second(s): {}", time_elapsed); + task::sleep(refresh).await + } + println!("Finished download(s) in {} second(s).", time_elapsed); + } else { + println!( + "Usage:\n{} (track_url | album_url | playlist_url)", + env::args() + .next() + .as_ref() + .map(Path::new) + .and_then(Path::file_name) + .and_then(OsStr::to_str) + .map(String::from) + .unwrap() + ); + } +} diff --git a/src/settings.rs b/src/settings.rs new file mode 100644 index 0000000..6f08f3e --- /dev/null +++ b/src/settings.rs @@ -0,0 +1,62 @@ +use crate::downloader::DownloaderConfig; +use crate::downloader::Quality; +use crate::error::SpotifyError; +use serde::{Deserialize, Serialize}; + +use tokio::{ + fs::File, + io::{AsyncReadExt, AsyncWriteExt}, +}; + +// Structure for holding all the settings +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Settings { + pub username: String, + pub password: String, + pub client_id: String, + pub client_secret: String, + pub refresh_ui_seconds: u64, + pub downloader: DownloaderConfig, +} +impl Settings { + // Create new instance + pub fn new( + username: &str, + password: &str, + client_id: &str, + client_secret: &str, + ) -> Option { + Some(Settings { + username: username.to_string(), + password: password.to_string(), + client_id: client_id.to_string(), + client_secret: client_secret.to_string(), + refresh_ui_seconds: 1, + downloader: DownloaderConfig { + concurrent_downloads: 4, + quality: Quality::Q320, + path: dirs::audio_dir().unwrap().join("downloads"), + filename_template: "%artist% - %title%".to_string(), + id3v24: true, + convert_to_mp3: false, + separator: ", ".to_string(), + }, + }) + } + + // Serialize the settings to a json file + pub async fn save(&self) -> Result<(), SpotifyError> { + let data = serde_json::to_string_pretty(self)?; + let mut file = File::create("settings.json").await?; + file.write_all(data.as_bytes()).await?; + Ok(()) + } + + // Deserialize the settings from a json file + pub async fn load() -> Result { + let mut file = File::open("settings.json").await?; + let mut buf = String::new(); + file.read_to_string(&mut buf).await?; + Ok(serde_json::from_str(&buf)?) + } +} diff --git a/src/spotify.rs b/src/spotify.rs new file mode 100644 index 0000000..ae13e0e --- /dev/null +++ b/src/spotify.rs @@ -0,0 +1,168 @@ +use aspotify::{ + Album, Client, ClientCredentials, Playlist, PlaylistItemType, Track, TrackSimplified, +}; +use librespot::core::authentication::Credentials; +use librespot::core::config::SessionConfig; +use librespot::core::session::Session; +use std::fmt; +use url::Url; + +use crate::error::SpotifyError; + +pub struct Spotify { + // librespotify sessopm + pub session: Session, + pub spotify: Client, +} + +impl Spotify { + /// Create new instance + pub async fn new( + username: &str, + password: &str, + client_id: &str, + client_secret: &str, + ) -> Result { + // librespot + let credentials = Credentials::with_password(username, password); + let session = Session::connect(SessionConfig::default(), credentials, None).await?; + //rspoitfy + let credentials = ClientCredentials { + id: client_id.to_string(), + secret: client_secret.to_string(), + }; + let spotify = Client::new(credentials); + + Ok(Spotify { session, spotify }) + } + + /// Parse URI or URL into URI + pub fn parse_uri(uri: &str) -> Result { + // Already URI + if uri.starts_with("spotify:") { + if uri.split(':').collect::>().len() < 3 { + return Err(SpotifyError::InvalidUri); + } + return Ok(uri.to_string()); + } + + // Parse URL + let url = Url::parse(uri)?; + // Spotify Web Player URL + if url.host_str() == Some("open.spotify.com") { + let path = url + .path_segments() + .ok_or(SpotifyError::Error("Missing URL path".into()))? + .collect::>(); + if path.len() < 2 { + return Err(SpotifyError::InvalidUri); + } + return Ok(format!("spotify:{}:{}", path[0], path[1])); + } + Err(SpotifyError::InvalidUri) + } + + /// Fetch data for URI + pub async fn resolve_uri(&self, uri: &str) -> Result { + let parts = uri.split(':').skip(1).collect::>(); + let id = parts[1]; + match parts[0] { + "track" => { + let track = self.spotify.tracks().get_track(id, None).await?; + Ok(SpotifyItem::Track(track.data)) + } + "playlist" => { + let playlist = self.spotify.playlists().get_playlist(id, None).await?; + Ok(SpotifyItem::Playlist(playlist.data)) + } + "album" => { + let album = self.spotify.albums().get_album(id, None).await?; + Ok(SpotifyItem::Album(album.data)) + } + // Unsupported / Unimplemented + _ => Ok(SpotifyItem::Other(uri.to_string())), + } + } + + /// Get all tracks from playlist + pub async fn full_playlist(&self, id: &str) -> Result, SpotifyError> { + let mut items = vec![]; + let mut offset = 0; + loop { + let page = self + .spotify + .playlists() + .get_playlists_items(id, 100, offset, None) + .await?; + items.append( + &mut page + .data + .items + .iter() + .filter_map(|i| { + if let Some(item) = &i.item { + if let PlaylistItemType::Track(t) = item { + Some(t.to_owned()) + } else { + None + } + } else { + None + } + }) + .collect(), + ); + + // End + offset += page.data.items.len(); + if page.data.total == offset { + return Ok(items); + } + } + } + + /// Get all tracks from album + pub async fn full_album(&self, id: &str) -> Result, SpotifyError> { + let mut items = vec![]; + let mut offset = 0; + loop { + let page = self + .spotify + .albums() + .get_album_tracks(id, 50, offset, None) + .await?; + items.append(&mut page.data.items.to_vec()); + + // End + offset += page.data.items.len(); + if page.data.total == offset { + return Ok(items); + } + } + } +} + +impl Clone for Spotify { + fn clone(&self) -> Self { + Self { + session: self.session.clone(), + spotify: Client::new(self.spotify.credentials.clone()), + } + } +} + +/// Basic debug implementation so can be used in other structs +impl fmt::Debug for Spotify { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "") + } +} + +#[derive(Debug, Clone)] +pub enum SpotifyItem { + Track(Track), + Album(Album), + Playlist(Playlist), + /// Unimplemented + Other(String), +} diff --git a/src/tag/id3.rs b/src/tag/id3.rs new file mode 100644 index 0000000..87edd27 --- /dev/null +++ b/src/tag/id3.rs @@ -0,0 +1,85 @@ +use chrono::{Datelike, NaiveDate}; +use id3::frame::{Picture, PictureType, Timestamp}; +use id3::{Tag, Version}; +use std::path::{Path, PathBuf}; + +use crate::error::SpotifyError; + +use super::Field; + +pub struct ID3Tag { + path: PathBuf, + tag: Tag, + separator: String, + version: Version, +} + +impl ID3Tag { + /// Load form path + pub fn open(path: impl AsRef) -> Result { + let tag = Tag::read_from_path(&path).unwrap_or_default(); + + Ok(ID3Tag { + path: path.as_ref().to_owned(), + tag, + separator: String::new(), + version: Version::Id3v23, + }) + } + + /// Wether to use ID3v2.4 + pub fn use_id3_v24(&mut self, v: bool) { + match v { + true => self.version = Version::Id3v24, + false => self.version = Version::Id3v23, + } + } +} + +impl super::Tag for ID3Tag { + fn set_separator(&mut self, separator: &str) { + self.separator = separator.to_string(); + } + + fn set_raw(&mut self, tag: &str, value: Vec) { + self.tag.set_text(tag, value.join(&self.separator)); + } + + fn set_field(&mut self, field: Field, value: Vec) { + let tag = match field { + Field::Title => "TIT2", + Field::Artist => "TPE1", + Field::Album => "TALB", + Field::TrackNumber => "TPOS", + Field::DiscNumber => "TRCK", + Field::Genre => "TCON", + Field::Label => "TPUB", + Field::AlbumArtist => "TPE2", + }; + self.set_raw(tag, value); + } + + fn save(&mut self) -> Result<(), SpotifyError> { + Ok(self.tag.write_to_path(&self.path, self.version)?) + } + + fn add_cover(&mut self, mime: &str, data: Vec) { + self.tag.add_picture(Picture { + mime_type: mime.to_owned(), + picture_type: PictureType::CoverFront, + description: "cover".to_string(), + data, + }); + } + + fn set_release_date(&mut self, date: NaiveDate) { + self.tag.set_date_recorded(Timestamp { + year: date.year(), + month: Some(date.month() as u8), + day: Some(date.day() as u8), + hour: None, + minute: None, + second: None, + }) + } +} diff --git a/src/tag/mod.rs b/src/tag/mod.rs new file mode 100644 index 0000000..929a4c1 --- /dev/null +++ b/src/tag/mod.rs @@ -0,0 +1,57 @@ +use chrono::NaiveDate; +use std::path::Path; + +use crate::downloader::AudioFormat; +use crate::error::SpotifyError; + +use self::id3::ID3Tag; +use ogg::OGGTag; + +mod id3; +mod ogg; + +pub enum TagWrap { + OGG(OGGTag), + ID3(ID3Tag), +} + +impl TagWrap { + /// Load from file + pub fn new(path: impl AsRef, format: AudioFormat) -> Result { + match format { + AudioFormat::OGG => Ok(TagWrap::OGG(OGGTag::open(path)?)), + AudioFormat::MP3 => Ok(TagWrap::ID3(ID3Tag::open(path)?)), + _ => Err(SpotifyError::Error("Invalid format!".into())), + } + } + + /// Get Tag trait + pub fn get_tag(&mut self) -> Box<&mut dyn Tag> { + match self { + TagWrap::OGG(tag) => Box::new(tag), + TagWrap::ID3(tag) => Box::new(tag), + } + } +} + +pub trait Tag { + // Set tag values separator + fn set_separator(&mut self, separator: &str); + fn set_raw(&mut self, tag: &str, value: Vec); + fn set_field(&mut self, field: Field, value: Vec); + fn set_release_date(&mut self, date: NaiveDate); + fn add_cover(&mut self, mime: &str, data: Vec); + fn save(&mut self) -> Result<(), SpotifyError>; +} + +#[derive(Debug, Clone)] +pub enum Field { + Title, + Artist, + Album, + TrackNumber, + DiscNumber, + AlbumArtist, + Genre, + Label, +} diff --git a/src/tag/ogg.rs b/src/tag/ogg.rs new file mode 100644 index 0000000..a0fa21e --- /dev/null +++ b/src/tag/ogg.rs @@ -0,0 +1,68 @@ +use chrono::{Datelike, NaiveDate}; +use oggvorbismeta::{read_comment_header, replace_comment_header, CommentHeader, VorbisComments}; +use std::fs::File; +use std::path::{Path, PathBuf}; + +use super::Field; +use crate::error::SpotifyError; + +pub struct OGGTag { + path: PathBuf, + tag: CommentHeader, +} + +impl OGGTag { + /// Load tag from file + pub fn open(path: impl AsRef) -> Result { + let mut file = File::open(&path)?; + let tag = read_comment_header(&mut file); + Ok(OGGTag { + path: path.as_ref().to_owned(), + tag, + }) + } +} + +impl super::Tag for OGGTag { + fn set_separator(&mut self, _separator: &str) {} + + fn set_field(&mut self, field: Field, value: Vec) { + let tag = match field { + Field::Title => "TITLE", + Field::Artist => "ARTIST", + Field::Album => "ALBUM", + Field::TrackNumber => "TRACKNUMBER", + Field::DiscNumber => "DISCNUMBER", + Field::Genre => "GENRE", + Field::Label => "LABEL", + Field::AlbumArtist => "ALBUMARTIST", + }; + self.set_raw(tag, value); + } + + fn add_cover(&mut self, _mime: &str, _data: Vec) { + error!("ALBUM ART IN OGG NOT SUPPORTED!"); + } + + fn set_raw(&mut self, tag: &str, value: Vec) { + self.tag.add_tag_multi( + tag, + &value.iter().map(|v| v.as_str()).collect::>(), + ); + } + + fn save(&mut self) -> Result<(), SpotifyError> { + let file = File::open(&self.path)?; + let mut out = replace_comment_header(file, self.tag.clone()); + let mut file = File::create(&self.path)?; + std::io::copy(&mut out, &mut file)?; + Ok(()) + } + + fn set_release_date(&mut self, date: NaiveDate) { + self.tag.add_tag_single( + "DATE", + &format!("{}-{:02}-{:02}", date.year(), date.month(), date.day()), + ) + } +}