From a0c15a8b3049e4066e0340d345aac14c8b0af838 Mon Sep 17 00:00:00 2001 From: MrDulfin Date: Sat, 7 Dec 2024 16:54:40 -0500 Subject: [PATCH] created rust workspace. merged dmp-core --- .gitignore | 4 + Cargo.toml | 20 + dmp-core/.gitignore | 18 - dmp-core/Cargo.toml | 51 - dmp-core/LICENSE | 661 --------- dmp-core/README.md | 3 - dmp-core/src/bus_control.rs | 0 dmp-core/src/config/mod.rs | 250 ---- dmp-core/src/config/other_settings.rs | 20 - dmp-core/src/lib.rs | 22 - dmp-core/src/music_controller/connections.rs | 103 -- dmp-core/src/music_controller/controller.rs | 197 --- dmp-core/src/music_controller/queue.rs | 25 - dmp-core/src/music_player/gstreamer.rs | 512 ------- dmp-core/src/music_player/kira.rs | 0 dmp-core/src/music_player/player.rs | 97 -- .../src/music_storage/db_reader/common.rs | 49 - .../music_storage/db_reader/extern_library.rs | 11 - .../music_storage/db_reader/foobar/reader.rs | 200 --- .../music_storage/db_reader/foobar/utils.rs | 15 - .../music_storage/db_reader/itunes/reader.rs | 358 ----- dmp-core/src/music_storage/db_reader/mod.rs | 13 - .../db_reader/musicbee/reader.rs | 220 --- .../music_storage/db_reader/musicbee/utils.rs | 29 - dmp-core/src/music_storage/library.rs | 1229 ----------------- .../src/music_storage/music_collection.rs | 7 - dmp-core/src/music_storage/playlist.rs | 353 ----- dmp-core/src/music_storage/utils.rs | 108 -- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 3 +- 30 files changed, 26 insertions(+), 4554 deletions(-) create mode 100644 Cargo.toml delete mode 100644 dmp-core/.gitignore delete mode 100644 dmp-core/Cargo.toml delete mode 100644 dmp-core/LICENSE delete mode 100644 dmp-core/README.md delete mode 100644 dmp-core/src/bus_control.rs delete mode 100644 dmp-core/src/config/mod.rs delete mode 100644 dmp-core/src/config/other_settings.rs delete mode 100644 dmp-core/src/lib.rs delete mode 100644 dmp-core/src/music_controller/connections.rs delete mode 100644 dmp-core/src/music_controller/controller.rs delete mode 100644 dmp-core/src/music_controller/queue.rs delete mode 100644 dmp-core/src/music_player/gstreamer.rs delete mode 100644 dmp-core/src/music_player/kira.rs delete mode 100644 dmp-core/src/music_player/player.rs delete mode 100644 dmp-core/src/music_storage/db_reader/common.rs delete mode 100644 dmp-core/src/music_storage/db_reader/extern_library.rs delete mode 100644 dmp-core/src/music_storage/db_reader/foobar/reader.rs delete mode 100644 dmp-core/src/music_storage/db_reader/foobar/utils.rs delete mode 100644 dmp-core/src/music_storage/db_reader/itunes/reader.rs delete mode 100644 dmp-core/src/music_storage/db_reader/mod.rs delete mode 100644 dmp-core/src/music_storage/db_reader/musicbee/reader.rs delete mode 100644 dmp-core/src/music_storage/db_reader/musicbee/utils.rs delete mode 100644 dmp-core/src/music_storage/library.rs delete mode 100644 dmp-core/src/music_storage/music_collection.rs delete mode 100644 dmp-core/src/music_storage/playlist.rs delete mode 100644 dmp-core/src/music_storage/utils.rs diff --git a/.gitignore b/.gitignore index 2c26531..6e69e0d 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,7 @@ dist-ssr *.njsproj *.sln *.sw? + +# Rust Stuff +target +Cargo.lock \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..8a90fb6 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,20 @@ +[workspace] +resolver = "2" +members = [ + "src-tauri", + "dmp-core", +] + +[workspace.package] +authors = ["G2-Games ", "MrDulfin"] + +[workspace.lints.rust] +unsafe_code = "forbid" + +[profile.production] +inherits = "release" +strip = true +lto = true +opt-level = "z" +codegen-units = 1 +panic = "abort" \ No newline at end of file diff --git a/dmp-core/.gitignore b/dmp-core/.gitignore deleted file mode 100644 index b2d0597..0000000 --- a/dmp-core/.gitignore +++ /dev/null @@ -1,18 +0,0 @@ -# Rust binary output dir -target/ -test-config/ -# Rust configuration -Cargo.lock - -# Database files -*.db3* -music_database* - -# Storage formats -*.kate-swp* -*.m3u -*.m3u8 -*.json -*.zip -*.xml - diff --git a/dmp-core/Cargo.toml b/dmp-core/Cargo.toml deleted file mode 100644 index b4ab8d0..0000000 --- a/dmp-core/Cargo.toml +++ /dev/null @@ -1,51 +0,0 @@ -[package] -name = "dmp-core" -version = "0.0.0" -edition = "2021" -license = "AGPL-3.0-only" -description = "Backend crate for the Dango Music Player " -homepage = "" -documentation = "" -readme = "README.md" -repository = "https://github.com/Dangoware/dmp-core" -keywords = [] -categories = [] - -[dependencies] -file-format = { version = "0.23.0", features = [ - "reader-asf", - "reader-ebml", - "reader-mp4", - "reader-rm", - "reader-txt", - "reader-xml", - "serde", -] } -lofty = "0.18.2" -serde = { version = "1.0.195", features = ["derive"] } -walkdir = "2.4.0" -chrono = { version = "0.4.31", features = ["serde"] } -bincode = { version = "2.0.0-rc.3", features = ["serde"] } -rayon = "1.8.0" -log = "0.4" -base64 = "0.21.5" -snap = "1" -rcue = "0.1.3" -gstreamer = "0.21.3" -glib = "0.18.5" -crossbeam-channel = "0.5.8" -crossbeam = "0.8.2" -quick-xml = "0.31.0" -leb128 = "0.2.5" -urlencoding = "2.1.3" -m3u8-rs = "5.0.5" -thiserror = "1.0.56" -uuid = { version = "1.6.1", features = ["v4", "serde"] } -serde_json = "1.0.111" -deunicode = "1.4.2" -opener = { version = "0.7.0", features = ["reveal"] } -tempfile = "3.10.1" -listenbrainz = "0.7.0" -discord-rpc-client = "0.4.0" -nestify = "0.3.3" -kushi = "0.1.3" diff --git a/dmp-core/LICENSE b/dmp-core/LICENSE deleted file mode 100644 index 0ad25db..0000000 --- a/dmp-core/LICENSE +++ /dev/null @@ -1,661 +0,0 @@ - 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/dmp-core/README.md b/dmp-core/README.md deleted file mode 100644 index 751669c..0000000 --- a/dmp-core/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# dango-core - -This is the backend crate for the [Dango Music Player](https://github.com/Dangoware/dango-music-player) diff --git a/dmp-core/src/bus_control.rs b/dmp-core/src/bus_control.rs deleted file mode 100644 index e69de29..0000000 diff --git a/dmp-core/src/config/mod.rs b/dmp-core/src/config/mod.rs deleted file mode 100644 index 6371afb..0000000 --- a/dmp-core/src/config/mod.rs +++ /dev/null @@ -1,250 +0,0 @@ -pub mod other_settings; - -use std::{ - fs::{self, File, OpenOptions}, - io::{Error, Read, Write}, - path::PathBuf, -}; - -use serde::{Deserialize, Serialize}; -use serde_json::to_string_pretty; -use thiserror::Error; -use uuid::Uuid; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ConfigLibrary { - pub name: String, - pub path: PathBuf, - pub uuid: Uuid, - pub scan_folders: Option>, -} - -impl Default for ConfigLibrary { - fn default() -> Self { - ConfigLibrary { - name: String::new(), - path: PathBuf::from("library"), - uuid: Uuid::new_v4(), - scan_folders: None, - } - } -} - -impl ConfigLibrary { - pub fn new(path: PathBuf, name: String, scan_folders: Option>) -> Self { - ConfigLibrary { - name, - path, - uuid: Uuid::new_v4(), - scan_folders, - } - } - - pub fn open(&self) -> Result { - match File::open(self.path.as_path()) { - Ok(ok) => Ok(ok), - Err(e) => Err(e), - } - } -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct ConfigLibraries { - pub default_library: Uuid, - pub library_folder: PathBuf, - pub libraries: Vec, -} - -impl ConfigLibraries { - pub fn set_default(mut self, uuid: &Uuid) { - self.default_library = *uuid; - } - - pub fn get_default(&self) -> Result<&ConfigLibrary, ConfigError> { - for library in &self.libraries { - if library.uuid == self.default_library { - return Ok(library); - } - } - Err(ConfigError::NoDefaultLibrary) - } - - pub fn get_library(&self, uuid: &Uuid) -> Result { - for library in &self.libraries { - // dbg!(&library.uuid, &uuid); - if &library.uuid == uuid { - return Ok(library.to_owned()); - } - } - Err(ConfigError::NoConfigLibrary(*uuid)) - } - - pub fn uuid_exists(&self, uuid: &Uuid) -> bool { - for library in &self.libraries { - if &library.uuid == uuid { - return true; - } - } - false - } -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct ConfigConnections { - pub listenbrainz_token: Option, -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -#[serde(default)] -pub struct Config { - pub path: PathBuf, - pub backup_folder: Option, - pub libraries: ConfigLibraries, - pub volume: f32, - pub connections: ConfigConnections, -} - -impl Config { - pub fn new() -> Self { - Config { - libraries: ConfigLibraries { - libraries: vec![ConfigLibrary::default()], - ..Default::default() - }, - ..Default::default() - } - } - - pub fn new_main() -> Self { - Config::default() - } - - pub fn write_file(&self) -> Result<(), Error> { - let mut writer = self.path.clone(); - writer.set_extension("tmp"); - let mut file = OpenOptions::new() - .create(true) - .truncate(true) - .read(true) - .write(true) - .open(&writer)?; - let config = to_string_pretty(self)?; - // dbg!(&config); - - file.write_all(config.as_bytes())?; - fs::rename(writer, self.path.as_path())?; - Ok(()) - } - - pub fn save_backup(&self) -> Result<(), Box> { - match &self.backup_folder { - Some(path) => { - let mut writer = path.clone(); - writer.set_extension("tmp"); - let mut file = OpenOptions::new() - .create(true) - .truncate(true) - .read(true) - .write(true) - .open(&writer)?; - let config = to_string_pretty(self)?; - // dbg!(&config); - - file.write_all(config.as_bytes())?; - fs::rename(writer, self.path.as_path())?; - Ok(()) - } - None => Err(ConfigError::NoBackupLibrary.into()), - } - } - - pub fn read_file(path: PathBuf) -> Result { - let mut file: File = File::open(path)?; - let mut bun: String = String::new(); - _ = file.read_to_string(&mut bun); - let config: Config = serde_json::from_str::(&bun)?; - Ok(config) - } - - pub fn push_library(&mut self, lib: ConfigLibrary) { - if self.libraries.libraries.is_empty() { - self.libraries.default_library = lib.uuid; - } - self.libraries.libraries.push(lib); - } -} - -#[derive(Error, Debug)] -pub enum ConfigError { - #[error("No Library Found for {0}!")] - NoConfigLibrary(Uuid), - #[error("There is no Default Library for this Config")] - NoDefaultLibrary, - //TODO: do something about playlists - #[error("Please provide a better m3u8 Playlist")] - BadPlaylist, - #[error("No backup Config folder present")] - NoBackupLibrary, -} - -#[cfg(test)] -pub mod tests { - use super::{Config, ConfigLibrary}; - use crate::music_storage::library::MusicLibrary; - use std::{ - path::PathBuf, - sync::{Arc, RwLock}, - }; - - pub fn new_config_lib() -> (Config, MusicLibrary) { - let lib = ConfigLibrary::new( - PathBuf::from("test-config/library"), - String::from("library"), - None, - ); - let mut config = Config { - path: PathBuf::from("test-config/config_test.json"), - ..Default::default() - }; - - config.push_library(lib); - config.write_file().unwrap(); - - let mut lib = MusicLibrary::init( - config.libraries.get_default().unwrap().path.clone(), - dbg!(config.libraries.default_library), - ) - .unwrap(); - lib.scan_folder("test-config/music/").unwrap(); - lib.save(config.libraries.get_default().unwrap().path.clone()).unwrap(); - - (config, lib) - } - - pub fn read_config_lib() -> (Config, MusicLibrary) { - let config = Config::read_file(PathBuf::from("test-config/config_test.json")).unwrap(); - - // dbg!(&config); - - let mut lib = MusicLibrary::init( - config.libraries.get_default().unwrap().path.clone(), - config.libraries.get_default().unwrap().uuid, - ) - .unwrap(); - - lib.scan_folder("test-config/music/").unwrap(); - - lib.save(config.libraries.get_default().unwrap().path.clone()).unwrap(); - - (config, lib) - } - - #[test] - fn test3() { - let (config, _) = read_config_lib(); - - _ = config.write_file(); - - dbg!(config); - } -} diff --git a/dmp-core/src/config/other_settings.rs b/dmp-core/src/config/other_settings.rs deleted file mode 100644 index 164221f..0000000 --- a/dmp-core/src/config/other_settings.rs +++ /dev/null @@ -1,20 +0,0 @@ -pub enum Setting { - String { - name: String, - value: String - }, - Int { - name: String, - value: i32 - }, - Bool { - name: String, - value: bool - }, - -} - -pub struct Form { - -} - diff --git a/dmp-core/src/lib.rs b/dmp-core/src/lib.rs deleted file mode 100644 index 6f93ec1..0000000 --- a/dmp-core/src/lib.rs +++ /dev/null @@ -1,22 +0,0 @@ -pub mod music_storage { - pub mod library; - pub mod music_collection; - pub mod playlist; - mod utils; - - #[allow(dead_code)] - pub mod db_reader; -} - -pub mod music_controller { - pub mod controller; - pub mod connections; - pub mod queue; -} - -pub mod music_player { - pub mod gstreamer; - pub mod player; -} - -pub mod config; diff --git a/dmp-core/src/music_controller/connections.rs b/dmp-core/src/music_controller/connections.rs deleted file mode 100644 index b2ceb85..0000000 --- a/dmp-core/src/music_controller/connections.rs +++ /dev/null @@ -1,103 +0,0 @@ -// use std::{ -// sync::{Arc, RwLock}, -// error::Error, -// }; - -// use discord_rpc_client::Client; -// use listenbrainz::ListenBrainz; -// use uuid::Uuid; - -// use crate::{ -// config::config::Config, music_controller::controller::{Controller, QueueCmd, QueueResponse}, music_storage::library::{MusicLibrary, Song, Tag} -// }; - -// use super::controller::DatabaseResponse; - - - -// impl Controller { -// pub fn listenbrainz_authenticate(&mut self) -> Result> { -// let config = &self.config.read().unwrap(); -// let mut client = ListenBrainz::new(); - -// let lbz_token = match &config.connections.listenbrainz_token { -// Some(token) => token, -// None => todo!("No ListenBrainz token in config") -// }; - -// if !client.is_authenticated() { -// client.authenticate(lbz_token)?; -// } - -// Ok(client) -// } -// pub fn lbz_scrobble(&self, client: ListenBrainz, uuid: Uuid) -> Result<(), Box> { -// let config = &self.config.read().unwrap(); - -// &self.db_mail.send(super::controller::DatabaseCmd::QueryUuid(uuid)); -// let res = &self.db_mail.recv()?; -// let song = match res { -// DatabaseResponse::Song(song) => song, -// _ => todo!() -// }; -// let unknown = &"unknown".to_string(); -// let artist = song.get_tag(&Tag::Artist).unwrap_or(unknown); -// let track = song.get_tag(&Tag::Title).unwrap_or(unknown); -// let release = song.get_tag(&Tag::Album).map(|rel| rel.as_str()); - -// client.listen(artist, track, release)?; -// Ok(()) -// } - -// pub fn lbz_now_playing(&self, client: ListenBrainz, uuid: Uuid) -> Result<(), Box> { -// let config = &self.config.read().unwrap(); - -// &self.db_mail.send(super::controller::DatabaseCmd::QueryUuid(uuid)); -// let res = &self.db_mail.recv()?; -// let song = match res { -// DatabaseResponse::Song(song) => song, -// _ => todo!() -// }; -// let unknown = &"unknown".to_string(); -// let artist = song.get_tag(&Tag::Artist).unwrap_or(unknown); -// let track = song.get_tag(&Tag::Title).unwrap_or(unknown); -// let release = song.get_tag(&Tag::Album).map(|rel| rel.as_str()); - -// client.listen(artist, track, release)?; -// Ok(()) -// } - -// pub fn discord_song_change(client: &mut Client,song: Song) { -// client.set_activity(|a| { -// a.state(format!("Listening to {}", song.get_tag(&Tag::Title).unwrap())) -// .into() -// }); -// } -// } - -// #[cfg(test)] -// mod test_super { -// use std::{thread::sleep, time::Duration}; - -// use super::*; -// use crate::config::config::tests::read_config_lib; - -// #[test] -// fn listenbrainz() { -// let mut c = Controller::start(".\\test-config\\config_test.json").unwrap(); - -// let client = c.listenbrainz_authenticate().unwrap(); - -// c.q_new().unwrap(); -// c.queue_mail[0].send(QueueCmd::SetVolume(0.04)).unwrap(); - -// let songs = c.lib_get_songs(); - -// c.q_enqueue(0, songs[1].location.to_owned()).unwrap(); -// c.q_play(0).unwrap(); - - -// sleep(Duration::from_secs(100)); -// c.lbz_scrobble(client, songs[1].uuid).unwrap(); -// } -// } diff --git a/dmp-core/src/music_controller/controller.rs b/dmp-core/src/music_controller/controller.rs deleted file mode 100644 index 3c9dcb3..0000000 --- a/dmp-core/src/music_controller/controller.rs +++ /dev/null @@ -1,197 +0,0 @@ -//! The [Controller] is the input and output for the entire -//! player. It manages queues, playback, library access, and -//! other functions - -use crossbeam_channel; -use crossbeam_channel::{Receiver, Sender}; -use kushi::QueueError; -use kushi::{Queue, QueueItemType}; -use std::path::PathBuf; -use std::sync::{Arc, Mutex, RwLock}; -use std::thread::spawn; -use thiserror::Error; - -use crossbeam_channel::unbounded; -use std::error::Error; -use uuid::Uuid; - -use crate::config::ConfigError; -use crate::music_player::player::{Player, PlayerCommand, PlayerError}; -use crate::{ - config::Config, music_storage::library::MusicLibrary, -}; - -use super::queue::{QueueAlbum, QueueSong}; - - -pub struct Controller { - pub queue: Arc>>, - pub config: Arc>, - pub library: MusicLibrary, - pub player: Arc>, -} - -#[derive(Error, Debug)] -pub enum ControllerError { - #[error("{0:?}")] - QueueError(#[from] QueueError), - #[error("{0:?}")] - PlayerError(#[from] PlayerError), - #[error("{0:?}")] - ConfigError(#[from] ConfigError), -} - -// TODO: move this to a different location to be used elsewhere -#[derive(Debug, Clone, Copy, PartialEq)] -#[non_exhaustive] -pub enum PlayerLocation { - Test, - Library, - Playlist(Uuid), - File, - Custom, -} - -#[derive(Debug)] -pub(super) struct MailMan { - pub tx: Sender, - rx: Receiver, -} - -impl MailMan { - pub fn new() -> Self { - let (tx, rx) = unbounded::(); - MailMan { tx, rx } - } -} -impl MailMan { - pub fn double() -> (MailMan, MailMan) { - let (tx, rx) = unbounded::(); - let (tx1, rx1) = unbounded::(); - - (MailMan { tx, rx: rx1 }, MailMan { tx: tx1, rx }) - } - - pub fn send(&self, mail: T) -> Result<(), Box> { - self.tx.send(mail).unwrap(); - Ok(()) - } - - pub fn recv(&self) -> Result> { - let u = self.rx.recv()?; - Ok(u) - } -} - -#[allow(unused_variables)] -impl Controller

{ - pub fn start(config_path: T) -> Result > - where - std::path::PathBuf: std::convert::From, - P: Player, - { - let config_path = PathBuf::from(config_path); - - let config = Config::read_file(config_path)?; - let uuid = config.libraries.get_default()?.uuid; - - let library = MusicLibrary::init(config.libraries.get_default()?.path.clone(), uuid)?; - let config_ = Arc::new(RwLock::from(config)); - - - let queue: Queue = Queue { - items: Vec::new(), - played: Vec::new(), - loop_: false, - shuffle: None - }; - - let controller = Controller { - queue: Arc::new(RwLock::from(queue)), - config: config_.clone(), - library, - player: Arc::new(Mutex::new(P::new()?)), - }; - - - let player = controller.player.clone(); - let queue = controller.queue.clone(); - let controller_thread = spawn(move || { - loop { - let signal = { player.lock().unwrap().message_channel().recv().unwrap() }; - match signal { - PlayerCommand::AboutToFinish => { - println!("Switching songs!"); - - let mut queue = queue.write().unwrap(); - - let uri = queue - .next() - .unwrap() - .clone(); - - player - .lock() - .unwrap() - .enqueue_next(&{ - match uri.item { - QueueItemType::Single(song) => song.song.primary_uri().unwrap().0.clone(), - _ => unimplemented!() - } - }) - .unwrap(); - }, - PlayerCommand::EndOfStream => {dbg!()} - _ => {} - } - } - - }); - - - Ok(controller) - } - - pub fn q_add(&mut self, item: &Uuid, source: PlayerLocation, by_human: bool) { - let item = self.library.query_uuid(item).unwrap().0.to_owned(); - self.queue.write().unwrap().add_item(QueueSong { song: item, location: source }, by_human) - } -} - -#[cfg(test)] -mod test_super { - use std::{thread::sleep, time::Duration}; - - use crate::{config::tests::read_config_lib, music_controller::controller::{PlayerLocation, QueueSong}, music_player::{gstreamer::GStreamer, player::Player}}; - - use super::Controller; - - #[test] - fn construct_controller() { - println!("starto!"); - let config = read_config_lib(); - - let next = config.1.library[2].clone(); - { - let controller = Controller::::start("test-config/config_test.json").unwrap(); - { - let mut queue = controller.queue.write().unwrap(); - for x in config.1.library { - queue.add_item(QueueSong { song: x, location: PlayerLocation::Library }, true); - } - } - { - controller.player.lock().unwrap().enqueue_next(next.primary_uri().unwrap().0).unwrap(); - } - { - controller.player.lock().unwrap().set_volume(0.1); - } - { - controller.player.lock().unwrap().play().unwrap(); - } - println!("I'm a tire"); - } - sleep(Duration::from_secs(10)) - - } -} diff --git a/dmp-core/src/music_controller/queue.rs b/dmp-core/src/music_controller/queue.rs deleted file mode 100644 index 27f8075..0000000 --- a/dmp-core/src/music_controller/queue.rs +++ /dev/null @@ -1,25 +0,0 @@ -use std::vec::IntoIter; - -use crate::music_storage::library::{Album, AlbumTrack, Song}; - -use super::controller::PlayerLocation; - -#[derive(Debug, Clone, PartialEq)] -pub struct QueueSong { - pub song: Song, - pub location: PlayerLocation, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct QueueAlbum { - pub album: Album, - pub location: PlayerLocation, -} - -impl IntoIterator for QueueAlbum { - type Item = AlbumTrack; - type IntoIter = IntoIter; - fn into_iter(self) -> Self::IntoIter { - self.album.into_iter() - } -} diff --git a/dmp-core/src/music_player/gstreamer.rs b/dmp-core/src/music_player/gstreamer.rs deleted file mode 100644 index a6915aa..0000000 --- a/dmp-core/src/music_player/gstreamer.rs +++ /dev/null @@ -1,512 +0,0 @@ -// Crate things -use crate::music_storage::library::URI; -use crossbeam_channel::{unbounded, Receiver, Sender}; -use std::error::Error; -use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}; - -// GStreamer things -use glib::FlagsClass; -use gst::{ClockTime, Element}; -use gstreamer as gst; -use gstreamer::prelude::*; - -// Extra things -use chrono::Duration; - -use super::player::{Player, PlayerCommand, PlayerError, PlayerState}; - -impl From for PlayerState { - fn from(value: gst::State) -> Self { - match value { - gst::State::VoidPending => Self::VoidPending, - gst::State::Playing => Self::Playing, - gst::State::Paused => Self::Paused, - gst::State::Ready => Self::Ready, - gst::State::Null => Self::Null, - } - } -} - -impl TryInto for PlayerState { - fn try_into(self) -> Result> { - match self { - Self::VoidPending => Ok(gst::State::VoidPending), - Self::Playing => Ok(gst::State::Playing), - Self::Paused => Ok(gst::State::Paused), - Self::Ready => Ok(gst::State::Ready), - Self::Null => Ok(gst::State::Null), - state => Err(format!("Invalid gst::State: {:?}", state).into()), - } - } - - type Error = Box; -} - -#[derive(Debug, PartialEq, Eq)] -enum PlaybackInfo { - Idle, - Switching, - Playing{ - start: Duration, - end: Duration, - }, - - /// When this is sent, the thread will die! Use it when the [Player] is - /// done playing - Finished -} - -/// An instance of a music player with a GStreamer backend -#[derive(Debug)] -pub struct GStreamer { - source: Option, - - message_rx: crossbeam::channel::Receiver, - playback_tx: crossbeam::channel::Sender, - - playbin: Arc>, - volume: f64, - start: Option, - end: Option, - paused: Arc>, - position: Arc>>, -} - -impl From for PlayerError { - fn from(value: gst::StateChangeError) -> Self { - PlayerError::StateChange(value.to_string()) - } -} - -impl From for PlayerError { - fn from(value: glib::BoolError) -> Self { - PlayerError::General(value.to_string()) - } -} - -impl GStreamer { - /// Set the playback URI - fn set_source(&mut self, source: &URI) -> Result<(), PlayerError> { - if !source.exists().is_ok_and(|x| x) { - // If the source doesn't exist, gstreamer will crash! - return Err(PlayerError::NotFound) - } - - // Make sure the playback tracker knows the stuff is stopped - println!("Beginning switch"); - self.playback_tx.send(PlaybackInfo::Switching).unwrap(); - - let uri = self.playbin.read().unwrap().property_value("current-uri"); - self.source = Some(source.clone()); - match source { - URI::Cue { start, end, .. } => { - self.playbin - .write() - .unwrap() - .set_property("uri", source.as_uri()); - - // Set the start and end positions of the CUE file - self.start = Some(Duration::from_std(*start).unwrap()); - self.end = Some(Duration::from_std(*end).unwrap()); - - // Send the updated position to the tracker - self.playback_tx.send(PlaybackInfo::Playing{ - start: self.start.unwrap(), - end: self.end.unwrap() - }).unwrap(); - - // Wait for it to be ready, and then move to the proper position - self.play().unwrap(); - let now = std::time::Instant::now(); - while now.elapsed() < std::time::Duration::from_millis(20) { - if self.seek_to(Duration::from_std(*start).unwrap()).is_ok() { - return Ok(()); - } - std::thread::sleep(std::time::Duration::from_millis(1)); - } - //panic!("Couldn't seek to beginning of cue track in reasonable time (>20ms)"); - return Err(PlayerError::StateChange("Could not seek to beginning of CUE track".into())) - } - _ => { - self.playbin - .write() - .unwrap() - .set_property("uri", source.as_uri()); - - if self.state() != PlayerState::Playing { - self.play().unwrap(); - } - - while self.raw_duration().is_none() { - std::thread::sleep(std::time::Duration::from_millis(10)); - } - - self.start = Some(Duration::seconds(0)); - self.end = self.raw_duration(); - - // Send the updated position to the tracker - self.playback_tx.send(PlaybackInfo::Playing{ - start: self.start.unwrap(), - end: self.end.unwrap() - }).unwrap(); - } - } - - Ok(()) - } - - /// Gets a mutable reference to the playbin element - fn playbin_mut( - &mut self, - ) -> Result, std::sync::PoisonError>> - { - let element = match self.playbin.write() { - Ok(element) => element, - Err(err) => return Err(err), - }; - Ok(element) - } - - /// Gets a read-only reference to the playbin element - fn playbin( - &self, - ) -> Result, std::sync::PoisonError>> - { - let element = match self.playbin.read() { - Ok(element) => element, - Err(err) => return Err(err), - }; - Ok(element) - } - - /// Set volume of the internal playbin player, can be - /// used to bypass the main volume control for seeking - fn set_gstreamer_volume(&mut self, volume: f64) { - self.playbin_mut().unwrap().set_property("volume", volume) - } - - fn set_state(&mut self, state: gst::State) -> Result<(), gst::StateChangeError> { - self.playbin_mut().unwrap().set_state(state)?; - - Ok(()) - } - - fn raw_duration(&self) -> Option { - self.playbin() - .unwrap() - .query_duration::() - .map(|pos| Duration::nanoseconds(pos.nseconds() as i64)) - } - - /// Get the current state of the playback - fn state(&mut self) -> PlayerState { - self.playbin().unwrap().current_state().into() - /* - match *self.buffer.read().unwrap() { - None => self.playbin().unwrap().current_state().into(), - Some(value) => PlayerState::Buffering(value), - } - */ - } - - fn property(&self, property: &str) -> glib::Value { - self.playbin().unwrap().property_value(property) - } - - fn ready(&mut self) -> Result<(), PlayerError> { - self.set_state(gst::State::Ready)?; - Ok(()) - } -} - -impl Player for GStreamer { - fn new() -> Result { - // Initialize GStreamer, maybe figure out how to nicely fail here - if let Err(err) = gst::init() { - return Err(PlayerError::Init(err.to_string())) - }; - let ctx = glib::MainContext::default(); - let _guard = ctx.acquire(); - let mainloop = glib::MainLoop::new(Some(&ctx), false); - - let playbin_arc = Arc::new(RwLock::new( - match gst::ElementFactory::make("playbin3").build() { - Ok(playbin) => playbin, - Err(error) => return Err(PlayerError::Init(error.to_string())), - } - )); - - let playbin = playbin_arc.clone(); - - let flags = playbin.read().unwrap().property_value("flags"); - let flags_class = FlagsClass::with_type(flags.type_()).unwrap(); - - // Set up the Playbin flags to only play audio - let flags = flags_class - .builder_with_value(flags) - .ok_or(PlayerError::Build)? - .set_by_nick("audio") - .set_by_nick("download") - .unset_by_nick("video") - .unset_by_nick("text") - .build() - .ok_or(PlayerError::Build)?; - - playbin.write().unwrap().set_property_from_value("flags", &flags); - //playbin.write().unwrap().set_property("instant-uri", true); - - let position = Arc::new(RwLock::new(None)); - - // Set up the thread to monitor the position - let (playback_tx, playback_rx) = unbounded(); - let (status_tx, status_rx) = unbounded::(); - let position_update = Arc::clone(&position); - - std::thread::spawn(|| playback_monitor(playbin_arc, status_rx, playback_tx, position_update)); - - // Set up the thread to monitor bus messages - let playbin_bus_ctrl = Arc::clone(&playbin); - let paused = Arc::new(RwLock::new(false)); - let bus_paused = Arc::clone(&paused); - let bus_watch = playbin - .read() - .unwrap() - .bus() - .expect("Failed to get GStreamer message bus") - .add_watch(move |_bus, msg| { - match msg.view() { - gst::MessageView::Eos(_) => println!("End of stream"), - gst::MessageView::StreamStart(_) => println!("Stream start"), - gst::MessageView::Error(err) => { - println!("Error recieved: {}", err); - return glib::ControlFlow::Break - } - gst::MessageView::Buffering(buffering) => { - if *bus_paused.read().unwrap() == true { - return glib::ControlFlow::Continue - } - - // If the player is not paused, pause it - let percent = buffering.percent(); - if percent < 100 { - playbin_bus_ctrl - .write() - .unwrap() - .set_state(gst::State::Paused) - .unwrap(); - } else if percent >= 100 { - println!("Finished buffering"); - playbin_bus_ctrl - .write() - .unwrap() - .set_state(gst::State::Playing) - .unwrap(); - } - } - _ => (), - } - glib::ControlFlow::Continue - }) - .expect("Failed to connect to GStreamer message bus"); - - // Set up a thread to watch the messages - std::thread::spawn(move || { - let _watch = bus_watch; - mainloop.run() - }); - - let source = None; - Ok(Self { - source, - playbin, - message_rx: playback_rx, - playback_tx: status_tx, - volume: 1.0, - start: None, - end: None, - paused, - position, - }) - } - - fn source(&self) -> &Option { - &self.source - } - - fn enqueue_next(&mut self, next_track: &URI) -> Result<(), PlayerError> { - println!("enqueuing in fn"); - self.set_source(next_track) - } - - fn set_volume(&mut self, volume: f64) { - self.volume = volume.clamp(0.0, 1.0); - self.set_gstreamer_volume(self.volume); - } - - fn volume(&self) -> f64 { - self.volume - } - - fn play(&mut self) -> Result<(), PlayerError> { - if self.state() == PlayerState::Playing { - return Ok(()) - } - *self.paused.write().unwrap() = false; - self.set_state(gst::State::Playing)?; - Ok(()) - } - - fn pause(&mut self) -> Result<(), PlayerError> { - if self.state() == PlayerState::Paused || *self.paused.read().unwrap() { - return Ok(()) - } - *self.paused.write().unwrap() = true; - self.set_state(gst::State::Paused)?; - Ok(()) - } - - fn is_paused(&self) -> bool { - self.playbin().unwrap().current_state() == gst::State::Paused - } - - fn position(&self) -> Option { - *self.position.read().unwrap() - } - - fn duration(&self) -> Option { - if self.end.is_some() && self.start.is_some() { - Some(self.end.unwrap() - self.start.unwrap()) - } else { - self.raw_duration() - } - } - - fn seek_by(&mut self, seek_amount: Duration) -> Result<(), PlayerError> { - let time_pos = match *self.position.read().unwrap() { - Some(pos) => pos, - None => return Err(PlayerError::Seek("No position".into())), - }; - let seek_pos = time_pos + seek_amount; - - self.seek_to(seek_pos)?; - Ok(()) - } - - fn seek_to(&mut self, target_pos: Duration) -> Result<(), PlayerError> { - let start = if self.start.is_none() { - return Err(PlayerError::Seek("No START time".into())); - } else { - self.start.unwrap() - }; - - let end = if self.end.is_none() { - return Err(PlayerError::Seek("No END time".into())); - } else { - self.end.unwrap() - }; - - let adjusted_target = target_pos + start; - let clamped_target = adjusted_target.clamp(start, end); - - let seek_pos_clock = - ClockTime::from_useconds(clamped_target.num_microseconds().unwrap() as u64); - - self.set_gstreamer_volume(0.0); - self.playbin_mut() - .unwrap() - .seek_simple(gst::SeekFlags::FLUSH, seek_pos_clock)?; - self.set_gstreamer_volume(self.volume); - Ok(()) - } - - fn stop(&mut self) -> Result<(), PlayerError> { - self.pause()?; - self.ready()?; - - // Send the updated position to the tracker - self.playback_tx.send(PlaybackInfo::Idle).unwrap(); - - // Set all positions to none - *self.position.write().unwrap() = None; - self.start = None; - self.end = None; - Ok(()) - } - - fn message_channel(&self) -> &crossbeam::channel::Receiver { - &self.message_rx - } -} - -impl Drop for GStreamer { - /// Cleans up the `GStreamer` pipeline and the monitoring - /// thread when [Player] is dropped. - fn drop(&mut self) { - self.playbin_mut() - .unwrap() - .set_state(gst::State::Null) - .expect("Unable to set the pipeline to the `Null` state"); - let _ = self.playback_tx.send(PlaybackInfo::Finished); - } -} - -fn playback_monitor( - playbin: Arc>, - status_rx: Receiver, - playback_tx: Sender, - position: Arc>>, -) { - let mut stats = PlaybackInfo::Idle; - let mut pos_temp; - let mut sent_atf = false; - loop { - // Check for new messages to decide how to proceed - if let Ok(result) = status_rx.recv_timeout(std::time::Duration::from_millis(50)) { - stats = result - } - - pos_temp = playbin - .read() - .unwrap() - .query_position::() - .map(|pos| Duration::nanoseconds(pos.nseconds() as i64)); - - match stats { - PlaybackInfo::Playing{start, end} if pos_temp.is_some() => { - // Check if the current playback position is close to the end - let finish_point = end - Duration::milliseconds(2000); - if pos_temp.unwrap().num_microseconds() >= end.num_microseconds() { - println!("MONITOR: End of stream"); - let _ = playback_tx.try_send(PlayerCommand::EndOfStream); - playbin - .write() - .unwrap() - .set_state(gst::State::Ready) - .expect("Unable to set the pipeline state"); - sent_atf = false - } else if pos_temp.unwrap().num_microseconds() >= finish_point.num_microseconds() - && !sent_atf - { - println!("MONITOR: About to finish"); - let _ = playback_tx.try_send(PlayerCommand::AboutToFinish); - sent_atf = true; - } - - // This has to be done AFTER the current time in the file - // is calculated, or everything else is wrong - pos_temp = Some(pos_temp.unwrap() - start) - }, - PlaybackInfo::Finished => { - println!("MONITOR: Shutting down"); - *position.write().unwrap() = None; - break - }, - PlaybackInfo::Idle | PlaybackInfo::Switching => { - sent_atf = false - }, - _ => () - } - - *position.write().unwrap() = pos_temp; - } -} diff --git a/dmp-core/src/music_player/kira.rs b/dmp-core/src/music_player/kira.rs deleted file mode 100644 index e69de29..0000000 diff --git a/dmp-core/src/music_player/player.rs b/dmp-core/src/music_player/player.rs deleted file mode 100644 index 1bfe18c..0000000 --- a/dmp-core/src/music_player/player.rs +++ /dev/null @@ -1,97 +0,0 @@ -use chrono::Duration; -use thiserror::Error; - -use crate::music_storage::library::URI; - -#[derive(Error, Debug)] -pub enum PlayerError { - #[error("player initialization failed: {0}")] - Init(String), - #[error("could not change playback state")] - StateChange(String), - #[error("seeking failed: {0}")] - Seek(String), - #[error("the file or source is not found")] - NotFound, - #[error("failed to build gstreamer item")] - Build, - #[error("poison error")] - Poison, - #[error("general player error")] - General(String), -} - -#[derive(Debug, PartialEq, Eq)] -pub enum PlayerState { - Playing, - Paused, - Ready, - Buffering(u8), - Null, - VoidPending, -} - -#[derive(Debug, PartialEq, Eq)] -pub enum PlayerCommand { - Play, - Pause, - EndOfStream, - AboutToFinish, -} - -pub trait Player { - /// Create a new player. - fn new() -> Result where Self: Sized; - - /// Get the currently playing [URI] from the player. - fn source(&self) -> &Option; - - /// Insert a new [`URI`] to be played. This method should be called at the - /// beginning to start playback of something, and once the [`PlayerCommand`] - /// indicates the track is about to finish to enqueue gaplessly. - /// - /// For backends which do not support gapless playback, `AboutToFinish` - /// will not be called, and the next [`URI`] should be enqueued once `Eos` - /// occurs. - fn enqueue_next(&mut self, next_track: &URI) -> Result<(), PlayerError>; - - /// Set the playback volume, accepts a float from `0` to `1`. - /// - /// Values outside the range of `0` to `1` will be capped. - fn set_volume(&mut self, volume: f64); - - /// Returns the current volume level, a float from `0` to `1`. - fn volume(&self) -> f64; - - /// If the player is paused or stopped, starts playback. - fn play(&mut self) -> Result<(), PlayerError>; - - /// If the player is playing, pause playback. - fn pause(&mut self) -> Result<(), PlayerError>; - - /// Stop the playback entirely, removing the current [`URI`] from the player. - fn stop(&mut self) -> Result<(), PlayerError>; - - /// Convenience function to check if playback is paused. - fn is_paused(&self) -> bool; - - /// Get the current playback position of the player. - fn position(&self) -> Option; - - /// Get the duration of the currently playing track. - fn duration(&self) -> Option; - - /// Seek relative to the current position. - /// - /// The position is capped at the duration of the song, and zero. - fn seek_by(&mut self, seek_amount: Duration) -> Result<(), PlayerError>; - - /// Seek absolutely within the song. - /// - /// The position is capped at the duration of the song, and zero. - fn seek_to(&mut self, target_pos: Duration) -> Result<(), PlayerError>; - - /// Return a reference to the player message channel, which can be cloned - /// in order to monitor messages from the player. - fn message_channel(&self) -> &crossbeam::channel::Receiver; -} diff --git a/dmp-core/src/music_storage/db_reader/common.rs b/dmp-core/src/music_storage/db_reader/common.rs deleted file mode 100644 index 368d86e..0000000 --- a/dmp-core/src/music_storage/db_reader/common.rs +++ /dev/null @@ -1,49 +0,0 @@ -use chrono::{DateTime, TimeZone, Utc}; - -pub fn get_bytes(iterator: &mut std::vec::IntoIter) -> [u8; S] { - let mut bytes = [0; S]; - - for byte in bytes.iter_mut().take(S) { - *byte = iterator.next().unwrap(); - } - - bytes -} - -pub fn get_bytes_vec(iterator: &mut std::vec::IntoIter, number: usize) -> Vec { - let mut bytes = Vec::new(); - - for _ in 0..number { - bytes.push(iterator.next().unwrap()); - } - - bytes -} - -/// Converts the windows DateTime into Chrono DateTime -pub fn get_datetime(iterator: &mut std::vec::IntoIter, topbyte: bool) -> DateTime { - let mut datetime_i64 = i64::from_le_bytes(get_bytes(iterator)); - - if topbyte { - // Zero the topmost byte - datetime_i64 &= 0x00FFFFFFFFFFFFFFF; - } - - if datetime_i64 <= 0 { - return Utc.timestamp_opt(0, 0).unwrap(); - } - - let unix_time_ticks = datetime_i64 - 621355968000000000; - - let unix_time_seconds = unix_time_ticks / 10000000; - - let unix_time_nanos = match (unix_time_ticks as f64 / 10000000.0) - unix_time_seconds as f64 - > 0.0 - { - true => ((unix_time_ticks as f64 / 10000000.0) - unix_time_seconds as f64) * 1000000000.0, - false => 0.0, - }; - - Utc.timestamp_opt(unix_time_seconds, unix_time_nanos as u32) - .unwrap() -} diff --git a/dmp-core/src/music_storage/db_reader/extern_library.rs b/dmp-core/src/music_storage/db_reader/extern_library.rs deleted file mode 100644 index 4312c4a..0000000 --- a/dmp-core/src/music_storage/db_reader/extern_library.rs +++ /dev/null @@ -1,11 +0,0 @@ -use std::path::Path; - -use crate::music_storage::library::Song; - -pub trait ExternalLibrary { - fn from_file(file: &Path) -> Self; - fn write(&self) { - unimplemented!(); - } - fn to_songs(&self) -> Vec; -} diff --git a/dmp-core/src/music_storage/db_reader/foobar/reader.rs b/dmp-core/src/music_storage/db_reader/foobar/reader.rs deleted file mode 100644 index 4d07e71..0000000 --- a/dmp-core/src/music_storage/db_reader/foobar/reader.rs +++ /dev/null @@ -1,200 +0,0 @@ -use std::collections::BTreeMap; -use std::{fs::File, io::Read, path::Path, time::Duration}; - -use uuid::Uuid; - -use super::utils::meta_offset; -use crate::music_storage::db_reader::common::{get_bytes, get_bytes_vec}; -use crate::music_storage::db_reader::extern_library::ExternalLibrary; -use crate::music_storage::library::{Song, URI}; - -const MAGIC: [u8; 16] = [ - 0xE1, 0xA0, 0x9C, 0x91, 0xF8, 0x3C, 0x77, 0x42, 0x85, 0x2C, 0x3B, 0xCC, 0x14, 0x01, 0xD3, 0xF2, -]; - -#[derive(Debug)] -pub struct FoobarPlaylist { - metadata: Vec, - songs: Vec, -} - -impl ExternalLibrary for FoobarPlaylist { - /// Reads the entire MusicBee library and returns relevant values - /// as a `Vec` of `Song`s - fn from_file(file: &Path) -> Self { - let mut f = File::open(file).unwrap(); - let mut buffer = Vec::new(); - let mut retrieved_songs: Vec = Vec::new(); - - // Read the whole file - f.read_to_end(&mut buffer).unwrap(); - - let mut buf_iter = buffer.into_iter(); - - // Parse the header - let magic = get_bytes::<16>(&mut buf_iter); - if magic != MAGIC { - panic!("Magic bytes mismatch!"); - } - - let meta_size = i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize; - let metadata = &get_bytes_vec(&mut buf_iter, meta_size); - let track_count = i32::from_le_bytes(get_bytes(&mut buf_iter)); - - // Read all the track fields - for _ in 0..track_count { - let flags = i32::from_le_bytes(get_bytes(&mut buf_iter)); - - let has_metadata = (0x01 & flags) != 0; - let has_padding = (0x04 & flags) != 0; - - let file_name_offset = i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize; - let file_name = meta_offset(metadata, file_name_offset); - - let subsong_index = i32::from_le_bytes(get_bytes(&mut buf_iter)); - - if !has_metadata { - let track = FoobarPlaylistTrack { - file_name, - subsong_index, - ..Default::default() - }; - retrieved_songs.push(track); - continue; - } - - let file_size = i64::from_le_bytes(get_bytes(&mut buf_iter)); - - // TODO: Figure out how to make this work properly - let file_time = i64::from_le_bytes(get_bytes(&mut buf_iter)); - - let duration = Duration::from_nanos(u64::from_le_bytes(get_bytes(&mut buf_iter)) / 100); - - let rpg_album = f32::from_le_bytes(get_bytes(&mut buf_iter)); - - let rpg_track = f32::from_le_bytes(get_bytes(&mut buf_iter)); - - let rpk_album = f32::from_le_bytes(get_bytes(&mut buf_iter)); - - let rpk_track = f32::from_le_bytes(get_bytes(&mut buf_iter)); - - get_bytes::<4>(&mut buf_iter); - - let mut entries = Vec::new(); - let primary_count = i32::from_le_bytes(get_bytes(&mut buf_iter)); - let secondary_count = i32::from_le_bytes(get_bytes(&mut buf_iter)); - let _secondary_offset = i32::from_le_bytes(get_bytes(&mut buf_iter)); - - // Get primary keys - for _ in 0..primary_count { - println!("{}", i32::from_le_bytes(get_bytes(&mut buf_iter))); - - let key = meta_offset( - metadata, - i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize, - ); - - entries.push((key, String::new())); - } - - // Consume unknown 32 bit value - println!("unk"); - get_bytes::<4>(&mut buf_iter); - - // Get primary values - for i in 0..primary_count { - println!("primkey {i}"); - - let value = meta_offset( - metadata, - i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize, - ); - - entries[i as usize].1 = value; - } - - // Get secondary Keys - for _ in 0..secondary_count { - let key = meta_offset( - metadata, - i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize, - ); - let value = meta_offset( - metadata, - i32::from_le_bytes(get_bytes(&mut buf_iter)) as usize, - ); - entries.push((key, value)); - } - - if has_padding { - get_bytes::<64>(&mut buf_iter); - } - - let track = FoobarPlaylistTrack { - flags, - file_name, - subsong_index, - file_size, - file_time, - duration, - rpg_album, - rpg_track, - rpk_album, - rpk_track, - entries, - }; - - retrieved_songs.push(track); - } - - Self { - songs: retrieved_songs, - metadata: metadata.clone(), - } - } - - fn to_songs(&self) -> Vec { - self.songs.iter().map(|song| song.find_song()).collect() - } -} - -#[derive(Debug, Default)] -pub struct FoobarPlaylistTrack { - flags: i32, - file_name: String, - subsong_index: i32, - file_size: i64, - file_time: i64, - duration: Duration, - rpg_album: f32, - rpg_track: f32, - rpk_album: f32, - rpk_track: f32, - entries: Vec<(String, String)>, -} - -impl FoobarPlaylistTrack { - fn find_song(&self) -> Song { - let location = URI::Local(self.file_name.clone().into()); - let internal_tags = Vec::new(); - - Song { - location: vec![location], - uuid: Uuid::new_v4(), - plays: 0, - skips: 0, - favorited: false, - banned: None, - rating: None, - format: None, - duration: self.duration, - play_time: Duration::from_secs(0), - last_played: None, - date_added: None, - date_modified: None, - album_art: Vec::new(), - tags: BTreeMap::new(), - internal_tags, - } - } -} diff --git a/dmp-core/src/music_storage/db_reader/foobar/utils.rs b/dmp-core/src/music_storage/db_reader/foobar/utils.rs deleted file mode 100644 index 278aa1a..0000000 --- a/dmp-core/src/music_storage/db_reader/foobar/utils.rs +++ /dev/null @@ -1,15 +0,0 @@ -pub fn meta_offset(metadata: &[u8], offset: usize) -> String { - let mut result_vec = Vec::new(); - - let mut i = offset; - loop { - if metadata[i] == 0x00 { - break; - } - - result_vec.push(metadata[i]); - i += 1; - } - - String::from_utf8_lossy(&result_vec).into() -} diff --git a/dmp-core/src/music_storage/db_reader/itunes/reader.rs b/dmp-core/src/music_storage/db_reader/itunes/reader.rs deleted file mode 100644 index 6ff383c..0000000 --- a/dmp-core/src/music_storage/db_reader/itunes/reader.rs +++ /dev/null @@ -1,358 +0,0 @@ -use file_format::FileFormat; -use lofty::{AudioFile, LoftyError, ParseOptions, Probe, TagType, TaggedFileExt}; -use quick_xml::events::Event; -use quick_xml::reader::Reader; -use uuid::Uuid; -use std::collections::{BTreeMap, HashMap}; -use std::fs::File; -use std::path::{Path, PathBuf}; -use std::str::FromStr; -use std::time::Duration as StdDur; -use std::vec::Vec; - -use chrono::prelude::*; - -use crate::music_storage::db_reader::extern_library::ExternalLibrary; -use crate::music_storage::library::{AlbumArt, BannedType, Service, Song, Tag, URI}; -use crate::music_storage::utils; - -use urlencoding::decode; - -#[derive(Debug, Default, Clone)] -pub struct ITunesLibrary { - tracks: Vec, -} -impl ITunesLibrary { - fn new() -> Self { - Default::default() - } - pub fn tracks(self) -> Vec { - self.tracks - } -} -impl ExternalLibrary for ITunesLibrary { - fn from_file(file: &Path) -> Self { - let mut reader = Reader::from_file(file).unwrap(); - reader.trim_text(true); - //count every event, for fun ig? - let mut count = 0; - //count for skipping useless beginning key - let mut count2 = 0; - //number of grabbed songs - let mut count3 = 0; - //number of IDs skipped - let mut count4 = 0; - - let mut buf = Vec::new(); - let mut skip = false; - - let mut converted_songs: Vec = Vec::new(); - - let mut song_tags: HashMap = HashMap::new(); - let mut key: String = String::new(); - let mut tagvalue: String = String::new(); - let mut key_selected = false; - - use std::time::Instant; - let now = Instant::now(); - - loop { - //push tag to song_tags map - if !key.is_empty() && !tagvalue.is_empty() { - song_tags.insert(key.clone(), tagvalue.clone()); - key.clear(); - tagvalue.clear(); - key_selected = false; - - //end the song to start a new one, and turn turn current song map into iTunesSong - if song_tags.contains_key(&"Location".to_string()) { - count3 += 1; - //check for skipped IDs - if &count3.to_string() - != song_tags.get_key_value(&"Track ID".to_string()).unwrap().1 - { - count3 += 1; - count4 += 1; - } - converted_songs.push(ITunesSong::from_hashmap(&mut song_tags).unwrap()); - song_tags.clear(); - skip = true; - } - } - match reader.read_event_into(&mut buf) { - Ok(Event::Start(_)) => { - count += 1; - count2 += 1; - } - Ok(Event::Text(e)) => { - if count < 17 && count != 10 { - continue; - } else if skip { - skip = false; - continue; - } - - let text = e.unescape().unwrap().to_string(); - if text == count2.to_string() && !key_selected { - continue; - } - - //Add the key/value depenidng on if the key is selected or not ⛩️sorry buzz - - match key_selected { - true => tagvalue.push_str(&text), - false => { - key.push_str(&text); - if !key.is_empty() { - key_selected = true - } else { - panic!("Key not selected?!") - } - } - } - } - Err(e) => panic!("Error at position {}: {:?}", reader.buffer_position(), e), - Ok(Event::Eof) => break, - _ => (), - } - buf.clear(); - } - let elasped = now.elapsed(); - println!("\n\niTunesReader grabbed {} songs in {:#?} seconds\nIDs Skipped: {}", count3, elasped.as_secs(), count4); - let mut lib = ITunesLibrary::new(); - lib.tracks.append(converted_songs.as_mut()); - lib - } - fn to_songs(&self) -> Vec { - let mut count = 0; - let mut bun: Vec = Vec::new(); - for track in &self.tracks { - //grab "other" tags - let mut tags_: BTreeMap = BTreeMap::new(); - for (key, val) in &track.tags { - tags_.insert(to_tag(key.clone()), val.clone()); - } - //make the path readable - let loc_ = if track.location.contains("file://localhost/") { - decode(track.location.strip_prefix("file://localhost/").unwrap()) - .unwrap() - .into_owned() - } else { - decode(track.location.as_str()).unwrap().into_owned() - }; - let loc = loc_.as_str(); - if File::open(loc).is_err() && !loc.contains("http") { - count += 1; - dbg!(loc); - continue; - } - - let location: URI = if track.location.contains("file://localhost/") { - URI::Local(PathBuf::from( - decode(track.location.strip_prefix("file://localhost/").unwrap()) - .unwrap() - .into_owned() - .as_str(), - )) - } else { - URI::Remote(Service::None, decode(&track.location).unwrap().into_owned()) - }; - let dur = match get_duration(Path::new(&loc)) { - Ok(e) => e, - Err(e) => { - dbg!(e); - StdDur::from_secs(0) - } - }; - let play_time_ = StdDur::from_secs(track.plays as u64 * dur.as_secs()); - - let internal_tags = Vec::new(); // TODO: handle internal tags generation - - let ny: Song = Song { - location: vec![location], - uuid: Uuid::new_v4(), - plays: track.plays, - skips: 0, - favorited: track.favorited, - banned: if track.banned { - Some(BannedType::All) - }else { - None - }, - rating: track.rating, - format: match FileFormat::from_file(PathBuf::from(&loc)) { - Ok(e) => Some(e), - Err(_) => None, - }, - duration: dur, - play_time: play_time_, - last_played: track.last_played, - date_added: track.date_added, - date_modified: track.date_modified, - album_art: match get_art(Path::new(&loc)) { - Ok(e) => e, - Err(_) => Vec::new(), - }, - tags: tags_, - internal_tags, - }; - // dbg!(&ny.tags); - bun.push(ny); - } - println!("skipped: {}", count); - bun - } -} -fn to_tag(string: String) -> Tag { - match string.to_lowercase().as_str() { - "name" => Tag::Title, - "album" => Tag::Album, - "artist" => Tag::Artist, - "album artist" => Tag::AlbumArtist, - "genre" => Tag::Genre, - "comment" => Tag::Comment, - "track number" => Tag::Track, - "disc number" => Tag::Disk, - _ => Tag::Key(string), - } -} -fn get_duration(file: &Path) -> Result { - let dur = match Probe::open(file)?.read() { - Ok(tagged_file) => tagged_file.properties().duration(), - - Err(_) => StdDur::from_secs(0), - }; - Ok(dur) -} -fn get_art(file: &Path) -> Result, LoftyError> { - let mut album_art: Vec = Vec::new(); - - let blank_tag = &lofty::Tag::new(TagType::Id3v2); - let normal_options = ParseOptions::new().parsing_mode(lofty::ParsingMode::Relaxed); - let tagged_file: lofty::TaggedFile; - - let tag = match Probe::open(file)?.options(normal_options).read() { - Ok(e) => { - tagged_file = e; - match tagged_file.primary_tag() { - Some(primary_tag) => primary_tag, - - None => match tagged_file.first_tag() { - Some(first_tag) => first_tag, - None => blank_tag, - }, - } - } - Err(_) => blank_tag, - }; - let mut img = match utils::find_images(file) { - Ok(e) => e, - Err(_) => Vec::new(), - }; - if !img.is_empty() { - album_art.append(img.as_mut()); - } - - for (i, _art) in tag.pictures().iter().enumerate() { - let new_art = AlbumArt::Embedded(i); - - album_art.push(new_art) - } - - Ok(album_art) -} - -#[derive(Debug, Clone, Default)] -pub struct ITunesSong { - pub id: i32, - pub plays: i32, - pub favorited: bool, - pub banned: bool, - pub rating: Option, - pub format: Option, - pub song_type: Option, - pub last_played: Option>, - pub date_added: Option>, - pub date_modified: Option>, - pub tags: BTreeMap, - pub location: String, -} - -impl ITunesSong { - pub fn new() -> ITunesSong { - Default::default() - } - - fn from_hashmap(map: &mut HashMap) -> Result { - let mut song = ITunesSong::new(); - //get the path with the first bit chopped off - let path_: String = map.get_key_value("Location").unwrap().1.clone(); - let track_type: String = map.get_key_value("Track Type").unwrap().1.clone(); - let path: String = match track_type.as_str() { - "File" => { - if path_.contains("file://localhost/") { - path_.strip_prefix("file://localhost/").unwrap(); - } - path_ - } - "URL" => path_, - _ => path_, - }; - - for (key, value) in map { - match key.as_str() { - "Track ID" => song.id = value.parse().unwrap(), - "Location" => song.location = path.to_string(), - "Play Count" => song.plays = value.parse().unwrap(), - "Love" => { - //check if the track is (L)Loved or (B)Banned - match value.as_str() { - "L" => song.favorited = true, - "B" => song.banned = false, - _ => continue, - } - } - "Rating" => song.rating = Some(value.parse().unwrap()), - "Kind" => song.format = Some(value.to_string()), - "Play Date UTC" => { - song.last_played = Some(DateTime::::from_str(value).unwrap()) - } - "Date Added" => song.date_added = Some(DateTime::::from_str(value).unwrap()), - "Date Modified" => { - song.date_modified = Some(DateTime::::from_str(value).unwrap()) - } - "Track Type" => song.song_type = Some(value.to_string()), - _ => { - song.tags.insert(key.to_string(), value.to_string()); - } - } - } - // println!("{:.2?}", song); - Ok(song) - } -} - -#[cfg(test)] -mod tests { - use std::{path::{Path, PathBuf}, sync::{Arc, RwLock}}; - - use crate::{config::{Config, ConfigLibrary}, music_storage::{db_reader::extern_library::ExternalLibrary, library::MusicLibrary}}; - - use super::ITunesLibrary; - - #[test] - fn itunes_lib_test() { - let mut config = Config::read_file(PathBuf::from("test-config/config_test.json")).unwrap(); - let config_lib = ConfigLibrary::new(PathBuf::from("test-config/library2"), String::from("library2"), None); - config.libraries.libraries.push(config_lib.clone()); - - let songs = ITunesLibrary::from_file(Path::new("test-config\\iTunesLib.xml")).to_songs(); - - let mut library = MusicLibrary::init(config.libraries.get_default().unwrap().path.clone(), config_lib.uuid).unwrap(); - - songs.iter().for_each(|song| library.add_song(song.to_owned()).unwrap()); - - config.write_file().unwrap(); - library.save(config.libraries.get_default().unwrap().path.clone()).unwrap(); - } -} diff --git a/dmp-core/src/music_storage/db_reader/mod.rs b/dmp-core/src/music_storage/db_reader/mod.rs deleted file mode 100644 index 2a5b979..0000000 --- a/dmp-core/src/music_storage/db_reader/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -pub mod foobar { - pub mod reader; - pub mod utils; -} -pub mod musicbee { - pub mod reader; - pub mod utils; -} -pub mod itunes { - pub mod reader; -} -pub mod common; -pub mod extern_library; \ No newline at end of file diff --git a/dmp-core/src/music_storage/db_reader/musicbee/reader.rs b/dmp-core/src/music_storage/db_reader/musicbee/reader.rs deleted file mode 100644 index 9811862..0000000 --- a/dmp-core/src/music_storage/db_reader/musicbee/reader.rs +++ /dev/null @@ -1,220 +0,0 @@ -use super::utils::get_string; -use crate::music_storage::db_reader::common::{get_bytes, get_datetime}; -use chrono::{DateTime, Utc}; -use std::fs::File; -use std::io::prelude::*; -use std::time::Duration; - -pub struct MusicBeeDatabase { - path: String, -} - -impl MusicBeeDatabase { - pub fn new(path: String) -> MusicBeeDatabase { - MusicBeeDatabase { path } - } - - /// Reads the entire MusicBee library and returns relevant values - /// as a `Vec` of `Song`s - pub fn read(&self) -> Result, Box> { - let mut f = File::open(&self.path).unwrap(); - let mut buffer = Vec::new(); - let mut retrieved_songs: Vec = Vec::new(); - - // Read the whole file - f.read_to_end(&mut buffer)?; - - let mut buf_iter = buffer.into_iter(); - - // Get the song count from the first 4 bytes - // and then right shift it by 8 for some reason - let mut database_song_count = i32::from_le_bytes(get_bytes(&mut buf_iter)); - database_song_count >>= 8; - - let mut song_count = 0; - loop { - // If the file designation is 1, then the end of the database - // has been reached - let file_designation = match buf_iter.next() { - Some(1) => break, - Some(value) => value, - None => break, - }; - - song_count += 1; - - // Get the file status. Unknown what this means - let status = buf_iter.next().unwrap(); - - buf_iter.next(); // Read in a byte to throw it away - - // Get the play count - let play_count = u16::from_le_bytes(get_bytes(&mut buf_iter)); - - // Get the time the song was last played, stored as a signed 64 bit number of microseconds - let last_played = get_datetime(buf_iter.by_ref(), true); - - // Get the number of times the song was skipped - let skip_count = u16::from_le_bytes(get_bytes(&mut buf_iter)); - - // Get the path to the song - let path = get_string(buf_iter.by_ref()); - - // Get the file size - let file_size = i32::from_le_bytes(get_bytes(&mut buf_iter)); - - // Get the sample rate - let sample_rate = i32::from_le_bytes(get_bytes(&mut buf_iter)); - - // Get the channel count - let channel_count = buf_iter.next().unwrap(); - - // Get the bitrate type (CBR, VBR, etc.) - let bitrate_type = buf_iter.next().unwrap(); - - // Get the actual bitrate - let bitrate = i16::from_le_bytes(get_bytes(&mut buf_iter)); - - // Get the track length in milliseconds - let track_length = - Duration::from_millis(i32::from_le_bytes(get_bytes(&mut buf_iter)) as u64); - - // Get the date added and modified in the same format - let date_added = get_datetime(buf_iter.by_ref(), true); - let date_modified = get_datetime(buf_iter.by_ref(), true); - - // Gets artwork information - // - // Artworks are stored as chunks describing the type - // (embedded, file), and some other information. - let mut artwork: Vec = vec![]; - loop { - let artwork_type = buf_iter.next().unwrap(); - if artwork_type > 253 { - break; - } - - let unknown_string = get_string(buf_iter.by_ref()); - let storage_mode = buf_iter.next().unwrap(); - let storage_path = get_string(buf_iter.by_ref()); - - artwork.push(MusicBeeAlbumArt { - artwork_type, - unknown_string, - storage_mode, - storage_path, - }); - } - - buf_iter.next(); // Read in a byte to throw it away - - // Gets all the tags on the song in the database - let mut tags: Vec = vec![]; - loop { - // If the tag code is 0, the end of the block has been reached, so break. - // - // If the tag code is 255, it pertains to some CUE file values that are not known - // throw away these values - let tag_code = match buf_iter.next() { - Some(0) => break, - Some(255) => { - let repeats = u16::from_le_bytes(get_bytes(&mut buf_iter)); - for _ in 0..(repeats * 13) - 2 { - buf_iter.next().unwrap(); - } - - 255 - } - Some(value) => value, - None => panic!(), - }; - - // Get the string value of the tag - let tag_value = get_string(buf_iter.by_ref()); - tags.push(MusicBeeTag { - tag_code, - tag_value, - }); - } - - // Construct the finished song and add it to the vec - let constructed_song = MusicBeeSong { - file_designation, - status, - play_count, - last_played, - skip_count, - path, - file_size, - sample_rate, - channel_count, - bitrate_type, - bitrate, - track_length, - date_added, - date_modified, - artwork, - tags, - }; - - retrieved_songs.push(constructed_song); - } - - println!("The database claims you have: {database_song_count} songs\nThe retrieved number is: {song_count} songs"); - - match database_song_count == song_count { - true => Ok(retrieved_songs), - false => Err("Song counts do not match!".into()), - } - } -} - -#[derive(Debug)] -pub struct MusicBeeTag { - tag_code: u8, - tag_value: String, -} - -#[derive(Debug)] -pub struct MusicBeeAlbumArt { - artwork_type: u8, - unknown_string: String, - storage_mode: u8, - storage_path: String, -} - -#[derive(Debug)] -pub struct MusicBeeSong { - file_designation: u8, - status: u8, - play_count: u16, - pub last_played: DateTime, - skip_count: u16, - path: String, - file_size: i32, - sample_rate: i32, - channel_count: u8, - bitrate_type: u8, - bitrate: i16, - track_length: Duration, - date_added: DateTime, - date_modified: DateTime, - - /* Album art stuff */ - artwork: Vec, - - /* All tags */ - tags: Vec, -} - -impl MusicBeeSong { - pub fn get_tag_code(self, code: u8) -> Option { - for tag in &self.tags { - if tag.tag_code == code { - return Some(tag.tag_value.clone()); - } - } - - None - } -} diff --git a/dmp-core/src/music_storage/db_reader/musicbee/utils.rs b/dmp-core/src/music_storage/db_reader/musicbee/utils.rs deleted file mode 100644 index 65d6333..0000000 --- a/dmp-core/src/music_storage/db_reader/musicbee/utils.rs +++ /dev/null @@ -1,29 +0,0 @@ -use leb128; - -/// Gets a string from the MusicBee database format -/// -/// The length of the string is defined by an LEB128 encoded value at the beginning, followed by the string of that length -pub fn get_string(iterator: &mut std::vec::IntoIter) -> String { - let mut string_length = iterator.next().unwrap() as usize; - if string_length == 0 { - return String::new(); - } - - // Decode the LEB128 value - let mut leb_bytes: Vec = vec![]; - loop { - leb_bytes.push(string_length as u8); - - if string_length >> 7 != 1 { - break; - } - string_length = iterator.next().unwrap() as usize; - } - string_length = leb128::read::unsigned(&mut leb_bytes.as_slice()).unwrap() as usize; - - let mut string_bytes = vec![]; - for _ in 0..string_length { - string_bytes.push(iterator.next().unwrap()); - } - String::from_utf8(string_bytes).unwrap() -} diff --git a/dmp-core/src/music_storage/library.rs b/dmp-core/src/music_storage/library.rs deleted file mode 100644 index 010fed3..0000000 --- a/dmp-core/src/music_storage/library.rs +++ /dev/null @@ -1,1229 +0,0 @@ -use super::playlist::PlaylistFolder; -// Crate things -use super::utils::{find_images, normalize, read_file, write_file}; -use crate::config::Config; - -use std::cmp::Ordering; -// Various std things -use std::collections::{BTreeMap, HashMap}; -use std::error::Error; -use std::ops::ControlFlow::{Break, Continue}; -use std::vec::IntoIter; - -// Files -use file_format::{FileFormat, Kind}; -use glib::filename_to_uri; - -use lofty::{AudioFile, ItemKey, ItemValue, ParseOptions, Probe, TagType, TaggedFileExt}; -use rcue::parser::parse_from_file; -use std::fs; -use std::path::{Path, PathBuf}; -use uuid::Uuid; -use walkdir::WalkDir; - -// Time -use chrono::{serde::ts_milliseconds_option, DateTime, Utc}; -use std::time::Duration; - -// Serialization/Compression -use base64::{engine::general_purpose, Engine as _}; -use serde::{Deserialize, Serialize}; - -// Fun parallel stuff -use rayon::prelude::*; -use std::sync::{Arc, Mutex, RwLock}; - -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] -pub enum AlbumArt { - Embedded(usize), - External(URI), -} - -impl AlbumArt { - pub fn uri(&self) -> Option<&URI> { - match self { - Self::Embedded(_) => None, - Self::External(uri) => Some(uri), - } - } -} - -/// A tag for a song -#[non_exhaustive] -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] -pub enum Tag { - Title, - Album, - Artist, - AlbumArtist, - Genre, - Comment, - Track, - Disk, - Key(String), - Field(String), -} - -impl ToString for Tag { - fn to_string(&self) -> String { - match self { - Self::Title => "TrackTitle".into(), - Self::Album => "AlbumTitle".into(), - Self::Artist => "TrackArtist".into(), - Self::AlbumArtist => "AlbumArtist".into(), - Self::Genre => "Genre".into(), - Self::Comment => "Comment".into(), - Self::Track => "TrackNumber".into(), - Self::Disk => "DiscNumber".into(), - Self::Key(key) => key.into(), - Self::Field(f) => f.into(), - } - } -} - -/// A field within a Song struct -#[derive(Debug)] -pub enum Field { - Location(URI), - Plays(i32), - Skips(i32), - Favorited(bool), - Rating(u8), - Format(FileFormat), - Duration(Duration), - PlayTime(Duration), - LastPlayed(DateTime), - DateAdded(DateTime), - DateModified(DateTime), -} - -impl ToString for Field { - fn to_string(&self) -> String { - match self { - Self::Location(location) => location.to_string(), - Self::Plays(plays) => plays.to_string(), - Self::Skips(skips) => skips.to_string(), - Self::Favorited(fav) => fav.to_string(), - Self::Rating(rating) => rating.to_string(), - Self::Format(format) => match format.short_name() { - Some(name) => name.to_string(), - None => format.to_string(), - }, - Self::Duration(duration) => duration.as_millis().to_string(), - Self::PlayTime(time) => time.as_millis().to_string(), - Self::LastPlayed(last) => last.to_rfc2822(), - Self::DateAdded(added) => added.to_rfc2822(), - Self::DateModified(modified) => modified.to_rfc2822(), - } - } -} - -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] -#[non_exhaustive] -pub enum InternalTag { - DoNotTrack(DoNotTrack), - SongType(SongType), - SongLink(Uuid, SongType), - // Volume Adjustment from -100% to 100% - VolumeAdjustment(i8), -} - -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] -#[non_exhaustive] -pub enum BannedType { - Shuffle, - All, -} - -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] -#[non_exhaustive] -pub enum DoNotTrack { - // TODO: add services to not track - LastFM, - LibreFM, - MusicBrainz, - Discord, -} - -#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)] -#[non_exhaustive] -pub enum SongType { - // TODO: add MORE?! song types - #[default] - Main, - Instrumental, - Remix, - Custom(String), -} - -/// Stores information about a single song -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] -pub struct Song { - pub location: Vec, - pub uuid: Uuid, - pub plays: i32, - pub skips: i32, - pub favorited: bool, - pub banned: Option, - pub rating: Option, - pub format: Option, - pub duration: Duration, - pub play_time: Duration, - #[serde(with = "ts_milliseconds_option")] - pub last_played: Option>, - #[serde(with = "ts_milliseconds_option")] - pub date_added: Option>, - #[serde(with = "ts_milliseconds_option")] - pub date_modified: Option>, - pub album_art: Vec, - pub tags: BTreeMap, - pub internal_tags: Vec, -} - -impl Song { - /// Get a tag's value - /// - /// ``` - /// use dango_core::music_storage::music_db::Tag; - /// // Assuming an already created song: - /// - /// let tag = this_song.get_tag(Tag::Title); - /// - /// assert_eq!(tag, "Some Song Title"); - /// ``` - pub fn get_tag(&self, target_key: &Tag) -> Option<&String> { - self.tags.get(target_key) - } - - /// Gets an internal field from a song - pub fn get_field(&self, target_field: &str) -> Option { - let lower_target = target_field.to_lowercase(); - match lower_target.as_str() { - "location" => Some(Field::Location(self.primary_uri().unwrap().0.clone())), //TODO: make this not unwrap() - "plays" => Some(Field::Plays(self.plays)), - "skips" => Some(Field::Skips(self.skips)), - "favorited" => Some(Field::Favorited(self.favorited)), - "rating" => self.rating.map(Field::Rating), - "duration" => Some(Field::Duration(self.duration)), - "play_time" => Some(Field::PlayTime(self.play_time)), - "format" => self.format.map(Field::Format), - _ => todo!(), // Other field types are not yet supported - } - } - - /// Sets the value of a tag in the song - pub fn set_tag(&mut self, target_key: Tag, new_value: String) { - self.tags.insert(target_key, new_value); - } - - /// Deletes a tag from the song - pub fn remove_tag(&mut self, target_key: &Tag) { - self.tags.remove(target_key); - } - - /// Creates a `Song` from a music file - pub fn from_file>(target_file: &P) -> Result> { - let normal_options = ParseOptions::new().parsing_mode(lofty::ParsingMode::Relaxed); - - let blank_tag = &lofty::Tag::new(TagType::Id3v2); - let tagged_file: lofty::TaggedFile; - let mut duration = Duration::from_secs(0); - let tag = match Probe::open(target_file)?.options(normal_options).read() { - Ok(file) => { - tagged_file = file; - - duration = tagged_file.properties().duration(); - - // Ensure the tags exist, if not, insert blank data - match tagged_file.primary_tag() { - Some(primary_tag) => primary_tag, - - None => match tagged_file.first_tag() { - Some(first_tag) => first_tag, - None => blank_tag, - }, - } - } - - Err(_) => blank_tag, - }; - - let mut tags: BTreeMap = BTreeMap::new(); - for item in tag.items() { - let key = match item.key() { - ItemKey::TrackTitle => Tag::Title, - ItemKey::TrackNumber => Tag::Track, - ItemKey::TrackArtist => Tag::Artist, - ItemKey::AlbumArtist => Tag::AlbumArtist, - ItemKey::Genre => Tag::Genre, - ItemKey::Comment => Tag::Comment, - ItemKey::AlbumTitle => Tag::Album, - ItemKey::DiscNumber => Tag::Disk, - ItemKey::Unknown(unknown) - if unknown == "ACOUSTID_FINGERPRINT" || unknown == "Acoustid Fingerprint" => - { - continue - } - ItemKey::Unknown(unknown) => Tag::Key(unknown.to_string()), - custom => Tag::Key(format!("{:?}", custom)), - }; - - let value = match item.value() { - ItemValue::Text(value) => value.clone(), - ItemValue::Locator(value) => value.clone(), - ItemValue::Binary(bin) => format!("BIN#{}", general_purpose::STANDARD.encode(bin)), - }; - - tags.insert(key, value); - } - - // Get all the album artwork information from the file - let mut album_art: Vec = Vec::new(); - for (i, _art) in tag.pictures().iter().enumerate() { - let new_art = AlbumArt::Embedded(i); - - album_art.push(new_art) - } - - // Find images around the music file that can be used - let found_images = find_images(target_file.as_ref()).unwrap(); - album_art.extend_from_slice(&found_images); - - // Get the format as a string - let format: Option = match FileFormat::from_file(target_file) { - Ok(fmt) => Some(fmt), - Err(_) => None, - }; - - // TODO: Fix error handling - let binding = fs::canonicalize(target_file).unwrap(); - - // TODO: Handle creation of internal tag: Song Type and Song Links - let internal_tags = { Vec::new() }; - let new_song = Song { - location: vec![URI::Local(binding)], - uuid: Uuid::new_v4(), - plays: 0, - skips: 0, - favorited: false, - banned: None, - rating: None, - format, - duration, - play_time: Duration::from_secs(0), - last_played: None, - date_added: Some(chrono::offset::Utc::now()), - date_modified: Some(chrono::offset::Utc::now()), - tags, - album_art, - internal_tags, - }; - Ok(new_song) - } - - /// creates a `Vec` from a cue file - pub fn from_cue(cuesheet: &Path) -> Result, Box> { - let mut tracks = Vec::new(); - - let cue_data = parse_from_file(&cuesheet.to_string_lossy(), false).unwrap(); - - // Get album level information - let album_title = &cue_data.title; - let album_artist = &cue_data.performer; - - let parent_dir = cuesheet.parent().expect("The file has no parent path??"); - for file in cue_data.files.iter() { - let audio_location = &parent_dir.join(file.file.clone()); - - if !audio_location.exists() { - continue; - } - - let next_track = file.tracks.clone(); - let mut next_track = next_track.iter().skip(1); - for (i, track) in file.tracks.iter().enumerate() { - // Get the track timing information - let pregap = match track.pregap { - Some(pregap) => pregap, - None => Duration::from_secs(0), - }; - let postgap = match track.postgap { - Some(postgap) => postgap, - None => Duration::from_secs(0), - }; - - let mut start; - if track.indices.len() > 1 { - start = track.indices[1].1; - } else { - start = track.indices[0].1; - } - if !start.is_zero() { - start -= pregap; - } - - let duration = match next_track.next() { - Some(future) => match future.indices.first() { - Some(val) => val.1 - start, - None => Duration::from_secs(0), - }, - None => match lofty::read_from_path(audio_location) { - Ok(tagged_file) => tagged_file.properties().duration() - start, - - Err(_) => match Probe::open(audio_location)?.read() { - Ok(tagged_file) => tagged_file.properties().duration() - start, - - Err(_) => Duration::from_secs(0), - }, - }, - }; - let end = start + duration + postgap; - - // Get the format as a string - let format: Option = match FileFormat::from_file(audio_location) { - Ok(fmt) => Some(fmt), - Err(_) => None, - }; - - // Get some useful tags - let mut tags: BTreeMap = BTreeMap::new(); - match album_title { - Some(title) => { - tags.insert(Tag::Album, title.clone()); - } - None => (), - } - match album_artist { - Some(artist) => { - tags.insert(Tag::Artist, artist.clone()); - } - None => (), - } - tags.insert(Tag::Track, track.no.parse().unwrap_or((i + 1).to_string())); - match track.title.clone() { - Some(title) => tags.insert(Tag::Title, title), - None => match track.isrc.clone() { - Some(title) => tags.insert(Tag::Title, title), - None => { - let namestr = format!("{} - {}", i, file.file.clone()); - tags.insert(Tag::Title, namestr) - } - }, - }; - match track.performer.clone() { - Some(artist) => tags.insert(Tag::Artist, artist), - None => None, - }; - - // Find images around the music file that can be used - let album_art = find_images(&audio_location.to_path_buf()).unwrap(); - - let new_song = Song { - location: vec![URI::Cue { - location: audio_location.clone(), - index: i, - start, - end, - }], - uuid: Uuid::new_v4(), - plays: 0, - skips: 0, - favorited: false, - banned: None, - rating: None, - format, - duration, - play_time: Duration::from_secs(0), - last_played: None, - date_added: Some(chrono::offset::Utc::now()), - date_modified: Some(chrono::offset::Utc::now()), - tags, - album_art, - internal_tags: Vec::new(), - }; - tracks.push((new_song, audio_location.clone())); - } - } - Ok(tracks) - } - - /// Returns a reference to the first valid URI in the song, and any invalid URIs that come before it, or errors if there are no valid URIs - #[allow(clippy::type_complexity)] - pub fn primary_uri(&self) -> Result<(&URI, Option>), Box> { - let mut invalid_uris = Vec::new(); - let mut valid_uri = None; - - for uri in &self.location { - if uri.exists()? { - valid_uri = Some(uri); - break; - } else { - invalid_uris.push(uri); - } - } - match valid_uri { - Some(uri) => Ok(( - uri, - if !invalid_uris.is_empty() { - Some(invalid_uris) - } else { - None - }, - )), - None => Err("No valid URIs for this song".into()), - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub enum URI { - Local(PathBuf), - Cue { - location: PathBuf, - index: usize, - start: Duration, - end: Duration, - }, - Remote(Service, String), -} - -impl URI { - pub fn index(&self) -> Result<&usize, Box> { - match self { - URI::Local(_) => Err("\"Local\" has no stored index".into()), - URI::Remote(_, _) => Err("\"Remote\" has no stored index".into()), - URI::Cue { index, .. } => Ok(index), - } - } - - /// Returns the start time of a CUEsheet song, or an - /// error if the URI is not a Cue variant - pub fn start(&self) -> Result<&Duration, Box> { - match self { - URI::Local(_) => Err("\"Local\" has no starting time".into()), - URI::Remote(_, _) => Err("\"Remote\" has no starting time".into()), - URI::Cue { start, .. } => Ok(start), - } - } - - /// Returns the end time of a CUEsheet song, or an - /// error if the URI is not a Cue variant - pub fn end(&self) -> Result<&Duration, Box> { - match self { - URI::Local(_) => Err("\"Local\" has no starting time".into()), - URI::Remote(_, _) => Err("\"Remote\" has no starting time".into()), - URI::Cue { end, .. } => Ok(end), - } - } - - /// Returns the location as a PathBuf - pub fn path(&self) -> PathBuf { - match self { - URI::Local(location) => location.clone(), - URI::Cue { location, .. } => location.clone(), - URI::Remote(_, location) => PathBuf::from(location), - } - } - - pub fn as_uri(&self) -> String { - let path_str = match self { - URI::Local(location) => filename_to_uri(location, None) - .expect("couldn't convert path to URI") - .to_string(), - URI::Cue { location, .. } => filename_to_uri(location, None) - .expect("couldn't convert path to URI") - .to_string(), - URI::Remote(_, location) => location.clone(), - }; - path_str.to_string() - } - - pub fn as_path(&self) -> Result<&PathBuf, Box> { - if let Self::Local(path) = self { - Ok(path) - } else { - Err("This URI is not local!".into()) - } - } - - pub fn exists(&self) -> Result { - match self { - URI::Local(loc) => loc.try_exists(), - URI::Cue { location, .. } => location.try_exists(), - URI::Remote(_, _loc) => Ok(true), // TODO: Investigate a way to do this? - } - } -} - -impl ToString for URI { - fn to_string(&self) -> String { - let path_str = match self { - URI::Local(location) => location.as_path().to_string_lossy(), - URI::Cue { location, .. } => location.as_path().to_string_lossy(), - URI::Remote(_, location) => location.into(), - }; - path_str.to_string() - } -} - -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub enum Service { - InternetRadio, - Spotify, - Youtube, - None, -} - -#[derive(Clone, Debug, PartialEq)] -pub struct Album { - title: String, - artist: Option, - cover: Option, - discs: BTreeMap>, -} - -#[allow(clippy::len_without_is_empty)] -impl Album { - //returns the Album title - pub fn title(&self) -> &String { - &self.title - } - - /// Returns the album cover as an AlbumArt struct, if it exists - fn cover(&self) -> &Option { - &self.cover - } - - /// Returns the Album Artist, if they exist - pub fn artist(&self) -> &Option { - &self.artist - } - - pub fn discs(&self) -> &BTreeMap> { - &self.discs - } - /// Returns the specified track at `index` from the album, returning - /// an error if the track index is out of range - pub fn track(&self, disc: u16, index: usize) -> Option<&(u16, Uuid)> { - self.discs.get(&disc)?.get(index) - } - - fn tracks(&self) -> Vec<(u16, Uuid)> { - let mut songs = Vec::new(); - for disc in self.discs.values() { - songs.extend_from_slice(&disc) - } - songs - } - - /// Returns the number of songs in the album - pub fn len(&self) -> usize { - let mut total = 0; - for disc in self.discs.values() { - total += disc.len(); - } - total - } -} - -impl IntoIterator for Album { - type Item = AlbumTrack; - type IntoIter = IntoIter; - - fn into_iter(self) -> Self::IntoIter { - let mut vec = vec![]; - - for (disc, mut tracks) in self.discs { - tracks.par_sort_by(|a, b| a.0.cmp(&b.0)); - - let mut tracks = tracks.into_iter() - .map(|(track, uuid)| - AlbumTrack { - disc, - track, - uuid - }) - .collect::>(); - - vec.append(&mut tracks); - } - vec.into_iter() - } -} - -pub struct AlbumTrack { - disc: u16, - track: u16, - uuid: Uuid -} - -impl AlbumTrack { - pub fn disc(&self) -> &u16 { - &self.disc - } - - pub fn track(&self) -> &u16 { - &self.track - } - - pub fn uuid(&self) -> &Uuid { - &self.uuid - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct MusicLibrary { - pub name: String, - pub uuid: Uuid, - pub library: Vec, - pub playlists: PlaylistFolder, - pub backup_songs: Vec, // maybe move this to the config instead? -} - -impl MusicLibrary { - const BLOCKED_EXTENSIONS: &'static [&'static str] = &["vob", "log", "txt", "sf2"]; - - /// Create a new library from a name and [Uuid] - fn new(name: String, uuid: Uuid) -> Self { - MusicLibrary { - name, - uuid, - library: Vec::new(), - playlists: PlaylistFolder::default(), - backup_songs: Vec::new(), - } - } - - /// Initialize the database - /// - /// If the database file already exists, return the [MusicLibrary], otherwise create - /// the database first. This needs to be run before anything else to retrieve - /// the [MusicLibrary] Vec - pub fn init(path: PathBuf, uuid: Uuid) -> Result> { - let library: MusicLibrary = match path.exists() { - true => read_file(path)?, - false => { - // If the library does not exist, re-create it - let lib = MusicLibrary::new(String::new(), uuid); - write_file(&lib, path)?; - lib - } - }; - Ok(library) - } - - //#[cfg(debug_assertions)] // We probably wouldn't want to use this for real, but maybe it would have some utility? - pub fn from_path>(path: &P) -> Result> { - let path: PathBuf = path.as_ref().to_path_buf(); - let library: MusicLibrary = match path.exists() { - true => read_file(path)?, - false => { - let lib = MusicLibrary::new(String::new(), Uuid::new_v4()); - write_file(&lib, path)?; - lib - } - }; - Ok(library) - } - - /// Serializes the database out to the file specified in the config - pub fn save_path>(&self, path: &P) -> Result<(), Box> { - let path = path.as_ref(); - match path.try_exists() { - Ok(_) => write_file(self, path)?, - Err(error) => return Err(error.into()), - } - - Ok(()) - } - - /// Serializes the database out to the file specified in the config - pub fn save(&self, path: PathBuf) -> Result<(), Box> { - match path.try_exists() { - Ok(_) => write_file(self, path)?, - Err(error) => return Err(error.into()), - } - - Ok(()) - } - - /// Returns the library size in number of tracks - pub fn len_tracks(&self) -> usize { - self.library.len() - } - - /// Returns the library size in number of albums - pub fn len_albums(&self) -> usize { - self.albums().len() - } - - /// Queries for a [Song] by its [URI], returning a single `Song` - /// with the `URI` that matches along with its position in the library - #[inline(always)] - pub fn query_uri(&self, path: &URI) -> Option<(&Song, usize)> { - let result = self - .library - .par_iter() - .enumerate() - .try_for_each(|(i, track)| { - for location in &track.location { - //TODO: check that this works - if path == location { - return Break((track, i)); - } - } - Continue(()) - }); - - match result { - Break(song) => Some(song), - Continue(_) => None, - } - } - - /// Queries for a [Song] by its [Uuid], returning a single `Song` - /// with the `Uuid` that matches along with its position in the library - pub fn query_uuid(&self, uuid: &Uuid) -> Option<(&Song, usize)> { - let result = self - .library - .par_iter() - .enumerate() - .try_for_each(|(i, track)| { - if uuid == &track.uuid { - return std::ops::ControlFlow::Break((track, i)); - } - Continue(()) - }); - - match result { - Break(song) => Some(song), - Continue(_) => None, - } - } - - /// Queries for a [Song] by its [PathBuf], returning a `Vec<&Song>` - /// with matching `PathBuf`s - fn query_path(&self, path: PathBuf) -> Option> { - let result: Arc>> = Arc::new(Mutex::new(Vec::new())); - self.library.par_iter().for_each(|track| { - if path == track.primary_uri().unwrap().0.path() { - //TODO: make this also not unwrap - Arc::clone(&result).lock().unwrap().push(track); - } - }); - - if result.lock().unwrap().len() > 0 { - Some(Arc::try_unwrap(result).unwrap().into_inner().unwrap()) - } else { - None - } - } - - /// Finds all the audio files within a specified folder - pub fn scan_folder>(&mut self, target_path: &P) -> Result> { - let mut total = 0; - let mut errors = 0; - for target_file in WalkDir::new(target_path) - .follow_links(true) - .into_iter() - .filter_map(|e| e.ok()) - { - let path = target_file.path(); - - // Ensure the target is a file and not a directory, - // if it isn't a file, skip this loop - if !path.is_file() { - continue; - } - - // Check if the file path is already in the db - if self.query_uri(&URI::Local(path.to_path_buf())).is_some() { - continue; - } - - let format = FileFormat::from_file(path)?; - let extension = match path.extension() { - Some(ext) => ext.to_string_lossy().to_ascii_lowercase(), - None => String::new(), - }; - - // If it's a normal file, add it to the database - // if it's a cuesheet, do a bunch of fancy stuff - if (format.kind() == Kind::Audio || format.kind() == Kind::Video) - && !Self::BLOCKED_EXTENSIONS.contains(&extension.as_str()) - { - match self.add_file(target_file.path()) { - Ok(_) => total += 1, - Err(_error) => { - errors += 1; - println!("{:?}: {}", target_file.file_name(), _error) - } // TODO: Handle more of these errors - }; - } else if extension == "cue" { - total += match self.add_cuesheet(target_file.path()) { - Ok(added) => added, - Err(_error) => { - errors += 1; - println!("{:?}: {}", target_file.file_name(), _error); - 0 - } - } - } - } - - println!("Total scanning errors: {}", errors); - - Ok(total) - } - - pub fn remove_missing(&mut self) { - let target_removals = Arc::new(Mutex::new(Vec::new())); - self.library.par_iter().for_each(|t|{ - for location in &t.location { - if !location.exists().unwrap() { - Arc::clone(&target_removals).lock().unwrap().push(location.clone()); - } - } - }); - - let target_removals = Arc::try_unwrap(target_removals).unwrap().into_inner().unwrap(); - for location in target_removals { - self.remove_uri(&location).unwrap(); - } - } - - pub fn add_file(&mut self, target_file: &Path) -> Result<(), Box> { - let new_song = Song::from_file(target_file)?; - match self.add_song(new_song) { - Ok(_) => (), - Err(_) => { - //return Err(error) - } - }; - - Ok(()) - } - - pub fn add_cuesheet(&mut self, cuesheet: &Path) -> Result> { - let tracks = Song::from_cue(cuesheet)?; - let mut tracks_added = tracks.len() as i32; - - for (new_song, location) in tracks { - // Try to remove the original audio file from the db if it exists - if self.remove_uri(&URI::Local(location.clone())).is_ok() { - tracks_added -= 1 - } - match self.add_song(new_song) { - Ok(_) => {} - Err(_error) => { - //println!("{}", _error); - continue; - } - }; - } - Ok(tracks_added) - } - - pub fn add_song(&mut self, new_song: Song) -> Result<(), Box> { - let location = new_song.primary_uri()?.0; - if self.query_uri(location).is_some() { - return Err(format!("URI already in database: {:?}", location).into()); - } - - match location { - URI::Local(_) if self.query_path(location.path()).is_some() => { - return Err(format!("Location exists for {:?}", location).into()) - } - _ => (), - } - - self.library.push(new_song); - - Ok(()) - } - - /// Removes a song indexed by URI, returning the position removed - pub fn remove_uri(&mut self, target_uri: &URI) -> Result> { - let location = match self.query_uri(target_uri) { - Some(value) => value.1, - None => return Err("URI not in database".into()), - }; - - self.library.remove(location); - - Ok(location) - } - - /// Scan the song by a location and update its tags - // TODO: change this to work with multiple uris - pub fn update_uri( - &mut self, - target_uri: &URI, - new_tags: Vec, - ) -> Result<(), Box> { - let (target_song, _) = match self.query_uri(target_uri) { - Some(song) => song, - None => return Err("URI not in database!".to_string().into()), - }; - - println!("{:?}", target_song.location); - - for tag in new_tags { - println!("{:?}", tag); - } - - todo!() - } - - /// Query the database, returning a list of [Song]s - /// - /// The order in which the `sort by` Vec is arranged - /// determines the output sorting. - /// - /// Example: - /// ``` - /// use dango_core::music_storage::music_db::Tag; - /// query_tracks( - /// &String::from("query"), - /// &vec![ - /// Tag::Title - /// ], - /// &vec![ - /// Tag::Field("location".to_string()), - /// Tag::Album, - /// Tag::Disk, - /// Tag::Track, - /// ], - /// ) - /// ``` - /// This would find all titles containing the sequence - /// "query", and would return the results sorted first - /// by path, then album, disk number, and finally track number. - pub fn query_tracks( - &self, - query_string: &String, // The query itself - target_tags: &Vec, // The tags to search - sort_by: &Vec, // Tags to sort the resulting data by - ) -> Option> { - let songs = Arc::new(Mutex::new(Vec::new())); - //let matcher = SkimMatcherV2::default(); - - self.library.par_iter().for_each(|track| { - for tag in target_tags { - let track_result = match tag { - Tag::Field(target) => match track.get_field(target) { - Some(value) => value.to_string(), - None => continue, - }, - _ => match track.get_tag(tag) { - Some(value) => value.clone(), - None => continue, - }, - }; - - /* - let match_level = match matcher.fuzzy_match(&normalize(&track_result), &normalize(query_string)) { - Some(conf) => conf, - None => continue - }; - - if match_level > 100 { - songs.lock().unwrap().push(track); - return; - } - */ - - if normalize(&track_result.to_string()) - .contains(&normalize(&query_string.to_owned())) - { - songs.lock().unwrap().push(track); - return; - } - } - }); - - let lock = Arc::try_unwrap(songs).expect("Lock still has multiple owners!"); - let mut new_songs = lock.into_inner().expect("Mutex cannot be locked!"); - - // Sort the returned list of songs - new_songs.par_sort_by(|a, b| { - for sort_option in sort_by { - let tag_a = match sort_option { - Tag::Field(field_selection) => match a.get_field(field_selection) { - Some(field_value) => field_value.to_string(), - None => continue, - }, - _ => match a.get_tag(sort_option) { - Some(tag_value) => tag_value.to_owned(), - None => continue, - }, - }; - - let tag_b = match sort_option { - Tag::Field(field_selection) => match b.get_field(field_selection) { - Some(field_value) => field_value.to_string(), - None => continue, - }, - _ => match b.get_tag(sort_option) { - Some(tag_value) => tag_value.to_owned(), - None => continue, - }, - }; - - if let (Ok(num_a), Ok(num_b)) = (tag_a.parse::(), tag_b.parse::()) { - // If parsing succeeds, compare as numbers - return num_a.cmp(&num_b); - } else { - // If parsing fails, compare as strings - return tag_a.cmp(&tag_b); - } - } - - // If all tags are equal, sort by Track number - let path_a = PathBuf::from(a.get_field("location").unwrap().to_string()); - let path_b = PathBuf::from(b.get_field("location").unwrap().to_string()); - - path_a.file_name().cmp(&path_b.file_name()) - }); - - if !new_songs.is_empty() { - Some(new_songs) - } else { - None - } - } - - /// Generates all albums from the track list - pub fn albums(&self) -> BTreeMap { - let mut paths = BTreeMap::new(); - - let mut albums: BTreeMap = BTreeMap::new(); - for song in &self.library { - let album_title = match song.get_tag(&Tag::Album) { - Some(title) => title.clone(), - None => continue, - }; - //let norm_title = normalize(&album_title); - - let disc_num = song - .get_tag(&Tag::Disk) - .unwrap_or(&"".to_string()) - .parse::() - .unwrap_or(1); - - match albums.get_mut(&album_title) { - // If the album is in the list, add the track to the appropriate disc within the album - Some(album) => match album.discs.get_mut(&disc_num) { - Some(disc) => disc.push(( - song.get_tag(&Tag::Track) - .unwrap_or(&String::new()) - .parse::() - .unwrap_or_default(), - song.uuid - )), - None => { - album.discs.insert(disc_num, vec![( - song.get_tag(&Tag::Track) - .unwrap_or(&String::new()) - .parse::() - .unwrap_or_default(), - song.uuid - )]); - } - }, - // If the album is not in the list, make it new one and add it - None => { - let album_art = song.album_art.first(); - let new_album = Album { - title: album_title.clone(), - artist: song.get_tag(&Tag::AlbumArtist).cloned(), - discs: BTreeMap::from([( - disc_num, - vec![( - song.get_tag(&Tag::Track) - .unwrap_or(&String::new()) - .parse::() - .unwrap_or_default(), - song.uuid - )])]), - cover: album_art.cloned(), - }; - albums.insert(album_title, new_album); - } - - } - paths.insert(song.uuid, song.primary_uri().unwrap()); - } - - // Sort the tracks in each disk in each album - albums.par_iter_mut().for_each(|album| { - for disc in &mut album.1.discs { - disc.1.sort_by(|a, b| { - let num_a = a.0; - let num_b = b.0; - - if (num_a, num_b) != (0,0) - { - // If parsing the track numbers succeeds, compare as numbers - num_a.cmp(&num_b) - } else { - // If parsing doesn't succeed, compare the locations - let a = match paths.get_key_value(&a.1) { - Some((_, (uri, _))) => uri, - None => return Ordering::Equal - }; - let b = match paths.get_key_value(&b.1) { - Some((_, (uri, _))) => uri, - None => return Ordering::Equal - }; - - a.as_uri().cmp(&b.as_uri()) - } - }); - } - }); - - // Return the albums! - albums - } - - /// Queries a list of albums by title - pub fn query_albums( - &self, - query_string: &str, // The query itself - ) -> Result, Box> { - let all_albums = self.albums(); - - let normalized_query = normalize(query_string); - let albums: Vec = all_albums - .par_iter() - .filter_map(|album| { - if normalize(album.0).contains(&normalized_query) { - Some(album.1.clone()) - } else { - None - } - }) - .collect(); - - Ok(albums) - } -} - -#[cfg(test)] -mod test { - use std::{ - path::PathBuf, - sync::{Arc, RwLock}, - }; - - use crate::{config::{tests::new_config_lib, Config}, music_storage::library::MusicLibrary}; - - #[test] - fn library_init() { - let config = Config::read_file(PathBuf::from("test_config/config_test.json")).unwrap(); - let target_uuid = config.libraries.libraries[0].uuid; - let a = MusicLibrary::init(config.libraries.get_default().unwrap().path.clone(), target_uuid).unwrap(); - dbg!(a); - } -} diff --git a/dmp-core/src/music_storage/music_collection.rs b/dmp-core/src/music_storage/music_collection.rs deleted file mode 100644 index 964c79a..0000000 --- a/dmp-core/src/music_storage/music_collection.rs +++ /dev/null @@ -1,7 +0,0 @@ -use crate::music_storage::library::{AlbumArt, Song}; - -pub trait MusicCollection { - fn title(&self) -> &String; - fn cover(&self) -> Option<&AlbumArt>; - fn tracks(&self) -> Vec; -} diff --git a/dmp-core/src/music_storage/playlist.rs b/dmp-core/src/music_storage/playlist.rs deleted file mode 100644 index c4c0d8c..0000000 --- a/dmp-core/src/music_storage/playlist.rs +++ /dev/null @@ -1,353 +0,0 @@ -use std::error::Error; -use std::{ - fs::File, - io::Read, - path::PathBuf, - sync::{Arc, RwLock}, -}; - -use std::time::Duration; - -// use chrono::Duration; -use super::library::{AlbumArt, MusicLibrary, Song, Tag, URI}; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -use m3u8_rs::{MediaPlaylist, MediaPlaylistType, MediaSegment, Playlist as List2}; -use nestify::nest; - -use rayon::prelude::*; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum SortOrder { - Manual, - Tag(Vec), -} - -nest! { - #[derive(Debug, Clone, Deserialize, Serialize)]* - #[derive(Default)] - pub struct PlaylistFolder { - name: String, - items: Vec< - pub enum PlaylistFolderItem { - Folder(PlaylistFolder), - List(Playlist) - } - > - } -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct Playlist { - uuid: Uuid, - title: String, - cover: Option, - tracks: Vec, - sort_order: SortOrder, - play_count: i32, - play_time: Duration, -} -impl Playlist { - pub fn new() -> Self { - Default::default() - } - pub fn play_count(&self) -> i32 { - self.play_count - } - pub fn play_time(&self) -> Duration { - self.play_time - } - - pub fn title(&self) -> &String { - &self.title - } - - pub fn cover(&self) -> Option<&AlbumArt> { - match &self.cover { - Some(e) => Some(e), - None => None, - } - } - - pub fn tracks(&self) -> Vec { - self.tracks.to_owned() - } - - pub fn set_tracks(&mut self, tracks: Vec) { - self.tracks = tracks; - } - - pub fn add_track(&mut self, track: Uuid) { - self.tracks.push(track); - } - - pub fn remove_track(&mut self, index: i32) { - let index = index as usize; - if (self.tracks.len() - 1) >= index { - self.tracks.remove(index); - } - } - pub fn get_index(&self, uuid: Uuid) -> Option { - let mut i = 0; - if self.contains(uuid) { - for track in &self.tracks { - i += 1; - if &uuid == track { - dbg!("Index gotted! ", i); - return Some(i); - } - } - } - None - } - pub fn contains(&self, uuid: Uuid) -> bool { - self.get_index(uuid).is_some() - } - - pub fn to_file(&self, path: &str) -> Result<(), Box> { - super::utils::write_file(self, PathBuf::from(path))?; - Ok(()) - } - - pub fn from_file(path: &str) -> Result> { - super::utils::read_file(PathBuf::from(path)) - } - - pub fn to_m3u8( - &mut self, - lib: Arc>, - location: &str, - ) -> Result<(), Box> { - let lib = lib.read().unwrap(); - let seg = self - .tracks - .iter() - .filter_map(|uuid| { - // TODO: The Unwraps need to be handled here - if let Some((track, _)) = lib.query_uuid(uuid) { - if let URI::Local(_) = track.primary_uri().unwrap().0 { - Some(MediaSegment { - uri: track.primary_uri().unwrap().0.to_string(), - duration: track.duration.as_millis() as f32, - title: track - .tags - .get_key_value(&Tag::Title) - .map(|tag| tag.1.into()), - ..Default::default() - }) - } else { - None - } - } else { - None - } - }) - .collect::>(); - - let m3u8 = MediaPlaylist { - version: Some(6), - target_duration: 3.0, - media_sequence: 338559, - discontinuity_sequence: 1234, - end_list: true, - playlist_type: Some(MediaPlaylistType::Vod), - segments: seg.clone(), - ..Default::default() - }; - - let mut file = std::fs::OpenOptions::new() - .read(true) - .create(true) - .truncate(true) - .write(true) - .open(location)?; - m3u8.write_to(&mut file)?; - Ok(()) - } - - pub fn from_m3u8( - path: &str, - lib: Arc>, - ) -> Result> { - let mut file = match File::open(path) { - Ok(file) => file, - Err(e) => return Err(e.into()), - }; - let mut bytes = Vec::new(); - file.read_to_end(&mut bytes).unwrap(); - - let parsed = m3u8_rs::parse_playlist(&bytes); - - let playlist = match parsed { - Result::Ok((_, playlist)) => playlist, - Result::Err(e) => panic!("Parsing error: \n{}", e), - }; - - match playlist { - List2::MasterPlaylist(_) => { - Err("This is a Master Playlist!\nPlase input a Media Playlist".into()) - } - List2::MediaPlaylist(playlist_) => { - let mut uuids = Vec::new(); - for seg in playlist_.segments { - let path_ = PathBuf::from(seg.uri.to_owned()); - let mut lib = lib.write().unwrap(); - - let uuid = if let Some((song, _)) = lib.query_uri(&URI::Local(path_.clone())) { - song.uuid - } else { - let song_ = Song::from_file(&path_)?; - let uuid = song_.uuid.to_owned(); - lib.add_song(song_)?; - uuid - }; - uuids.push(uuid); - } - let mut playlist = Playlist::new(); - - #[cfg(target_family = "windows")] - { - playlist.title = path - .split('\\') - .last() - .unwrap_or_default() - .strip_suffix(".m3u8") - .unwrap_or_default() - .to_string(); - } - #[cfg(target_family = "unix")] - { - playlist.title = path - .split("/") - .last() - .unwrap_or_default() - .strip_suffix(".m3u8") - .unwrap_or_default() - .to_string(); - } - - playlist.set_tracks(uuids); - Ok(playlist) - } - } - } - - pub fn out_tracks(&self, lib: Arc>) -> (Vec, Vec<&Uuid>) { - let lib = lib.read().unwrap(); - let mut songs = vec![]; - let mut invalid_uuids = vec![]; - - for uuid in &self.tracks { - if let Some((track, _)) = lib.query_uuid(uuid) { - songs.push(track.to_owned()); - } else { - invalid_uuids.push(uuid); - } - } - - if let SortOrder::Tag(sort_by) = &self.sort_order { - println!("sorting by: {:?}", sort_by); - - songs.par_sort_by(|a, b| { - for (i, sort_option) in sort_by.iter().enumerate() { - dbg!(&i); - let tag_a = match sort_option { - Tag::Field(field_selection) => { - match a.get_field(field_selection.as_str()) { - Some(field_value) => field_value.to_string(), - None => continue, - } - } - _ => match a.get_tag(sort_option) { - Some(tag_value) => tag_value.to_owned(), - None => continue, - }, - }; - - let tag_b = match sort_option { - Tag::Field(field_selection) => match b.get_field(field_selection) { - Some(field_value) => field_value.to_string(), - None => continue, - }, - _ => match b.get_tag(sort_option) { - Some(tag_value) => tag_value.to_owned(), - None => continue, - }, - }; - dbg!(&i); - - if let (Ok(num_a), Ok(num_b)) = (tag_a.parse::(), tag_b.parse::()) { - // If parsing succeeds, compare as numbers - return dbg!(num_a.cmp(&num_b)); - } else { - // If parsing fails, compare as strings - return dbg!(tag_a.cmp(&tag_b)); - } - } - - // If all tags are equal, sort by Track number - let path_a = PathBuf::from(a.get_field("location").unwrap().to_string()); - let path_b = PathBuf::from(b.get_field("location").unwrap().to_string()); - - path_a.file_name().cmp(&path_b.file_name()) - }) - } - - (songs, invalid_uuids) - } -} - -impl Default for Playlist { - fn default() -> Self { - Playlist { - uuid: Uuid::new_v4(), - title: String::default(), - cover: None, - tracks: Vec::default(), - sort_order: SortOrder::Manual, - play_count: 0, - play_time: Duration::from_secs(0), - } - } -} - -#[cfg(test)] -mod test_super { - use super::*; - use crate::config::tests::read_config_lib; - - #[test] - fn list_to_m3u8() { - let (_, lib) = read_config_lib(); - let mut playlist = Playlist::new(); - let tracks = lib.library.iter().map(|track| track.uuid).collect(); - playlist.set_tracks(tracks); - - _ = playlist.to_m3u8( - Arc::new(RwLock::from(lib)), - ".\\test-config\\playlists\\playlist.m3u8", - ); - } - - fn m3u8_to_list() -> Playlist { - let (_, lib) = read_config_lib(); - let arc = Arc::new(RwLock::from(lib)); - let playlist = - Playlist::from_m3u8(".\\test-config\\playlists\\playlist.m3u8", arc).unwrap(); - - _ = playlist.to_file(".\\test-config\\playlists\\playlist"); - dbg!(playlist) - } - - #[test] - fn out_queue_sort() { - let (_, lib) = read_config_lib(); - let mut list = m3u8_to_list(); - list.sort_order = SortOrder::Tag(vec![Tag::Album]); - - let songs = &list.out_tracks(Arc::new(RwLock::from(lib))); - - dbg!(songs); - } -} diff --git a/dmp-core/src/music_storage/utils.rs b/dmp-core/src/music_storage/utils.rs deleted file mode 100644 index b1171da..0000000 --- a/dmp-core/src/music_storage/utils.rs +++ /dev/null @@ -1,108 +0,0 @@ -use deunicode::deunicode_with_tofu; -use file_format::{FileFormat, Kind}; -use snap; -use std::error::Error; -use std::fs::{self, File}; -use std::io::{BufReader, BufWriter}; -use std::path::{Path, PathBuf}; -use walkdir::WalkDir; - -use super::library::{AlbumArt, URI}; - -#[cfg(target_family = "windows")] -use std::os::windows::fs::MetadataExt; - -pub(super) fn normalize(input_string: &str) -> String { - // Normalize the string to latin characters... this needs a lot of work - let mut normalized = deunicode_with_tofu(input_string, " "); - - // Remove non alphanumeric characters - normalized.retain(|c| c.is_alphanumeric()); - normalized = normalized.to_ascii_lowercase(); - - normalized -} - -/// Write any data structure which implements [serde::Serialize] -/// out to a [bincode] encoded file compressed using [snap] -pub(super) fn write_file< - T: serde::Serialize, - U: std::convert::AsRef + std::convert::AsRef + Clone, ->( - library: T, - path: U, -) -> Result<(), Box> { - // Create a temporary name for writing out - let mut writer_name = PathBuf::from(&path); - writer_name.set_extension("tmp"); - - // Create a new BufWriter on the file and a snap frame encoder - let mut writer = BufWriter::new(File::create(&writer_name)?); - //let mut e = snap::write::FrameEncoder::new(writer); - - // Write out the data - bincode::serde::encode_into_std_write( - library, - &mut writer, - bincode::config::standard() - .with_little_endian() - .with_variable_int_encoding(), - )?; - fs::rename(writer_name, &path)?; - - Ok(()) -} - -/// Read a file serialized out with [write_file] and turn it into -/// the desired structure -pub(super) fn read_file serde::Deserialize<'de>>( - path: PathBuf, -) -> Result> { - // Create a new snap reader over the file - let mut file_reader = BufReader::new(File::open(path)?); - //let mut d = snap::read::FrameDecoder::new(file_reader); - - // Decode the library from the serialized data into the vec - let library: T = bincode::serde::decode_from_std_read( - &mut file_reader, - bincode::config::standard() - .with_little_endian() - .with_variable_int_encoding(), - )?; - - Ok(library) -} - -pub fn find_images(song_path: &Path) -> Result, Box> { - let mut images: Vec = Vec::new(); - - let song_dir = song_path.parent().ok_or("")?; - for target_file in WalkDir::new(song_dir) - .follow_links(true) - .into_iter() - .filter_map(|e| e.ok()) - .filter(|e| e.depth() < 3) - // Don't recurse very deep - { - let path = target_file.path(); - if !path.is_file() || !path.exists() { - continue; - } - - let format = FileFormat::from_file(path)?.kind(); - if format != Kind::Image { - continue; - } - - #[cfg(target_family = "windows")] - if (4 & path.metadata().unwrap().file_attributes()) == 4 { - continue; - } - - let image_uri = URI::Local(path.to_path_buf().canonicalize()?); - - images.push(AlbumArt::External(image_uri)); - } - - Ok(images) -} diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 245d6af..ee57f0a 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -22,7 +22,7 @@ tauri = { version = "2", features = ["unstable"] } tauri-plugin-shell = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" -dmp-core = { path = "/media/rocket/Dangoware/Dango Music Player/dmp-core" } +dmp-core = { path = "../dmp-core" } futures = "0.3.31" crossbeam = "0.8.4" directories = "5.0.1" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index ee5ea94..b642688 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -6,8 +6,7 @@ "build": { "beforeDevCommand": "npm run dev", "devUrl": "http://localhost:1420", - "beforeBuildCommand": "npm run build", - "frontendDist": "../dist" + "beforeBuildCommand": "npm run build" }, "app": { "windows": [