diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000..e35fec0
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,14 @@
+# You can override the included template(s) by including variable overrides
+# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
+# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/pipeline/#customization
+# Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings
+# Container Scanning customization: https://docs.gitlab.com/ee/user/application_security/container_scanning/#customizing-the-container-scanning-settings
+# Note that environment variables can be set in several places
+# See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
+stages:
+- test
+sast:
+ stage: test
+include:
+- template: Security/SAST.gitlab-ci.yml
+- template: Security/Secret-Detection.gitlab-ci.yml
diff --git a/BUGS b/BUGS
new file mode 100644
index 0000000..d79a49f
--- /dev/null
+++ b/BUGS
@@ -0,0 +1,18 @@
+Known bugs:
+(Real bugs have been moved to https://framagit.org/ScriptFanix/AtOM/-/issues)
+
+1. Video formats suck
+---------------------
+One would expect video formats to allow for a wide range of tags. Things like
+writer, director, main actors, original author, etc. But most of them only
+provide ONE tag: title...
+Planned workaround: tag guessing (see TODO).
+
+Tools you can use to add metadata to video files:
+(use ALBUM_ARTIST, ALBUM, ARTIST, COMPOSER, GENRE, PERFORMER, TITLE, TRACK,
+DATE)
+ * AVI: ffmpeg
+ * FLV: ffmpeg, or flvmeta >= 1.1
+ * MKV/WebM: ffmpeg (only allows TITLE)
+ * MPEG: ffmpeg
+ * ASF/WMA/WMV: ffmpeg, exfalso
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..f238ad7
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C) 2025 Vincent Riquer
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C) 2025 Vincent Riquer
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/README b/README
deleted file mode 100644
index b87cabe..0000000
--- a/README
+++ /dev/null
@@ -1,77 +0,0 @@
-AtOM: Anything to Ogg and Mp3
-
-URL: http://gitorious.org/atom
-Author: Vincent Riquer
-Copyright/left: 2012-2013 Vincent Riquer - GPLv3 (see doc/GPL-3)
- except: transogg: WTFPL 2.0
-
-============
-Dependencies
-------------
-Required:
-* bash (>= 4.0)
- http://www.gnu.org/software/bash/bash.html
-* SoX
- http://sox.sourceforge.net/
-* SQLite
- http://www.sqlite.org/
-
-Optional:
-* vorbis-tools
- http://www.vorbis.com/
- * ogginfo (Ogg Vorbis metadata)
- * oggenc (Ogg Vorbis encoding)
-* opus-tools
- http://opus-codec.org/
- * opusinfo (Opus metadata)
- * opusenc (Opus encoding)
-* LAME MP3 Encoder
- http://lame.sourceforge.net/
- * lame (MP3 encoding)
-* FLAC
- http://flac.sourceforge.net/
- * metaflac (FLAC metadata)
-* Musepack
- http://www.musepack.net/
- * mpcdec (Musepack decoding)
-* FFmpeg
- http://ffmpeg.org/
- * ffprobe (ID3v2, Musepack, Windows Media and video metadata)
- * ffmpeg (Windows Media and video decoding)
-
-==================
-Using the software
-------------------
-Configuration:
-Please read doc/config before anything else.
-
-====
-Toys
-----
-AtOM requires a database to function. Now that we have a database containing
-various information about our media files, why not use it?
-AtOM comes with a small set of tools in the toys/ directory. These are
-documented in toys/README.
-
-========================
-Shameless Self Promotion
-------------------------
-I am the author of free (Creative Commons CC-By-SA) music which you can stream
-for free, or buy to get high quality and bonuses from Bandcamp
-(http://djblackred.bandcamp.com). If you like electronic music taking its
-inspiration from Trance, Drum & Bass, Ambient and (rarely) Free Jazz, please
-check it out!
-Downloads are available in FLAC, Ogg, MP3, and more, and includes the "source
-code" (sequencer files and the likes) for most tracks.
-I am receiving 85% of the money you'll spend, so you won't be feeding some
-greedy BigCorp producer or distributor.
-And if you don't like it, you can still spread the word to friends who may like.
-You can see this as a way to thank me for this piece of code.
-
-=====
-Legal
------
-Some of the format and/or tool names cited above are trademarks belonging to
-their rightful owners. AtOM and its authors are not linked in any way to
-those companies or individuals. Said companies do not endorse nor support
-AtOM in any way.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..611f29b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,176 @@
+# AtOM: Anything to Ogg and Mp3
+
+* URL: https://framagit.org/ScriptFanix/AtOM/
+* Author: Vincent Riquer
+* Copyright/left: 2012-2013,2015,2025 Vincent Riquer - GPLv3 (see doc/GPL-3)
+ - except: transogg: WTFPL 2.0
+
+## Dependencies
+### Required:
+* [bash (>= 4.0)](http://www.gnu.org/software/bash/bash.html)
+* [SoX](http://sox.sourceforge.net/)
+* [SQLite](http://www.sqlite.org/)
+
+### Optional:
+* [vorbis-tools](http://www.vorbis.com/)
+ * `ogginfo` (Ogg Vorbis metadata)
+ * `oggenc` (Ogg Vorbis encoding)
+* [opus-tools](http://opus-codec.org/)
+ * `opusinfo` (Opus metadata)
+ * `opusenc` (Opus encoding)
+ * `opusdec` (Opus decoding)
+* [LAME MP3 Encoder](http://lame.sourceforge.net/)
+ * `lame` (MP3 encoding)
+* [FLAC](http://flac.sourceforge.net/)
+ * `metaflac` (FLAC metadata)
+* [Musepack](http://www.musepack.net/)
+ * `mpcdec` (Musepack decoding)
+* [FFmpeg](http://ffmpeg.org/)
+ * `ffprobe` (ID3v2, Musepack, Windows Media and video metadata)
+ * `ffmpeg` (Windows Media and video decoding)
+
+## Using the software
+
+### Configuration:
+On first run, AtOM will ask a set of questions to help you create a
+configuration file.
+You can run `atom -S` at any time to re-run the setup. It will be prefilled with
+your current configuration.
+
+If, however, you still want to make changes manually, please read doc/config.
+There are a lot of comments in the generated config file too.
+
+### Preparing data:
+Nothing specific needs to be done. You can edit ypur tags, rename files, move
+them around how you see fit. However, make sure you setup your tag editor
+to *do* update the files' timestamps: though it was initially plan to make this
+optional, using checksums or tags, it was abandoned due to the huge amount of
+IO required.
+
+### Running:
+Make sure your configuration is correct by running
+```
+$ atom -C
+```
+This will produce a human-readable dump of your current configuration.
+If all settings are correct, simply run atom with no argument. Go get a beer.
+Meet some friends. Go to bed. Depending on the size of your collection, the
+first run can take hours, even days.
+After adding/tagging/renaming/deleting files, just re-run atom. It should be
+much faster this time, as only changed data will be treated.
+
+If, for whatever reason, you need to force the regeneration of a destination,
+after changing the quality settings for example, run
+```
+$ atom -F
+```
+
+### Running as a cronjob:
+If you want to run AtOM as a cronjob, `atom -q` will give you a cleaner output,
+more suitable for mail or logfile output. You may also want to limit the size of
+each batch with `-B `. AtOM will not create or update more than
+`` destination files.
+
+For example:
+```
+#m h dom mon dow command
+0 5 * * * atom -B 1000 -q
+```
+
+### Full options list
+Please refer to `atom -h`, as this will always be the most up to date.
+
+## Technical details
+### I. Source scan
+After reading its configuration file, AtOM uses find to get a list of all files
+in the source directory.
+Each file is checked against the database. If it's already there, and its last
+modification time is unchanged, the last_seen field is updated, and that's all.
+If its mtime has changed, mime-type scan is attempted. It is updated in the
+database, along with last_seen.
+If the file is new, its mime-type is scanned, and it is added to the database.
+
+### II. Obsolete files
+Using the last_seen field, AtOM removes from destinations each files which are
+not present anymore in the source directory. AtOM never touches files not
+present in its database (unless there is a filename conflict, in which case your
+file *WILL* be overwritten). If you wish to clear unknown files from your
+destinations, have a look at toys/cleandestinations.
+
+### III. Reading metadata
+AtOM then tries to read metadata from each new or changed file. It also re-reads
+metadata from files scanned with an older version of AtOM, if the parser for
+that format has changed. The actual data read depends on the format, but at the
+very least, AtOM should identify the sampling rate, bitrate and number of
+channels. Unknown file types are scanned with `ffprobe`, so you may still have
+some luck, depending on your FFmpeg setup.
+
+### IV. Task creation
+For every destination files having their last change field different from their
+corresponding source file entry, we create one or more tasks, to generate or
+update (overwrite) the destination file. AtOM tries to generate as few tasks as
+possible, by reusing intermediate files wherever feasible. E.g. if you define
+two destinations, only differing by the encoding format, we will create only one
+decoding task, and two encoding tasks. On average, the number of generated tasks
+shall always be less than 2*n (where n is destinations*file count), unless each
+of them uses different sampling rate/normalization parameters.
+
+Files matching the format, sampling-rate, channel count and bitrate constraints
+are copied (symlinked where possible) during that stage ("immediate copies").
+(also see higher-than setting).
+
+The steps required for each file depend on the format and destination parameters
+(resampling, aso.). Basically, one destination file requires 1 (if reusing
+decoded/resampled file) to 3 (if format can't be decoded using sox and
+resampling/normalization is required) tasks.
+
+### V. Running tasks
+
+#### V.1 Running tasks
+Progress display:
+```
+L:/ W:/ T:/ (F:) % (A:s/task) ETA:
+```
+
+#### V.2 Renaming files
+If rename pattern (or FAT32 compatibility) for one or more destinations has
+changed, files already transcoded will be renamed. Otherwise, this step is
+skipped.
+
+### VI. Copies
+During that stage, files which mime-types matched copy_mime-type directives are
+copied (symlinked where possible) to the destination.
+When a rename pattern is defined and impacts path, files that are not in the
+same directories as files which have been successfully transcoded are ignored,
+as AtOM cannot guess what the destination path should be. Otherwise, files are
+copied with their name and path unchanged.
+
+### VII. Obsolete files 2
+Whenever a file is transcoded, if it was already present in the database but its
+name changed, following a rename pattern change, the old file is removed during
+that stage.
+
+## Toys
+AtOM requires a database to function. Now that we have a database containing
+various information about our media files, why not use it?
+AtOM comes with a small set of tools in the toys/ directory. These are
+documented in `toys/README`.
+
+# Shameless Self Promotion
+I am the author of free (Creative Commons CC-By-SA) music which you can stream
+for free, or buy to get high quality and bonuses from
+[Bandcamp](http://djblackred.bandcamp.com). If you like electronic music taking
+its inspiration from Trance, Drum & Bass, Ambient and (rarely) Free Jazz, please
+check it out!
+Downloads are available in FLAC, Ogg, MP3, and more, and includes the "source
+code" (sequencer files and the likes) for most tracks.
+I am receiving 80% of the money you'll spend, so you won't be feeding some
+greedy BigCorp producer or distributor.
+And if you don't like it, you can still spread the word to friends who may like.
+You can see this as a way to thank me for this piece of code.
+
+# Legal
+Some of the format and/or tool names cited above are trademarks belonging to
+their rightful owners. AtOM and its authors are not linked in any way to
+those companies or individuals. Said companies do not endorse nor support
+AtOM in any way.
diff --git a/TODO b/TODO
new file mode 100644
index 0000000..e5e27d4
--- /dev/null
+++ b/TODO
@@ -0,0 +1,5 @@
+Tag Guessing
+------------
+From a user-defined pattern, guess tags for unsupported formats/untagged files
+from the file path and file name.
+
diff --git a/atom b/atom
index a3f86b7..671ca4e 100755
--- a/atom
+++ b/atom
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
## Define exit codes
# General config errors [10-19]
@@ -13,6 +13,8 @@ EFMTINVPARM=49
# config structures
declare -A \
+ destinationenabled \
+ destinationascii \
destinationchannels \
destinationfat32compat \
destinationcopymime \
@@ -56,9 +58,26 @@ do
source "$function"
done
+help() {
+ cat <<-EOF
+ Options:
+ -c Load configuration file
+ -C Dump configuration and exit
+ -l Override max-load
+ -T override load-interval
+ -F Force re-generation of all files in
+
+ -B Create/update no more than files
+ -S Run setup
+ -h Show this text
+ -D Increase debug
+ -q Cron mode
+ EOF
+}
+
#parse arguments
OPTERR=0
-while getopts ':c:Cl:T:F:hD' opt
+while getopts ':c:Cl:T:F:B:ShDq' opt
do
case $opt in
c)
@@ -76,6 +95,12 @@ do
F)
forceall+=("$OPTARG")
;;
+ B)
+ maxbatch="$OPTARG"
+ ;;
+ S)
+ forcesetup=1
+ ;;
h)
help
exit 0
@@ -83,6 +108,9 @@ do
D)
(( debug++ ))
;;
+ q)
+ cron=1
+ ;;
:)
echo "-$OPTARG requires an argument"
help
@@ -96,32 +124,223 @@ do
esac
done
-#FIXME: check sanity
+askconf() {
+ if (( cron ))
+ then
+ echo 'Non-interactive, not running setup. Please run atom -S.' >&2
+ createconf=n
+ else
+ read -p"Create one now? [Y/n/q] " createconf
+ fi
+ case $createconf in
+ ''|[yY])
+ setup
+ ;;
+ [nNqQ])
+ echo "You need a configuration file. If you" \
+ "want to create it yourself, please" \
+ "read doc/config and doc/example.cfg." >&2
+ exit $ENOCFG
+ ;;
+ *)
+ echo "Come again?" >&2
+ askconf
+ ;;
+ esac
+}
if [ ! -f "$cffile" ]
then
- if [ ! -d ~/.atom ]
+ if [ ! -d "${cffile%/*}" ]
then
- mkdir -p ~/.atom
+ mkdir -p "${cffile%/*}"
fi
- sed "s:%HOME%:$HOME:" "$exampleconf" > "$cffile"
- cat >&2 <<-EOCfgNotice
- No configuration file found!
- An example file has been created as "${cffile/$HOME/~}".
- You should change it to your likings using you favorite editor.
-
- Bailing out.
- EOCfgNotice
- exit $ENOCFG
+ echo "No configuration file found!" >&2
+ askconf
fi
getConfig
+(( forcesetup )) && setup
+
set +H
+
+# Apply CLI overrides
+[ -n "$cliload" ] && maxload=$cliload
+[ -n "$cliltimer" ] && loadinterval=$cliltimer
+
(( debug || cfgdump )) && printConfig
(( cfgdump )) && exit
+# check sanity
+if [ ! -d "$tempdir" ] && ! mkdir -p "$tempdir"
+then
+ echo "[FATAL] Could not create temp directory $tempdir" >&2
+ (( sanityfail++ ))
+fi
+if [ ! -f "$database" ] && [ ! -d "${database%/*}" ] && ! mkdir -p "${database%/*}"
+then
+ echo "[FATAL] Directory holding database file does not exist and could" \
+ "not be created" >&2
+ (( sanityfail++ ))
+fi
+if [ ! -d "$sourcepath" ]
+then
+ echo "[FATAL] Source path $sourcepath does not exist or is not a directory" >&2
+ (( sanityfail++ ))
+fi
+if ! which sed >/dev/null
+then
+ echo "[FATAL] Required tool sed is not installed or not in PATH
+ I never thought this would actually hit someone..." >&2
+ (( sanityfail++ ))
+fi
+if ! which sox >/dev/null
+then
+ echo "[FATAL] Required tool sox is not installed or not in PATH" >&2
+ (( sanityfail++ ))
+fi
+if ! which ogginfo >/dev/null
+then
+ echo "[WARNING] Tool ogginfo (from vorbis-tools) is not" \
+ "installed or not in PATH
+ WebM metadata disabled" >&2
+ disableogginfo=1
+ (( sanitywarn++ ))
+fi
+if ! which soxi >/dev/null
+then
+ echo "[WARNING] Tool soxi (from sox) is not" \
+ "installed or not in PATH
+ Vorbis metadata disabled" >&2
+ disablesoxi=1
+ (( sanitywarn++ ))
+fi
+if (( oggencneeded )) && ! which oggenc >/dev/null
+then
+ echo "[WARNING] Tool oggenc (from vorbis-tools) is not" \
+ "installed or not in PATH
+ Vorbis targets disabled" >&2
+ disableoggenc=1
+ (( sanitywarn++ ))
+fi
+if ! which opusinfo >/dev/null
+then
+ echo "[WARNING] Tool opusinfo (from opus-tools) is not" \
+ "installed or not in PATH
+ Opus metadata disabled" >&2
+ disableopusinfo=1
+ (( sanitywarn++ ))
+fi
+if (( opusencneeded )) && ! which opusenc >/dev/null
+then
+ echo "[WARNING] Tool opusenc (from opus-tools) is not" \
+ "installed or not in PATH
+ Opus targets disabled" >&2
+ disableopusenc=1
+ (( sanitywarn++ ))
+fi
+if ! which opusdec >/dev/null
+then
+ echo "[WARNING] Tool opusdec (from opus-tools) is not" \
+ "installed or not in PATH
+ Opus support disabled" >&2
+ disableopusdec=1
+ (( sanitywarn++ ))
+fi
+if (( lameneeded )) && ! which lame >/dev/null
+then
+ echo "[WARNING] Tool lame is not installed or not in PATH
+ MP3 targets disabled" >&2
+ disablelame=1
+ (( sanitywarn++ ))
+fi
+if ! which metaflac >/dev/null
+then
+ echo "[WARNING] Tool metaflac (from FLAC) is not installed" \
+ "or not in PATH
+ FLAC metadata disabled" >&2
+ disableflac=1
+ (( sanitywarn++ ))
+fi
+if ! which mpcdec >/dev/null
+then
+ echo "[WARNING] Tool mpcdec (from Musepack) is not" \
+ "installed or not in PATH
+ Musepack support disabled" >&2
+ disablempcdec=1
+ (( sanitywarn++ ))
+fi
+if ! which mkvextract >/dev/null
+then
+ echo "[WARNING] Tool mkvextract (from MKVToolNix) is not" \
+ "installed or not in PATH
+ WebM metadata disabled
+ WebM support disabled" >&2
+ disablemkvextract=1
+ (( sanitywarn++ ))
+fi
+if ! which ffprobe >/dev/null
+then
+ echo "[WARNING] Tool ffprobe (from FFmpeg) is not installed or not in PATH
+ Video metadata disabled
+ MPEG metadata disabled
+ MusePack metadata disabled
+ Unknown format metadata disabled" >&2
+ disableffprobe=1
+ (( sanitywarn++ ))
+fi
+if ! which ffmpeg >/dev/null
+then
+ echo "[WARNING] Tool ffmpeg is not installed or not in PATH
+ Video support disabled" >&2
+ disablevideo=1
+ (( sanitywarn++ ))
+fi
+if (( textunidecodeneeded )) && ! perl -MText::Unidecode -e 'exit;' 2>/dev/null
+then
+ echo "[WARNING] Perl module Text::Unidecode is not available
+ Renaming to ASCII-only disabled" >&2
+ unset destinationascii
+ destinationascii=0
+ textunidecodeneeded=0
+ (( sanitywarn++ ))
+fi
+if (( sanityfail ))
+then
+ echo "
+Sanity checks raised ${sanitywarn:-0} warnings, $sanityfail failures. Dying now." >&2
+ exit $ESANITY
+elif (( sanitywarn ))
+then
+ echo "
+Sanity checks raised $sanitywarn warnings... Hit Control-C to abort." >&2
+ if ! (( cron ))
+ then
+ timeout=$(( sanitywarn * 10 ))
+ echo -n "Starting in $(printf %3i $timeout)" \
+ $'seconds...\b\b\b\b\b\b\b\b\b\b\b' >&2
+ while (( timeout ))
+ do
+ echo -n $'\b\b\b'"$(printf %3i $timeout)" >&2
+ sleep 1
+ (( timeout-- ))
+ done
+ echo -en "\r\033[K"
+ fi
+fi
+
openDatabase
+for destination in "${destinations[@]}"
+do
+ if (( ${destinationenabled["$destination"]} ))
+ then
+ Update destinations enabled 1 <<<"name = $destination"
+ else
+ Update destinations enabled 0 <<<"name = $destination"
+ fi
+done
+
createDestinations
getFiles
@@ -131,21 +350,37 @@ updateMimes
removeObsoleteFiles
echo '
- SELECT id,
- filename
+ SELECT COUNT(id)
FROM destination_files
- WHERE source_file_id is NULL;
+ WHERE source_file_id is NULL;' >&3
- SELECT "AtOM:NoMoreFiles";
-' >&3
+read -u4 removecount
+until (( ${#removefile[@]} == removecount ))
+do
+ echo '
+ SELECT id,
+ filename
+ FROM destination_files
+ WHERE source_file_id is NULL
+ LIMIT 500 OFFSET '${#removefile[@]}';
+
+ SELECT "AtOM:NoMoreFiles";
+ ' >&3
+
+ read -u4 line
+ until [[ $line == AtOM:NoMoreFiles ]]
+ do
+ removefile[${line%::AtOM:SQL:Sep::*}]="${line%::AtOM:SQL:Sep::*}"
+ read -u4 line
+ done
+done
deleted=0
removed=0
-read -u4 line
-until [[ $line == AtOM:NoMoreFiles ]]
+echo 'BEGIN TRANSACTION;' >&3
+for id in ${!removefile[@]}
do
- id=${line%::AtOM:SQL:Sep::*}
- filename=${line#*::AtOM:SQL:Sep::}
+ filename=${removefile[id]}
if [ -n "$filename" ]
then
if rm -f "$filename"
@@ -157,17 +392,44 @@ do
Delete destination_files <<<"id = $id"
(( ++removed ))
fi
- read -u4 line
+ if (( (deleted + removed) % 1000 == 0 ))
+ then
+ echo 'COMMIT;BEGIN TRANSACTION;' >&3
+ fi
+ (( cron )) || echo -en "\rClean obsolete data: $(((deleted+removed)*100/removecount))%"
done
-echo "Suppressed $deleted files, $removed removed from database"
+echo 'COMMIT;' >&3
+(( cron )) || echo -n $'\r'
+echo -n "Suppressed $deleted files, $removed removed from database"
+(( cron )) || echo -ne "\033[K"
+echo
+unset removecount deleted removed removefile
updateTags
+for forcedest in "${forceall[@]}"
+do
+ if forcedestid=$(Select destinations id <<<"name = $forcedest")
+ then
+ echo "Resetting destination files timestamps on" \
+ "$forcedest ($forcedestid)..."
+ Update destination_files last_change 0 \
+ <<<"destination_id = $forcedestid"
+ else
+ echo "Destination $forcedest does not exist!" >&2
+ fi
+done
+
echo '
CREATE TEMPORARY TABLE tasks(
id INTEGER PRIMARY KEY,
+ requires INTEGER,
+ required INTEGER,
+ status INTEGER NOT NULL,
key TEXT UNIQUE,
rename_pattern TEXT,
+ fat32compat INTEGER,
+ ascii INTEGER,
source_file INTEGER,
fileid INTEGER,
filename TEXT,
@@ -201,15 +463,53 @@ echo '
cmd_arg27 TEXT,
cmd_arg28 TEXT,
cmd_arg29 TEXT,
- requires INTEGER,
- required INTEGER,
- status INTEGER NOT NULL,
+ cmd_arg30 TEXT,
+ cmd_arg31 TEXT,
+ cmd_arg32 TEXT,
+ cmd_arg33 TEXT,
+ cmd_arg34 TEXT,
+ cmd_arg35 TEXT,
+ cmd_arg36 TEXT,
+ cmd_arg37 TEXT,
+ cmd_arg38 TEXT,
+ cmd_arg39 TEXT,
+ cmd_arg40 TEXT,
+ cmd_arg41 TEXT,
+ cmd_arg42 TEXT,
+ cmd_arg43 TEXT,
+ cmd_arg44 TEXT,
+ cmd_arg45 TEXT,
+ cmd_arg46 TEXT,
+ cmd_arg47 TEXT,
+ cmd_arg48 TEXT,
+ cmd_arg49 TEXT,
+ cmd_arg50 TEXT,
+ cmd_arg51 TEXT,
+ cmd_arg52 TEXT,
+ cmd_arg53 TEXT,
+ cmd_arg54 TEXT,
+ cmd_arg55 TEXT,
+ cmd_arg56 TEXT,
+ cmd_arg57 TEXT,
+ cmd_arg58 TEXT,
+ cmd_arg59 TEXT,
cleanup TEXT,
FOREIGN KEY(requires) REFERENCES tasks(id)
ON DELETE SET NULL
);
CREATE INDEX tasks_by_key ON tasks ( key );
CREATE INDEX tasks_by_sourcefile ON tasks ( source_file );
+
+ CREATE TEMPORARY TRIGGER fail_depends
+ AFTER UPDATE OF
+ status
+ ON
+ tasks
+ WHEN
+ NEW.status=2
+ BEGIN
+ UPDATE tasks SET status=2 WHERE requires=NEW.id;
+ END;
' >&3
echo '
@@ -224,11 +524,17 @@ echo '
ON mime_type_actions.id = source_files.mime_type
INNER JOIN tags
ON source_files.id = tags.source_file
- WHERE CAST(destination_files.last_change AS TEXT)
+ WHERE destinations.enabled = 1
+ AND CAST(destination_files.last_change AS TEXT)
<> CAST(source_files.last_change AS TEXT)
AND mime_type_actions.destination_id = destinations.id
AND mime_type_actions.action = 1;' >&3
read -u4 filecount
+if [ -n "$maxbatch" ] && (( maxbatch < filecount ))
+then
+ (( togo = filecount - maxbatch ))
+ filecount=$maxbatch
+fi
echo '
SELECT
source_files.id,
@@ -236,6 +542,7 @@ echo '
mime_type_actions.mime_text,
destinations.name,
destination_files.id,
+ tags.depth,
tags.rate,
tags.channels,
tags.bitrate,
@@ -259,11 +566,14 @@ echo '
ON mime_type_actions.id = source_files.mime_type
INNER JOIN tags
ON source_files.id = tags.source_file
- WHERE CAST(destination_files.last_change AS TEXT)
+ WHERE destinations.enabled = 1
+ AND CAST(destination_files.last_change AS TEXT)
<> CAST(source_files.last_change AS TEXT)
AND mime_type_actions.destination_id = destinations.id
- AND mime_type_actions.action = 1;
-
+ AND mime_type_actions.action = 1
+ ORDER BY source_files.id' >&3
+(( maxbatch )) && echo "LIMIT $maxbatch" >&3
+echo ';
SELECT "AtOM:NoMoreFiles";' >&3
read -u4 line
while ! [[ $line = AtOM:NoMoreFiles ]]
@@ -271,7 +581,9 @@ do
decodefiles+=("$line::AtOM:SQL:Sep::")
read -u4 line
done
-echo -n 'Creating tasks... '
+(( cron )) || echo -n 'Creating tasks... '
+
+(( textunidecodeneeded )) && ascii
echo 'BEGIN TRANSACTION;' >&3
for line in "${decodefiles[@]}"
@@ -286,6 +598,8 @@ do
rest=${rest#*::AtOM:SQL:Sep::}
destfileid=${rest%%::AtOM:SQL:Sep::*}
rest=${rest#*::AtOM:SQL:Sep::}
+ bitdepth=${rest%%::AtOM:SQL:Sep::*}
+ rest=${rest#*::AtOM:SQL:Sep::}
rate=${rest%%::AtOM:SQL:Sep::*}
rest=${rest#*::AtOM:SQL:Sep::}
channels=${rest%%::AtOM:SQL:Sep::*}
@@ -312,9 +626,22 @@ do
rest=${rest#*::AtOM:SQL:Sep::}
performer=${rest%%::AtOM:SQL:Sep::*}
unset rest
+ case ${destinationformat["$destination"]} in
+ vorbis) (( disableoggenc )) && continue ;;
+ opus) (( disableopusenc )) && continue ;;
+ mp3) (( disablelame )) && continue ;;
+ esac
decodeFile
getDestDir
getDestFile
+ for copy_ext in "${destinationcopyext[@]}"
+ do
+ if [[ $filename =~ '.*\.'$copy_ext'$' ]]
+ then
+ copied=1
+ break
+ fi
+ done
if (( copied ))
then
copyFiles_matching
@@ -325,6 +652,7 @@ do
album \
albumartist \
artist \
+ bitdepth \
bitrate \
channels \
commandline \
@@ -350,7 +678,11 @@ do
tmpfile
done
echo 'COMMIT;' >&3
-echo -e "\rCreated ${count:-0} tasks for $filecount files (${copies:-0} immediate copies)"
+(( cron )) || echo -n $'\r'
+echo "Created ${count:-0} tasks for $filecount files ${togo:+($togo left) }(${copies:-0} immediate copies)"
+
+# remove perl unicode to ascii coprocess
+(( textunidecodeneeded )) && eval exec "${toascii[1]}>&-"
concurrency=$(( maxload / 2 ))
(( concurrency )) || concurrency=1
@@ -362,23 +694,11 @@ remaining=$taskcount
failed=0
while (( (remaining || ${#workers[@]}) && ! quit ))
do
- if read -n 1 -t 0.1 userinput
- then
- case $userinput in
- '+')
- ((maxload++))
- ;;
- '-')
- ((--maxload)) || ((maxload=1))
- ;;
- [qQ])
- quit=1
- ;;
- esac
- fi
read humanload garbage < /proc/loadavg
load=${humanload%.*}
- if [ -z "$quit" ] && (( $(date +%s)-concurrencychange >= loadinterval ))
+ if [ -z "$quit" ] \
+ && (( ! pause )) \
+ && (( $(date +%s)-concurrencychange >= loadinterval ))
then
if (( concurrency > 1 )) \
&& (( load > maxload ))
@@ -393,14 +713,20 @@ do
fi
checkworkers
cleaner
- master
- if (( ran ))
+ (( pause )) || master
+ if (( ran - failed ))
then
currenttime=$(date +%s)
+ if (( pause ))
+ then
+ (( runtime = pausestart - starttime - pausedtime ))
+ else
+ (( runtime = currenttime - starttime - pausedtime ))
+ fi
avgduration=$((
- ((currenttime - starttime) * 1000)
+ ( runtime * 1000)
/
- ran
+ ( ran - failed )
))
secsremaining=$(( remaining * avgduration / 1000 ))
(( days =
@@ -438,7 +764,7 @@ do
${seconds:-0} seconds" \
+'%d/%m %H:%M:%S'
)"
- printf \
+ (( cron )) || printf \
"\r$fmtload $fmtworkers $fmtprogress $fmttime $eta\033[K"\
$humanload \
$maxload \
@@ -453,12 +779,21 @@ do
${minutes:-0} \
${seconds:-0} \
${avgdsec:-0}.${avgdmsec:-0}
+ if (( pause ))
+ then
+ if (( active ))
+ then
+ echo -n ' | (pause)'
+ else
+ echo -n ' | PAUSED'
+ fi
+ fi
done
unset count
endtime=$(date +%s)
-(( elapsedseconds = endtime - starttime ))
+(( elapsedseconds = endtime - starttime - pausedtime ))
(( days =
elapsedseconds
/
@@ -480,8 +815,98 @@ endtime=$(date +%s)
( ( ( ( days*24 + hours ) *60 ) + minutes ) *60 )
)) || true
-echo -e "\rRan ${ran:=0} tasks, $failed of which failed, in $days" \
- "days, $hours hours, $minutes minutes and $seconds seconds.\033[K"
+(( cron )) || echo -n $'\r'
+echo -n "Ran ${ran:=0} tasks, $failed of which failed, in $days" \
+ "days, $hours hours, $minutes minutes and $seconds seconds."
+(( cron )) || echo -en "\033[K"
+echo
+if (( failed ))
+then
+ echo $'\nFailed tasks:\n'
+ echo '
+ SELECT source_files.filename,
+ tasks.cmd_arg0,
+ tasks.cmd_arg1,
+ tasks.cmd_arg2,
+ tasks.cmd_arg3,
+ tasks.cmd_arg4,
+ tasks.cmd_arg5,
+ tasks.cmd_arg6,
+ tasks.cmd_arg7,
+ tasks.cmd_arg8,
+ tasks.cmd_arg9,
+ tasks.cmd_arg10,
+ tasks.cmd_arg11,
+ tasks.cmd_arg12,
+ tasks.cmd_arg13,
+ tasks.cmd_arg14,
+ tasks.cmd_arg15,
+ tasks.cmd_arg16,
+ tasks.cmd_arg17,
+ tasks.cmd_arg18,
+ tasks.cmd_arg19,
+ tasks.cmd_arg20,
+ tasks.cmd_arg21,
+ tasks.cmd_arg22,
+ tasks.cmd_arg23,
+ tasks.cmd_arg24,
+ tasks.cmd_arg25,
+ tasks.cmd_arg26,
+ tasks.cmd_arg27,
+ tasks.cmd_arg28,
+ tasks.cmd_arg29,
+ tasks.cmd_arg30,
+ tasks.cmd_arg31,
+ tasks.cmd_arg32,
+ tasks.cmd_arg33,
+ tasks.cmd_arg34,
+ tasks.cmd_arg35,
+ tasks.cmd_arg36,
+ tasks.cmd_arg37,
+ tasks.cmd_arg38,
+ tasks.cmd_arg39,
+ tasks.cmd_arg40,
+ tasks.cmd_arg41,
+ tasks.cmd_arg42,
+ tasks.cmd_arg43,
+ tasks.cmd_arg44,
+ tasks.cmd_arg45,
+ tasks.cmd_arg46,
+ tasks.cmd_arg47,
+ tasks.cmd_arg48,
+ tasks.cmd_arg49,
+ tasks.cmd_arg50,
+ tasks.cmd_arg51,
+ tasks.cmd_arg52,
+ tasks.cmd_arg53,
+ tasks.cmd_arg54,
+ tasks.cmd_arg55,
+ tasks.cmd_arg56,
+ tasks.cmd_arg57,
+ tasks.cmd_arg58,
+ tasks.cmd_arg59
+ FROM tasks
+ INNER JOIN source_files
+ ON tasks.source_file=source_files.id
+ WHERE tasks.status = 2
+ AND requires is NULL;
+
+ SELECT "AtOM:NoMoreFiles";' >&3
+ read -u4 line
+ while ! [[ $line = AtOM:NoMoreFiles ]]
+ do
+ failedtasks+=("$line")
+ read -u4 line
+ done
+ for line in "${failedtasks[@]}"
+ do
+ echo "${line%%::AtOM:SQL:Sep::*}"
+ line="${line#*::AtOM:SQL:Sep::}"
+ line="${line//::AtOM:SQL:Sep::/ }"
+ echo $'\t'"${line/+( )$/}"
+ echo
+ done
+fi
if [ -n "$quit" ]
then
@@ -489,7 +914,6 @@ then
exit
fi
-#set -x
for destination in "${!destinationpath[@]}"
do
echo '
@@ -517,27 +941,40 @@ do
INNER JOIN source_files
ON destination_files.source_file_id
=source_files.id
+ INNER JOIN mime_actions
+ ON source_files.mime_type
+ =mime_actions.mime_type
WHERE destinations.name="'"$destination"'"
AND (destination_files.rename_pattern
!=
-"'"${destinationrenamepath[$destination]}/${destinationrename[$destination]}:${destinationfat32compat["$destination"]}"'"
+"'"${destinationrenamepath[$destination]}/${destinationrename[$destination]}"'"
+ OR fat32compat != '${destinationfat32compat["$destination"]}'
+ OR ascii != '${destinationascii["$destination"]}'
OR destination_files.rename_pattern is NULL)
AND destination_files.last_change > 0
+ AND mime_actions.action=1
;
SELECT "AtOM:NoMoreFiles";
' >&3
read -u4 line
- if [[ $line != AtOM:NoMoreFiles ]]
+ while [[ $line != AtOM:NoMoreFiles ]]
+ do
+ renamefiles+=("$line")
+ read -u4 line
+ done
+ if (( ${#renamefiles[@]} ))
then
case "${destinationformat[$destination]}" in
'mp3') extension=mp3 ;;
'opus') extension=opus ;;
'vorbis') extension=ogg ;;
esac
- echo -n "$destination: rename pattern changed, renaming files... "
- while [[ $line != AtOM:NoMoreFiles ]]
+ (( cron )) || echo -en "$destination: rename pattern changed, renaming files...\033[K"
+ (( textunidecodeneeded )) && ascii
+ echo 'BEGIN TRANSACTION;' >&3
+ for line in "${renamefiles[@]}"
do
oldfilename=${line%%::AtOM:SQL:Sep::*}
rest=${line#*::AtOM:SQL:Sep::}'::AtOM:SQL:Sep::'
@@ -570,23 +1007,39 @@ do
getDestDir
getDestFile
destfilename="$destdir/$destfile.$extension"
- if [[ $oldfilename != $destfilename ]]
+ progressSpin
+ if [[ "$oldfilename" != "$destfilename" ]]
then
mv "$oldfilename" "$destfilename"
- progressSpin
+ (( changedcount++ ))
+ commit=1
fi
echo "UPDATE destination_files" \
"SET filename=\"${destfilename//\"/\"\"}\"," \
" rename_pattern=" \
-"\"${destinationrenamepath[$destination]}/${destinationrename[$destination]}:${destinationfat32compat["$destination"]}\"" \
+"\"${destinationrenamepath[$destination]}/${destinationrename[$destination]}\","\
+ " fat32compat=" \
+"${destinationfat32compat["$destination"]}," \
+ " ascii=" \
+"${destinationascii["$destination"]}" \
"WHERE id=$destfileid;" \
>&3
+ if (( commit ))
+ then
+ echo $'COMMIT;\nBEGIN TRANSACTION;' >&3
+ unset commit
+ fi
fi
- read -u4 line
done
- echo -e $'\r'"$destination: Renamed ${count:-0} files\033[K"
+ # remove perl unicode to ascii coprocess
+ (( textunidecodeneeded )) && eval exec "${toascii[1]}>&-"
+ echo 'COMMIT;' >&3
+ (( cron )) || echo -n $'\r'
+ echo -n "$destination: Renamed ${changedcount:-0} files"
+ (( cron )) || echo -en "\033[K"
+ echo
fi
- unset count
+ unset count changedcount renamefiles
done
copyFiles_action
@@ -601,7 +1054,7 @@ echo '
SELECT "AtOM:NoMoreFiles";
' >&3
-echo -n 'Removing obsolete files... '
+(( cron )) || echo -n 'Removing obsolete files... '
lines=()
read -u4 line
while [[ $line != AtOM:NoMoreFiles ]]
@@ -622,10 +1075,13 @@ do
fi
Update destination_files old_filename NULL <<<"id = $id"
(( count++ ))
- printf '\b\b\b\b%3i%%' $(( (100 * count) / ${#lines[@]} ))
+ (( cron )) || printf '\b\b\b\b%3i%%' $(( (100 * count) / ${#lines[@]} ))
done
echo 'COMMIT;' >&3
-echo -e "\rRemoved ${count:-0} obsolete files.\033[K"
+(( cron )) || echo -n $'\r'
+echo -n "Removed ${count:-0} obsolete files."
+(( cron )) || echo -en "\033[K"
+echo
echo "Purging empty directories."
for path in "${destinationpath[@]}"
diff --git a/doc/config b/doc/config
index 4aa2f32..9068e64 100644
--- a/doc/config
+++ b/doc/config
@@ -1,5 +1,7 @@
-On first launch, AtOM will create an example configuration file for you. This is
-only an example, suitable to *my* needs (I'm lazy).
+On first launch, AtOM will create a configuration file for you. The recommended
+way of changing your configuration is by running
+$ atom -S.
+
Default config file is ~/.atom/atom.cfg.
The file is divided in sections, beginning with []. The section
@@ -68,13 +70,18 @@ Sections:
%{track},
%{year}.
Untagged files or files in unrecognized formats will not be changed.
+ Surrounding a field with [] makes it optional, meaning renaming will still
+ happen if the corresponding tag is not defined.
* fat32compat /: Rename files for compatibility with FAT32
filesystems.
+ * ascii-only /: Rename files for compatibility with ASCII-only
+ systems. Uses Perl with Text::Unidecode to replace cyrillic or kanji
+ with an ASCII representation.
* skip_mime-type : Files with mime-type will not
be included in that destination. For more than one mime-type, use multiple
times, as needed. The '*' character is a wildcard.
* copy_mime-type : Same as skip_mime-type, except that files
- matching will be copied as-is to tha destination. E.g. image/* will copy
+ matching will be copied as-is to the destination. E.g. image/* will copy
covers and other images to the destination. In fact, AtOM will try to use
hard links instead of copies.
* channels : Files with more than channels will be
diff --git a/doc/performances.txt b/doc/performances.txt
new file mode 100644
index 0000000..45ca95d
--- /dev/null
+++ b/doc/performances.txt
@@ -0,0 +1,29 @@
+# TEST DATA
+
+155GB, 14430 files in 2716 directories and subdirectories:
+$ du -sh /var/lib/mpd/music
+155G /var/lib/mpd/music
+$ find /var/lib/mpd/music -type d|wc -l
+2716
+$ find /var/lib/mpd/music -type f|wc -l
+14430
+
+# SCANNING
+
+Initial scan takes 5 minutes, probably because of mime-type detection:
+$ time ./atom
+[...]
+14430 files found, 14430 new or changed.
+
+real 5m4.144s
+user 0m19.821s
+sys 0m17.393s
+
+A second scan takes less than 14 seconds:
+$ time ./atom
+[...]
+14430 files found, 0 new or changed.
+
+real 0m13.770s
+user 0m11.169s
+sys 0m1.844s
diff --git a/lib/config/getConfig b/lib/config/get
similarity index 90%
rename from lib/config/getConfig
rename to lib/config/get
index cbc9b5f..63e0e50 100644
--- a/lib/config/getConfig
+++ b/lib/config/get
@@ -19,6 +19,7 @@ getConfig() {
context=Destination
destination="${key#[}"
destination="${destination%]}"
+ destinations+=("${destination%]}")
;;
*)
getConfig$context
diff --git a/lib/config/getConfigGeneral b/lib/config/getConfigGeneral
deleted file mode 100644
index ec183ab..0000000
--- a/lib/config/getConfigGeneral
+++ /dev/null
@@ -1,79 +0,0 @@
-#!/bin/bash
-getConfigGeneral() {
- case $key in
- 'max-load')
- expr='^[0-9]*$'
- if [[ $value =~ $expr ]]
- then
- maxload="$value"
- else
- echo "Invalid max-load value: $value" >&2
- exit $ELOAD
- fi
- unset expr
- ;;
- 'load-interval')
- expr='^[0-9]*$'
- if [[ $value =~ $expr ]]
- then
- loadinterval="$value"
- else
- echo "Invalid load-interval value: $value" >&2
- exit $EINTERVAL
- fi
- unset expr
- ;;
- 'ionice')
- read class niceness <<<"$value"
- case $class in
- 1)
- # real-time class, only root can do that
- if (( UID ))
- then
- echo "IO class 'realtime' is"\
- "not available to unprivileged"\
- "users" >&2
- exit $EIONICE
- fi
- if [ -n "$niceness" ] \
- && (( niceness >= 0 && niceness <= 7 ))
- then
- ionice="ionice -c1 -n$niceness "
- else
- echo "Invalid IO priority"\
- "'$niceness'" >&2
- exit $EIONICE
- fi
- ;;
- 2)
- if [ -n "$niceness" ] \
- && (( niceness >= 0 && niceness <= 7 ))
- then
- ionice="ionice -c2 -n$niceness "
- else
- echo "Invalid IO priority"\
- "'$niceness'" >&2
- exit $EIONICE
- fi
- ;;
- 3)
- ionice="ionice -c3 "
- ;;
- *)
- echo "Invalid ionice parameters $value"\
- >&2
- exit $EIONICE
- ;;
- esac
- ;;
- 'temporary-directory')
- tempdir="$value"
- ;;
- 'database')
- database="$value"
- ;;
- debug)
- (( value > debug )) && debug=$value
- ;;
- esac
-}
diff --git a/lib/config/getConfigDestination b/lib/config/getDestination
similarity index 81%
rename from lib/config/getConfigDestination
rename to lib/config/getDestination
index a6fa6a8..5284c75 100644
--- a/lib/config/getConfigDestination
+++ b/lib/config/getDestination
@@ -1,6 +1,9 @@
#!/bin/bash
getConfigDestination() {
case "$key" in
+ 'enabled')
+ destinationenabled["$destination"]="$value"
+ ;;
'path')
destinationpath["$destination"]="$value"
;;
@@ -8,15 +11,24 @@ getConfigDestination() {
case "$value" in
'mp3')
destinationformat["$destination"]=mp3
+ lameneeded=1
+ # MP3 can't handfle more than 2 channels
+ [[ -z ${destinationchannels["$destination"]} ]] \
+ && destinationchannels["$destination"]=2
;;
'opus')
destinationformat["$destination"]=opus
+ opusencneeded=1
;;
'vorbis')
destinationformat["$destination"]=vorbis
+ oggencneeded=1
+ ;;
+ 'copy')
+ destinationformat["$destination"]=copy
;;
*)
- echo "Unsupported destination format: $value" >2&
+ echo "Unsupported destination format: $value" >&2
exit $EFORMAT
;;
esac
@@ -151,12 +163,31 @@ getConfigDestination() {
;;
esac
;;
+ 'ascii-only')
+ case $value in
+ 'true'|'on'|'yes')
+ destinationascii["$destination"]=1
+ textunidecodeneeded=1
+ ;;
+ 'false'|'off'|'no')
+ destinationascii["$destination"]=0
+ ;;
+ *)
+ echo "ascii-only takes values:" \
+ "'yes' ,'true' ,'on', 'no', 'false',"\
+ "'off'"
+ ;;
+ esac
+ ;;
'skip_mime-type')
destinationskipmime[$destination]="${destinationskipmime[$destination]:+${destinationskipmime[$destination]}|}$value"
;;
'copy_mime-type')
destinationcopymime[$destination]="${destinationcopymime[$destination]:+${destinationcopymime[$destination]}|}$value"
;;
+ 'copy_extension')
+ destinationcopyext[$destination]="${destinationcopyext[$destination]:+${destinationcopyext[$destination]}|}$value"
+ ;;
'higher-than')
expr='^[0-9]*$'
if ! [[ $value =~ $expr ]]
diff --git a/lib/config/getGeneral b/lib/config/getGeneral
new file mode 100644
index 0000000..1e7797d
--- /dev/null
+++ b/lib/config/getGeneral
@@ -0,0 +1,82 @@
+#!/bin/bash
+getConfigGeneral() {
+ case $key in
+ 'max-load')
+ expr='^[0-9]*$'
+ if [[ $value =~ $expr ]]
+ then
+ maxload="$value"
+ else
+ echo "Invalid max-load value: $value" >&2
+ exit $ELOAD
+ fi
+ unset expr
+ ;;
+ 'load-interval')
+ expr='^[0-9]*$'
+ if [[ $value =~ $expr ]]
+ then
+ loadinterval="$value"
+ else
+ echo "Invalid load-interval value: $value" >&2
+ exit $EINTERVAL
+ fi
+ unset expr
+ ;;
+ 'ionice')
+ if which ionice >/dev/null
+ then
+ read class niceness <<<"$value"
+ case $class in
+ 1)
+ # real-time class, only root can do that
+ if (( UID ))
+ then
+ echo "IO class 'realtime' is"\
+ "not available to unprivileged"\
+ "users" >&2
+ exit $EIONICE
+ fi
+ if [ -n "$niceness" ] \
+ && (( niceness >= 0 && niceness <= 7 ))
+ then
+ ionice="ionice -c1 -n$niceness "
+ else
+ echo "Invalid IO priority"\
+ "'$niceness'" >&2
+ exit $EIONICE
+ fi
+ ;;
+ 2)
+ if [ -n "$niceness" ] \
+ && (( niceness >= 0 && niceness <= 7 ))
+ then
+ ionice="ionice -c2 -n$niceness "
+ else
+ echo "Invalid IO priority"\
+ "'$niceness'" >&2
+ exit $EIONICE
+ fi
+ ;;
+ 3)
+ ionice="ionice -c3 "
+ ;;
+ *)
+ echo "Invalid ionice parameters $value"\
+ >&2
+ exit $EIONICE
+ ;;
+ esac
+ fi
+ ;;
+ 'temporary-directory')
+ tempdir="$value"
+ ;;
+ 'database')
+ database="$value"
+ ;;
+ debug)
+ (( value > debug )) && debug=$value
+ ;;
+ esac
+}
diff --git a/lib/config/getConfigSource b/lib/config/getSource
similarity index 100%
rename from lib/config/getConfigSource
rename to lib/config/getSource
diff --git a/lib/config/print b/lib/config/print
index 0a4fea4..db25e0e 100644
--- a/lib/config/print
+++ b/lib/config/print
@@ -4,51 +4,58 @@ printConfig() {
echo "General|Config file|$cffile"
[ -n "$ionice" ] && echo "|IO Nice|$ionice"
cat <<-EOF
- |Load|$maxload
- |Load Interval|$loadinterval
- |Temp Dir|$tempdir
- |Database|$database
- |Debug|$debug
+ |Load|$maxload
+ |Load Interval|$loadinterval
+ |Temp Dir|$tempdir
+ |Database|$database
+ |Debug|$debug
+
Source|Path|$sourcepath
EOF
for prune_expression in "${skippeddirectories[@]}"
do
(( printed )) \
- && echo -n '||' \
- || echo -n '|Skipped directories|'
+ && echo -n ' | |' \
+ || echo -n ' |Skipped directories|'
echo "$prune_expression"
printed=1
done
unset printed
- for destination in ${!destinationpath[@]}
+ for destination in ${destinations[@]}
do
cat <<-EOF
+
$destination|Path|${destinationpath["$destination"]}
- |Format|${destinationformat["$destination"]}
- |Quality|${destinationquality["$destination"]}
+ |Enabled|${destinationenabled["$destination"]}
+ |Format|${destinationformat["$destination"]}
+ |Quality|${destinationquality["$destination"]}
EOF
if [[ ${destinationformat["$destination"]} == opus ]]
then
- echo "|Expected loss|${destinationloss["$destination"]}"
+ echo " |Expected loss|${destinationloss["$destination"]}"
elif [[ ${destinationformat["$destination"]} == mp3 ]]
then
- echo "|Prevent resampling|${destinationnoresample["$destination"]}"
+ echo " |Prevent resampling|${destinationnoresample["$destination"]}"
fi
cat <<-EOF
- |Normalize|${destinationnormalize["$destination"]}
- |Channels|${destinationchannels["$destination"]}
- |Frequency|${destinationfrequency["$destination"]}
- |Higher than|${destinationmaxbps["$destination"]}
- |Fat32 Compat.|${destinationfat32compat["$destination"]}
- |Path Change|${destinationrenamepath["$destination"]}
- |File Rename|${destinationrename["$destination"]}
+ |Normalize|${destinationnormalize["$destination"]}
+ |Channels|${destinationchannels["$destination"]}
+ |Frequency|${destinationfrequency["$destination"]}
+ |Higher than|${destinationmaxbps["$destination"]}
+ |Fat32 Compat.|${destinationfat32compat["$destination"]}
+ |ASCII Compat.|${destinationascii["$destination"]}
+ |Path Change|${destinationrenamepath["$destination"]}
+ |File Rename|${destinationrename["$destination"]}
EOF
[ -n "${destinationskipmime["$destination"]}" ] \
- && echo "|Skipped mime-types|${destinationskipmime["$destination"]//\|/
-||}"
- [ -n "${destinationmskipime["$destination"]}" ] \
- && echo "|Copied mime-types|${destinationcopymime["$destination"]//\|/
-||}"
+ && echo " |Skipped mime-types|${destinationskipmime["$destination"]//\|/
+| | |}"
+ [ -n "${destinationcopymime["$destination"]}" ] \
+ && echo " |Copied mime-types|${destinationcopymime["$destination"]//\|/
+| | |}"
+ [ -n "${destinationcopyext["$destination"]}" ] \
+ && echo " |Copied extensions|${destinationcopyext["$destination"]//\|/
+| | |}"
done
- }|column -t -s'|' -n
+ }|column -t -s'|'
}
diff --git a/lib/config/write b/lib/config/write
new file mode 100644
index 0000000..7da7b3a
--- /dev/null
+++ b/lib/config/write
@@ -0,0 +1,262 @@
+#!/bin/bash
+
+writeConfig() {
+ cat <<-EOCfg
+[general]
+# This section contains parameters of the program itself.
+
+# * max-load : Integer. Defines how parallel processing will behave. AtOM
+# will try to keep the 1 minute load average between and +1 by
+# adjusting concurrency.
+# Initial concurrency will be set to half of that value.
+max-load $maxload
+
+# * load-interval : Integer. How often should we check the load average
+# and adjust concurrency. Set this too low, and concurrency may be increased
+# too quickly. Set this too high, and AtOM will not adapt quickly enough to
+# load increase. In both cases, your hard drive will suffer. In my
+# experience, 30 seconds is a good value.
+load-interval $loadinterval
+
+# * ionice [niceness]: IO-hungry processes will be run with ionice class
+# and niceness [niceness] (if applicable). See man ionice for details.
+ionice $class $niceness
+
+# * temporary-directory : String. Name speaks for itself: this is
+# where FIFOs (for communicating with sqlite) and temporary WAVE files will
+# be created. Note that debug logs (if enabled) will go there too.
+temporary-directory $tempdir
+
+# * database : String. Where the SQLite database should be stored.
+database $database
+
+# * debug : Integer.
+#debug 1
+
+
+[source]
+# This section defines where are the files you want transcoded.
+
+# * path : String. The root of your collection.
+# Default: /var/lib/mpd/music
+path $sourcepath
+
+# * skip : String. Files in will be ignored. Note that
+# can be any expression accepted by find.
+ EOCfg
+ for dir in "${skippeddirectories[@]}"
+ do
+ echo $'skip\t\t\t'"$dir"
+ done
+ cat <<-EOCfg
+
+
+ EOCfg
+ for destination in "${destinations[@]}"
+ do
+ cat <<-EOCfg
+[$destination]
+# Each section not named 'general' or 'source' will define a new destination.
+
+# Common parameters:
+# Mandatory parameters:
+# * enabled: Whether or not to treat this destination (1=tue/0=false)
+enabled 1
+
+# * path: Where files will be written
+path ${destinationpath["$destination"]}
+
+# * format: copy, ogg, opus or mp3. Other formats may appear in the future -
+# feel free to implement your preferred format.
+format ${destinationformat["$destination"]}
+
+ EOCfg
+ case "${destinationformat["$destination"]}" in
+ vorbis)
+ cat <<-EOCfg
+# Ogg parameters:
+# * quality : The quality parameter of oggenc. See man oggenc for
+# more info. This is the only mode supported and planned. Still, if you want
+# to be able to use bitrate settings, feel free to fork and file a pull
+# request.
+quality ${destinationquality["$destination"]}
+
+
+ EOCfg
+ ;;
+ opus)
+ cat <<-EOCfg
+# Opus parameters:
+# * bitrate : Set (VBR) bitrate to . Note that while Opus
+# allows for decimal values, AtOM does not. The reason for this is simple:
+# we do numeric comparisons, and Bash only manipulates integers.
+bitrate ${destinationquality["$destination"]}
+
+# * loss : If you intend to stream the resulting files over an
+# unreliable protocol, you may want to make use of Opus' Forward Error
+# Correction algorythm. See the Opus-codec.org website for details.
+# Default: 0
+loss ${destinationloss["$destination"]:-0}
+
+
+ EOCfg
+ ;;
+ mp3)
+ cat <<-EOCfg
+# MP3 parameters:
+# * bitrate : Set ABR to . Again, if you want CBR or any
+# other mode supported by lame, please fork and file a pull request.
+bitrate ${destinationquality["$destination"]}
+
+# * noresample /: LAME may decide to encode your file to a lower
+# sampling-rate if you use a low bitrate. Setting this to yes will
+# append --resample , preventing any resampling from
+# happening.
+ EOCfg
+ if (( ${destinationnoresample["$destination"]} ))
+ then
+ echo $'noresample\t\tyes'
+ else
+ echo $'noresample\t\tno'
+ fi
+ cat <<-EOCfg
+
+
+ EOCfg
+ ;;
+ esac
+ cat <<-EOCfg
+# Optional parameters:
+# * normalize /: Normalize output files.
+ EOCfg
+ if (( ${destinationnormalize["$destination"]} ))
+ then
+ echo $'normalize\t\tyes'
+ else
+ echo $'normalize\t\tno'
+ fi
+ cat <<-EOCfg
+
+# * rename : Destination files will be named according to ,
+# after expansion of special strings:
+# %{album},
+# %{albumartist},
+# %{artist},
+# %{disc},
+# %{genre},
+# %{title},
+# %{track},
+# %{year}.
+# Untagged files or files in unrecognized formats will not be changed.
+ EOCfg
+ if [[ ${destinationrenamepath["$destination"]}/${destinationrename["$destination"]} == / ]]
+ then
+ echo $'#rename\t\t\t'
+ elif [[ ${destinationrenamepath["$destination"]}/${destinationrename["$destination"]} == /* ]]
+ then
+ echo $'rename\t\t\t'"${destinationrename["$destination"]}"
+ else
+ echo $'rename\t\t\t'"${destinationrenamepath["$destination"]}/${destinationrename["$destination"]}"
+ fi
+ cat <<-EOCfg
+
+# * fat32compat /: Rename files for compatibility with FAT32
+# filesystems.
+ EOCfg
+ if (( ${destinationfat32compat["$destination"]} ))
+ then
+ echo $'fat32compat\t\tyes'
+ else
+ echo $'fat32compat\t\tno'
+ fi
+ cat <<-EOCfg
+
+# * ascii-only /: Rename files for compatibility with ASCII-only
+# systems.
+ EOCfg
+ if (( ${destinationascii["$destination"]} ))
+ then
+ echo $'ascii-only\t\tyes'
+ else
+ echo $'ascii-only\t\tno'
+ fi
+ cat <<-EOCfg
+
+# * skip_mime-type : Files with mime-type will not
+# be included in that destination. For more than one mime-type, use multiple
+# times, as needed. The '*' character is a wildcard.
+ EOCfg
+ destinationskipmime["$destination"]="${destinationskipmime["$destination"]}|"
+ while [[ ${destinationskipmime["$destination"]} =~ \| ]]
+ do
+ echo $'skip_mime-type\t\t'"${destinationskipmime["$destination"]%%|*}"
+ destinationskipmime["$destination"]="${destinationskipmime["$destination"]#*|}"
+ done
+ cat <<-EOCfg
+
+# * copy_mime-type : Same as skip_mime-type, except that files
+# matching will be copied as-is to the destination. E.g. image/* will copy
+# covers and other images to the destination. In fact, AtOM will try to use
+# hard links instead of copies.
+ EOCfg
+ destinationcopymime["$destination"]="${destinationcopymime["$destination"]}|"
+ while [[ ${destinationcopymime["$destination"]} =~ \| ]]
+ do
+ echo $'copy_mime-type\t\t'"${destinationcopymime["$destination"]%%|*}"
+ destinationcopymime["$destination"]="${destinationcopymime["$destination"]#*|}"
+ done
+ cat <<-EOCfg
+
+# * copy_extension : Same as skip_extension, except that files
+# matching will be copied as-is to the destination.
+ EOCfg
+ destinationcopyext["$destination"]="${destinationcopyext["$destination"]}|"
+ while [[ ${destinationcopyext["$destination"]} =~ \| ]]
+ do
+ echo $'copy_extension\t\t'"${destinationcopyext["$destination"]%%|*}"
+ destinationcopyext["$destination"]="${destinationcopyext["$destination"]#*|}"
+ done
+ cat <<-EOCfg
+
+# * channels : Files with more than channels will be
+# downmixed. Useful if you create files for telephony music-on-hold.
+ EOCfg
+ if (( ${destinationchannels["$destination"]} ))
+ then
+ echo $'channels\t\t'${destinationchannels["$destination"]}
+ else
+ echo $'#channels\t\t2'
+ fi
+ cat <<-EOCfg
+
+# * frequency : Files will be resampled as needed to Hz
+# sampling-rate. Shoutcast/Icecast streams require a constant sampling-rate.
+# Telephony systems often require a sample rate of 8000Hz.
+ EOCfg
+ if (( ${destinationfrequency["$destination"]} ))
+ then
+ echo $'frequency\t\t'${destinationfrequency["$destination"]}
+ else
+ echo $'#frequency\t\t44100'
+ fi
+ cat <<-EOCfg
+
+# * higher-than : Integer. Only reencode files with bitrates higher
+# then kbps. This only applies if sample-rate, channel count and of
+# course format are equal. If unset, only files with bitrates equal to that
+# of the target will be copied (actually, hardlinking will be attempted
+# first). As Ogg Vorbis target quality is not defined by its bitrate, Ogg
+# Vorbis files will always be reencoded if unset.
+ EOCfg
+ if (( ${destinationmaxbps["$destination"]} ))
+ then
+ echo $'higher-than\t\t'${destinationmaxbps["$destination"]}
+ else
+ echo $'#higher-than\t\t128'
+ fi
+ cat <<-EOCfg
+
+
+ EOCfg
+ done
+}
diff --git a/lib/copy/copyFiles_action b/lib/copy/action
similarity index 63%
rename from lib/copy/copyFiles_action
rename to lib/copy/action
index 0134eb5..63e7d75 100644
--- a/lib/copy/copyFiles_action
+++ b/lib/copy/action
@@ -1,6 +1,6 @@
#!/bin/bash
copyFiles_action() {
- echo -n "Copying files... "
+ (( cron )) || echo -n "Copying files... "
echo '
SELECT
source_files.filename,
@@ -44,22 +44,39 @@ copyFiles_action() {
destfileid=${rest%%::AtOM:SQL:Sep::*}
rest=${rest#*::AtOM:SQL:Sep::}
(( count++ ))
- printf '\b\b\b\b%3i%%' $(( (count * 100) / ${#copyfiles[@]} ))
- if [ -n "${renamepath["$destination"]}" ]
+ (( cron )) || printf '\b\b\b\b%3i%%' $(( (count * 100) / ${#copyfiles[@]} ))
+ if [ -n "${destinationrenamepath["$destination"]}" ]
then
- destdir="$(guessPath)" || continue
+ destdir="$(guessPath)"
+ guessstatus=$?
+ case $guessstatus in
+ 1)
+ continue
+ ;;
+ 2)
+ (( postponed++ ))
+ continue
+ ;;
+ esac
else
destdir="${destinationpath["$destination"]}/"
- destdir+=$(sanitizeFile "${sourcefilename%%/*}" dir)
- part=${sourcefilename#*/}
- while [[ $part =~ / ]]
- do
- destdir+="/$(sanitizeFile "${part%%/*}" dir)"
- part=${part#*/}
- done
- if ! [ -d "$destdir" ]
+ if [[ $sourcefilename =~ / ]]
then
- mkdir -p "$destdir"
+ destdir+=$(
+ sanitizeFile "${sourcefilename%%/*}" dir
+ )
+ part=${sourcefilename#*/}
+ while [[ $part =~ / ]]
+ do
+ destdir+="/$(
+ sanitizeFile "${part%%/*}" dir
+ )"
+ part=${part#*/}
+ done
+ if ! [ -d "$destdir" ]
+ then
+ mkdir -p "$destdir"
+ fi
fi
fi
if cp -al "$sourcepath/$sourcefilename" "$destdir" 2>/dev/null\
@@ -67,7 +84,9 @@ copyFiles_action() {
then
Update destination_files \
filename "$destdir/${sourcefilename##*/}"\
- rename_pattern "${destinationrenamepath[$destination]}/${destinationrename[$destination]}:${destinationfat32compat["$destination"]}"\
+ rename_pattern "${destinationrenamepath[$destination]}/${destinationrename[$destination]}"\
+ fat32compat ${destinationfat32compat["$destination"]}\
+ ascii ${destinationascii["$destination"]}\
last_change $lastchange \
<<-EOWhere
id = $destfileid
@@ -76,6 +95,15 @@ copyFiles_action() {
fi
done
echo 'COMMIT;' >&3
- echo -e "\rCopied ${done:-0} of $count files.\033[K"
+ if (( count ))
+ then
+ (( cron )) || echo -n $'\r'
+ echo -n "Copied ${done:-0} of $count" \
+ "files${postponed+ ($postponed postponed)}."
+ (( cron )) || echo -en "\033[K"
+ echo
+ else
+ (( cron )) || echo -e "\rNothing to copy.\033[K"
+ fi
unset count done
}
diff --git a/lib/copy/checkCopy b/lib/copy/check
similarity index 100%
rename from lib/copy/checkCopy
rename to lib/copy/check
diff --git a/lib/copy/copyFiles_matching b/lib/copy/copyFiles_matching
deleted file mode 100644
index 64a3744..0000000
--- a/lib/copy/copyFiles_matching
+++ /dev/null
@@ -1,30 +0,0 @@
-#!/bin/bash
-copyFiles_matching() {
- extension="${filename##*.}"
- cp -al \
- "$sourcepath/$filename" \
- "$destdir/$destfile.$extension" \
- 2>/dev/null \
- || cp -a \
- "$sourcepath/$filename" \
- "$destdir/$destfile.$extension"
- echo \
- "UPDATE destination_files" \
- "SET filename=" \
- "\"${destdir//\"/\"\"}/${destfile//\"/\"\"}.$extension\"," \
- " last_change=(" \
- " SELECT last_change" \
- " FROM source_files" \
- " WHERE id=$fileid" \
- " )," \
- " old_filename=(" \
- " SELECT filename" \
- " FROM destination_files" \
- " WHERE id=$destfileid" \
- " )," \
- " rename_pattern=" \
-"\"${destinationrenamepath[$destination]}/${destinationrename[$destination]}:${destinationfat32compat["$destination"]}\""\
- "WHERE id=$destfileid;" \
- >&3
- (( ++copies ))
-}
diff --git a/lib/copy/guessPath b/lib/copy/guessPath
index ccb0e4b..75e1914 100644
--- a/lib/copy/guessPath
+++ b/lib/copy/guessPath
@@ -1,6 +1,31 @@
#!/bin/bash
guessPath() {
+ echo 'SELECT IFNULL( (
+ SELECT destination_files.last_change
+ FROM destination_files
+ INNER JOIN source_files
+ ON destination_files.source_file_id=source_files.id
+ INNER JOIN mime_type_actions
+ ON
+ mime_type_actions.id=source_files.mime_type
+ INNER JOIN destinations
+ ON destinations.id=destination_files.destination_id
+ WHERE destinations.id = '$destinationid'
+ AND source_files.filename LIKE
+ "'"${sourcedir//\"/\"\"}"'/%"
+ AND NOT source_files.filename LIKE
+ "'"${sourcedir//\"/\"\"}"'/%/%"
+ AND mime_type_actions.action = 1
+ ORDER BY destination_files.last_change DESC
+ LIMIT 1
+ ),"0.0");
+ '>&3
+ read -u4 timestamp
+ if (( ${timestamp/./} == 0 ))
+ then
+ return 2
+ fi
echo 'SELECT IFNULL( (
SELECT destination_files.filename
FROM destination_files
@@ -14,7 +39,10 @@ guessPath() {
WHERE destinations.id = '$destinationid'
AND source_files.filename LIKE
"'"${sourcedir//\"/\"\"}"'/%"
+ AND NOT source_files.filename LIKE
+ "'"${sourcedir//\"/\"\"}"'/%/%"
AND mime_type_actions.action = 1
+ ORDER BY destination_files.last_change DESC
LIMIT 1
),"AtOM:NotFound");
'>&3
diff --git a/lib/copy/matching b/lib/copy/matching
new file mode 100644
index 0000000..b0f6516
--- /dev/null
+++ b/lib/copy/matching
@@ -0,0 +1,33 @@
+#!/bin/bash
+copyFiles_matching() {
+ extension="${filename##*.}"
+ cp -al \
+ "$sourcepath/$filename" \
+ "$destdir/$destfile.$extension" \
+ 2>/dev/null \
+ || cp -a \
+ "$sourcepath/$filename" \
+ "$destdir/$destfile.$extension"
+ echo \
+ "UPDATE destination_files" \
+ "SET filename=" \
+ "\"${destdir//\"/\"\"}/${destfile//\"/\"\"}.$extension\"," \
+ " last_change=(" \
+ " SELECT last_change" \
+ " FROM source_files" \
+ " WHERE id=$fileid" \
+ " )," \
+ " old_filename=(" \
+ " SELECT filename" \
+ " FROM destination_files" \
+ " WHERE id=$destfileid" \
+ " )," \
+ " rename_pattern=" \
+"\"${destinationrenamepath[$destination]}/${destinationrename[$destination]}\","\
+ " fat32compat=" \
+ "${destinationfat32compat["$destination"]}," \
+ " ascii=${destinationascii["$destination"]}" \
+ "WHERE id=$destfileid;" \
+ >&3
+ (( ++copies ))
+}
diff --git a/lib/database/checkVersion b/lib/database/checkVersion
new file mode 100644
index 0000000..dcef357
--- /dev/null
+++ b/lib/database/checkVersion
@@ -0,0 +1,26 @@
+#!/bin/bash
+currentdbversion=3
+checkDatabaseVersion() {
+ local dbversion
+ if dbversion=$(Select atom version <<<"\"1\" = 1")
+ then
+ if (( dbversion == currentdbversion ))
+ then
+ return 0
+ elif (( dbversion < currentdbversion ))
+ then
+ until (( dbversion == currentdbversion ))
+ do
+ upgradedatabase_${dbversion}_$((dbversion+1))
+ dbversion=$(Select atom version <<<"\"1\" = 1")
+ done
+ else
+ echo "Database schema version $dbversion is higher than
+ that of this version of AtOM
+ ($currentdbversion). Bailing out." >&2
+ exit 1
+ fi
+ else
+ Insert atom 1 <<<"version $currentdbversion"
+ fi
+}
diff --git a/lib/database/closeDatabase b/lib/database/close
similarity index 83%
rename from lib/database/closeDatabase
rename to lib/database/close
index 2d6e1a2..9fb38ca 100644
--- a/lib/database/closeDatabase
+++ b/lib/database/close
@@ -1,10 +1,10 @@
#!/bin/bash
closeDatabase() {
+ echo 'vacuum;' >&3
echo .quit >&3
(( debug )) && echo -n "Waiting for SQLite to terminate... "
wait
(( debug )) && echo OK
exec 3>&-
exec 4<&-
- rm "$tempdir"/sqlite.{in,out}
}
diff --git a/lib/database/openDatabase b/lib/database/open
similarity index 66%
rename from lib/database/openDatabase
rename to lib/database/open
index b75cef4..9d9fdc3 100644
--- a/lib/database/openDatabase
+++ b/lib/database/open
@@ -1,23 +1,13 @@
#!/bin/bash
openDatabase() {
- if [ ! -d "$tempdir" ]
- then
- mkdir -p "$tempdir"
- fi
rm -f "$tempdir"/sqlite.{in,out}
mkfifo "$tempdir"/sqlite.{in,out}
- if [ ! -f "$database" ]
- then
- if [ ! -d "${database%/*}" ]
- then
- mkdir -p "${database%/*}"
- fi
- fi
sqlite3 -bail "$database" \
< "$tempdir/sqlite.in" \
> "$tempdir/sqlite.out" &
exec 3> "$tempdir"/sqlite.in
exec 4< "$tempdir"/sqlite.out
+ rm "$tempdir"/sqlite.in "$tempdir"/sqlite.out
if (( debug > 2 ))
then
exec 5>&3
@@ -26,4 +16,10 @@ openDatabase() {
cat $schema >&3
echo '.separator ::AtOM:SQL:Sep::' >&3
echo 'PRAGMA foreign_keys = ON;' >&3
+ echo 'PRAGMA recursive_triggers = ON;' >&3
+ echo 'PRAGMA temp_store = 2;' >&3
+ echo 'PRAGMA locking_mode = EXCLUSIVE;' >&3
+ read -u4
+ unset REPLY
+ checkDatabaseVersion
}
diff --git a/lib/database/upgradedatabase_1_2 b/lib/database/upgradedatabase_1_2
new file mode 100644
index 0000000..b1d3ed1
--- /dev/null
+++ b/lib/database/upgradedatabase_1_2
@@ -0,0 +1,43 @@
+#!/bin/bash
+
+upgradedatabase_1_2() {
+ local data \
+ datas \
+ id \
+ rename_pattern \
+ pattern \
+ fat32
+ echo "Upgrading database to version 2... (backup is $database.bak_v1)"
+ cp "$database" "$database.bak_v1"
+ echo 'ALTER TABLE destination_files ADD COLUMN fat32compat INTEGER;' >&3
+ echo 'ALTER TABLE destination_files ADD COLUMN ascii INTEGER;' >&3
+ echo '
+SELECT id,
+ rename_pattern
+FROM destination_files;
+
+SELECT "AtOM::NoMoreData";' >&3
+
+ read -u4 data
+ while [[ $data != AtOM::NoMoreData ]]
+ do
+ datas+=( "$data" )
+ read -u4 data
+ done
+ echo 'BEGIN TRANSACTION;' >&3
+ for data in "${datas[@]}"
+ do
+ id="${data%%::AtOM:SQL:Sep::*}"
+ rename_pattern="${data#*::AtOM:SQL:Sep::}"
+ IFS=':'
+ read pattern fat32 <<<"$rename_pattern"
+ IFS="$oldIFS"
+ Update destination_files \
+ rename_pattern "$pattern" \
+ fat32compat "$fat32" \
+ ascii 0 \
+ <<<"id = $id"
+ done
+ echo 'COMMIT;' >&3
+ Update atom version 2 <<<"1 = 1"
+}
diff --git a/lib/database/upgradedatabase_2_3 b/lib/database/upgradedatabase_2_3
new file mode 100644
index 0000000..998556f
--- /dev/null
+++ b/lib/database/upgradedatabase_2_3
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+upgradedatabase_2_3() {
+ local data \
+ datas \
+ id \
+ destination
+ echo "Upgrading database to version 3... (backup is $database.bak_v2)"
+ cp "$database" "$database.bak_v2"
+ echo 'ALTER TABLE destinations ADD COLUMN enabled INTEGER DEFAULT 1;' >&3
+ Update destinations enabled 1 <<< "1 = 1"
+
+ Update atom version 3 <<<"1 = 1"
+}
\ No newline at end of file
diff --git a/lib/decode/decodeFile b/lib/decode/decodeFile
deleted file mode 100644
index 09c2dc3..0000000
--- a/lib/decode/decodeFile
+++ /dev/null
@@ -1,131 +0,0 @@
-#!/bin/bash
-decodeFile() {
- case "$mimetype" in
- 'video/'*)
- extractAudio
- if (( ${destinationnormalize["$destination"]}))\
- || (
- [ -n "${destinationfrequency["$destination"]}" ]\
- && (( ${rate:-0} != ${destinationfrequency["$destination"]}))\
- ) || (
- [ -n "${destinationchannels["$destination"]}" ]\
- && (( ${channels:-0} != ${destinationchannels["$destination"]} ))
- )
- then
- sox_needed=1
- fi
- ;;
- 'audio/mpeg')
- if [[ ${destinationformat[$destination]} = mp3 ]] \
- && checkCopy
- then
- copied=1
- else
- decodeSox
- fi
- ;;
- 'application/ogg opus')
- if [[ ${destinationformat[$destination]} = opus ]] \
- && checkCopy
- then
- copied=1
- else
- decodeOpusdec
- if (( ${destinationnormalize["$destination"]}))\
- || (
- [ -n "${destinationfrequency["$destination"]}" ]\
- && (( ${rate:-0} != ${destinationfrequency["$destination"]}))\
- ) || (
- [ -n "${destinationchannels["$destination"]}" ]\
- && (( ${channels:-0} != ${destinationchannels["$destination"]} ))
- )
- then
- sox_needed=1
- fi
- fi
- ;;
- 'application/ogg'*)
- if [[ ${destinationformat[$destination]} = vorbis ]] \
- && checkCopy
- then
- copied=1
- else
- decodeSox
- fi
- ;;
- 'audio/x-flac')
- decodeSox
- ;;
- *)
- extendedtype=$(file -b "$sourcepath/$filename")
- case "$extendedtype" in
- *'Musepack '*)
- decodeMpcdec
- if (( ${destinationnormalize["$destination"]}))\
- || (
- [ -n "${destinationfrequency["$destination"]}" ]\
- && (( ${rate:-0} != ${destinationfrequency["$destination"]}))\
- ) || (
- [ -n "${destinationchannels["$destination"]}" ]\
- && (( ${channels:-0} != ${destinationchannels["$destination"]} ))
- )
- then
- sox_needed=1
- fi
- ;;
- *)
- decodeSox
- ;;
- esac
- ;;
- esac
- if ! (( copied ))
- then
- if ! decodetaskid=$(
- Select tasks id <<<"key = $tmpfile"
- )
- then
- decodetaskid=$(
- Insert tasks <<-EOInsert
- key $tmpfile
- source_file $fileid
- $(
- for key in ${!commandline[@]}
- do
- echo "cmd_arg$key ${commandline[key]}"
- done
- )
- status 0
- EOInsert
- )
- progressSpin
- fi
- if (( sox_needed ))
- then
- cleanup="$tempdir/$tmpfile"
- decodeSox "$tempdir/$tmpfile.wav"
- if ! soxtaskid=$(
- Select tasks id <<<"key = $tmpfile"
- )
- then
- soxtaskid=$(
- Insert tasks <<-EOInsert
- key $tmpfile
- source_file $fileid
- $(
- for key in ${!commandline[@]}
- do
- echo "cmd_arg$key ${commandline[key]}"
- done
- )
- requires $decodetaskid
- required $decodetaskid
- status 0
- cleanup $cleanup
- EOInsert
- )
- progressSpin
- fi
- fi
- fi
-}
diff --git a/lib/decode/file b/lib/decode/file
new file mode 100644
index 0000000..5797b9d
--- /dev/null
+++ b/lib/decode/file
@@ -0,0 +1,189 @@
+#!/bin/bash
+declare soxtaskid
+decodeFile() {
+ if [[ ${destinationformat["$destination"]} == copy ]]
+ then
+ copied=1
+ else
+ case "$mimetype" in
+ 'video/'*)
+ (( disablevideo )) && continue
+ extractAudio
+ if (( ${destinationnormalize["$destination"]}))\
+ || (
+ [ -n "${destinationfrequency["$destination"]}" ]\
+ && (( ${rate:-0} != ${destinationfrequency["$destination"]}))\
+ ) || (
+ [ -n "${destinationchannels["$destination"]}" ]\
+ && (( ${channels:-0} != ${destinationchannels["$destination"]} ))
+ )
+ then
+ sox_needed=1
+ fi
+ ;;
+ 'audio/mpeg')
+ if [[ ${destinationformat[$destination]} = mp3 ]] \
+ && checkCopy
+ then
+ copied=1
+ else
+ decodeSox
+ fi
+ ;;
+ 'application/ogg opus')
+ if [[ ${destinationformat[$destination]} = opus ]] \
+ && checkCopy
+ then
+ copied=1
+ else
+ (( disableopusdec )) && continue
+ decodeOpusdec
+ if (( ${destinationnormalize["$destination"]}))\
+ || (
+ [ -n "${destinationfrequency["$destination"]}" ]\
+ && (( ${rate:-0} != ${destinationfrequency["$destination"]}))\
+ ) || (
+ [ -n "${destinationchannels["$destination"]}" ]\
+ && (( ${channels:-0} != ${destinationchannels["$destination"]} ))
+ )
+ then
+ sox_needed=1
+ fi
+ fi
+ ;;
+ 'audio/ogg opus')
+ if [[ ${destinationformat[$destination]} = opus ]] \
+ && checkCopy
+ then
+ copied=1
+ else
+ (( disableopusdec )) && continue
+ decodeOpusdec
+ if (( ${destinationnormalize["$destination"]}))\
+ || (
+ [ -n "${destinationfrequency["$destination"]}" ]\
+ && (( ${rate:-0} != ${destinationfrequency["$destination"]}))\
+ ) || (
+ [ -n "${destinationchannels["$destination"]}" ]\
+ && (( ${channels:-0} != ${destinationchannels["$destination"]} ))
+ )
+ then
+ sox_needed=1
+ fi
+ fi
+ ;;
+ 'application/ogg'*)
+ if [[ ${destinationformat[$destination]} = vorbis ]] \
+ && checkCopy
+ then
+ copied=1
+ else
+ decodeSox
+ fi
+ ;;
+ 'audio/ogg'*)
+ if [[ ${destinationformat[$destination]} = vorbis ]] \
+ && checkCopy
+ then
+ copied=1
+ else
+ decodeSox
+ fi
+ ;;
+ 'audio/x-flac')
+ decodeSox
+ ;;
+ 'audio/flac')
+ decodeSox
+ ;;
+ *)
+ extendedtype=$(file -b "$sourcepath/$filename")
+ case "$extendedtype" in
+ *'Musepack '*)
+ (( disablempcdec )) && continue
+ decodeMpcdec
+ if (( ${destinationnormalize["$destination"]}))\
+ || (
+ [ -n "${destinationfrequency["$destination"]}" ]\
+ && (( ${rate:-0} != ${destinationfrequency["$destination"]}))\
+ ) || (
+ [ -n "${destinationchannels["$destination"]}" ]\
+ && (( ${channels:-0} != ${destinationchannels["$destination"]} ))
+ )
+ then
+ sox_needed=1
+ fi
+ ;;
+ *)
+ if (( disablevideo ))
+ then
+ decodeSox
+ else
+ extractAudio
+ if (( ${destinationnormalize["$destination"]}))\
+ || (
+ [ -n "${destinationfrequency["$destination"]}" ]\
+ && (( ${rate:-0} != ${destinationfrequency["$destination"]}))\
+ ) || (
+ [ -n "${destinationchannels["$destination"]}" ]\
+ && (( ${channels:-0} != ${destinationchannels["$destination"]} ))
+ )
+ then
+ sox_needed=1
+ fi
+ fi
+ ;;
+ esac
+ ;;
+ esac
+ if ! (( copied ))
+ then
+ if ! decodetaskid=$(
+ Select tasks id <<<"key = $tmpfile"
+ )
+ then
+ decodetaskid=$(
+ Insert tasks <<-EOInsert
+ key $tmpfile
+ source_file $fileid
+ $(
+ for key in ${!commandline[@]}
+ do
+ echo "cmd_arg$key ${commandline[key]}"
+ done
+ )
+ status 0
+ EOInsert
+ )
+ progressSpin
+ fi
+ if (( sox_needed ))
+ then
+ cleanup="$tempdir/$tmpfile"
+ decodeSox "$tempdir/$tmpfile"
+ if ! soxtaskid=$(
+ Select tasks id <<<"key = $tmpfile"
+ )
+ then
+ soxtaskid=$(
+ Insert tasks <<-EOInsert
+ key $tmpfile
+ source_file $fileid
+ $(
+ for key in ${!commandline[@]}
+ do
+ echo "cmd_arg$key ${commandline[key]}"
+ done
+ )
+ requires $decodetaskid
+ required $decodetaskid
+ status 0
+ cleanup $cleanup
+ EOInsert
+ )
+ progressSpin
+ fi
+ fi
+ fi
+ fi
+}
diff --git a/lib/decode/decodeMpcdec b/lib/decode/mpcdec
similarity index 100%
rename from lib/decode/decodeMpcdec
rename to lib/decode/mpcdec
diff --git a/lib/decode/decodeOpusdec b/lib/decode/opusdec
similarity index 100%
rename from lib/decode/decodeOpusdec
rename to lib/decode/opusdec
diff --git a/lib/decode/decodeSox b/lib/decode/sox
similarity index 91%
rename from lib/decode/decodeSox
rename to lib/decode/sox
index 5d7e6df..ef4c7f6 100644
--- a/lib/decode/decodeSox
+++ b/lib/decode/sox
@@ -26,6 +26,11 @@ decodeSox() {
commandline+=(-c ${destinationchannels["$destination"]})
soxoptions_out+=" -c ${destinationchannels["$destination"]}"
fi
+ if (( ${bitdepth:-0} > 16 ))
+ then
+ commandline+=(-b 16)
+ soxoptions_out+=" -b 16"
+ fi
tmpfile="$fileid${soxoptions_in// /}${soxoptions_out// /}"
commandline+=("$tempdir/$tmpfile.wav")
}
diff --git a/lib/destinations/createDestinations b/lib/destinations/create
similarity index 70%
rename from lib/destinations/createDestinations
rename to lib/destinations/create
index fc6e3bc..5c9910b 100644
--- a/lib/destinations/createDestinations
+++ b/lib/destinations/create
@@ -1,6 +1,6 @@
#!/bin/bash
createDestinations() {
- for destination in ${!destinationpath[@]}
+ for destination in ${destinations[@]}
do
if ! [ -d "${destinationpath["$destination"]}" ]
then
@@ -11,7 +11,7 @@ createDestinations() {
fi
fi
destinationid["$destination"]=$(
- InsertIfUnset destinations <<<"name $destination"
+ InsertIfUnset destinations <<<"name $destination ${destinationenabled[\"$destination\"]}"
)
done
}
diff --git a/lib/destinations/updateMimes b/lib/destinations/updateMimes
index 189572b..6835e01 100644
--- a/lib/destinations/updateMimes
+++ b/lib/destinations/updateMimes
@@ -29,4 +29,5 @@ updateMimes() {
)
done
done
+ IFS="$oldIFS"
}
diff --git a/lib/encode/encodeFile::mp3 b/lib/encode/mp3
similarity index 83%
rename from lib/encode/encodeFile::mp3
rename to lib/encode/mp3
index 1496570..d0d6800 100644
--- a/lib/encode/encodeFile::mp3
+++ b/lib/encode/mp3
@@ -8,6 +8,10 @@ encodeFile::mp3() {
[ -n "$title" ] && lameopts+=(--tt "$title")
[ -n "$track" ] && lameopts+=(--tn "$track")
[ -n "$year" ] && lameopts+=(--ty "$year")
+ [ -n "$albumartist" ] && lameopts+=(--tv TPE2="$albumartist")
+ [ -n "$composer" ] && lameopts+=(--tv TCOM="$composer")
+ [ -n "$performer" ] && lameopts+=(--tv TOPE="$performer")
+ [ -n "$disc" ] && lameopts+=(--tv TPOS="$disc")
if (( ${destinationnoresample[$destination]:-0} == 1 ))
then
# If 'rate' is not one of these value, it cannot be encoded to
@@ -26,6 +30,9 @@ encodeFile::mp3() {
11025) lameopts+=(--resample 11.025) ;;
8000) lameopts+=(--resample 8) ;;
esac
+ elif (( rate > 48000 ))
+ then
+ lameopts+=(--resample 48)
else
case $rate in
48000) lameopts+=(--resample 48) ;;
@@ -62,8 +69,11 @@ encodeFile::mp3() {
cleanup $tempdir/$tmpfile.wav
source_file $fileid
status 0
- rename_pattern ${destinationrenamepath[$destination]}/${destinationrename[$destination]}:${destinationfat32compat["$destination"]}
+ rename_pattern ${destinationrenamepath[$destination]}/${destinationrename[$destination]}
+ fat32compat ${destinationfat32compat["$destination"]}
+ ascii ${destinationascii["$destination"]}
EOInsert
)
progressSpin
+ soxtaskid=''
}
diff --git a/lib/encode/encodeFile::opus b/lib/encode/opus
similarity index 86%
rename from lib/encode/encodeFile::opus
rename to lib/encode/opus
index 8e9bbcd..2c25f9a 100644
--- a/lib/encode/encodeFile::opus
+++ b/lib/encode/opus
@@ -1,6 +1,6 @@
#!/bin/bash
encodeFile::opus() {
- opusencopts=(${ionice}opusenc --music --quiet)
+ opusencopts=(${ionice}opusenc --quiet)
opusencopts+=(--bitrate ${destinationquality[$destination]})
[ -n "${destinationloss["$destination"]}" ] \
&& opusencopts+=(--expect-loss "${destinationloss["$destination"]}")
@@ -15,7 +15,7 @@ encodeFile::opus() {
[ -n "$track" ] && opusencopts+=(--comment "TRACKNUMBER=${track%/*}")
[ -n "${track#*/}" ] && opusencopts+=(--comment "TRACKTOTAL=${track#*/}")
[ -n "$year" ] && opusencopts+=(--comment "DATE=$year")
- opusencopts+=("$tempdir/$tmpfile.wav" "$destdir/$destfile.opus")
+ opusencopts+=("$tempdir/$tmpfile".wav "$destdir/$destfile.opus")
encodetaskid=$(
Insert tasks <<-EOInsert
key ${fileid}opusenc$destination
@@ -37,8 +37,11 @@ encodeFile::opus() {
cleanup $tempdir/$tmpfile.wav
source_file $fileid
status 0
- rename_pattern ${destinationrenamepath[$destination]}/${destinationrename[$destination]}:${destinationfat32compat["$destination"]}
+ rename_pattern ${destinationrenamepath[$destination]}/${destinationrename[$destination]}
+ fat32compat ${destinationfat32compat["$destination"]}
+ ascii ${destinationascii["$destination"]}
EOInsert
)
progressSpin
+ soxtaskid=''
}
diff --git a/lib/encode/encodeFile::vorbis b/lib/encode/vorbis
similarity index 90%
rename from lib/encode/encodeFile::vorbis
rename to lib/encode/vorbis
index 7c32538..a96523f 100644
--- a/lib/encode/encodeFile::vorbis
+++ b/lib/encode/vorbis
@@ -33,8 +33,11 @@ encodeFile::vorbis() {
cleanup $tempdir/$tmpfile.wav
source_file $fileid
status 0
- rename_pattern ${destinationrenamepath[$destination]}/${destinationrename[$destination]}:${destinationfat32compat["$destination"]}
+ rename_pattern ${destinationrenamepath[$destination]}/${destinationrename[$destination]}
+ fat32compat ${destinationfat32compat["$destination"]}
+ ascii ${destinationascii["$destination"]}
EOInsert
)
progressSpin
+ soxtaskid=''
}
diff --git a/lib/files/getDestDir b/lib/files/getDestDir
index ff4f26b..e92a899 100644
--- a/lib/files/getDestDir
+++ b/lib/files/getDestDir
@@ -1,58 +1,92 @@
#!/bin/bash
getDestDir() {
- destdir="${destinationpath[$destination]}/"
- if [ -n "${destinationrenamepath[$destination]}" ] \
+ if [ -n "${destinationrenamepath[$destination]}" ] \
&& (
(
- ! [[ ${destinationrenamepath[$destination]} =~ %\{album\} ]] \
- || [ -n "$album" ]
- ) && (
- ! [[ ${destinationrenamepath[$destination]} =~ %\{albumartist\} ]] \
- || [ -n "$albumartist" ]
- ) && (
- ! [[ ${destinationrenamepath[$destination]} =~ %\{artist\} ]] \
- || [ -n "$artist" ]
- ) && (
- ! [[ ${destinationrenamepath[$destination]} =~ %\{genre\} ]] \
- || [ -n "$genre" ]
- ) && (
- ! [[ ${destinationrenamepath[$destination]} =~ %\{title\} ]] \
- || [ -n "$title" ]
- ) && (
- ! [[ ${destinationrenamepath[$destination]} =~ %\{track\} ]] \
- || [ -n "$track" ]
- ) && (
- ! [[ ${destinationrenamepath[$destination]} =~ %\{year\} ]] \
- || [ -n "$year" ]
- ) && (
- ! [[ ${destinationrenamepath[$destination]} =~ %\{disc\} ]] \
- || [ -n "$disc" ]
+ [[ ${destinationrenamepath[$destination]} == \
+ *?([^[])%\{album\}?([^\]])* ]] \
+ && [ -n "$album" ]
+ ) || (
+ [[ ${destinationrenamepath[$destination]} == \
+ *?([^[])%\{albumartist\}?([^\]])* ]] \
+ && [ -n "$albumartist" ]
+ ) || (
+ [[ ${destinationrenamepath[$destination]} == \
+ *?([^[])%\{artist\}?([^\]])* ]] \
+ && [ -n "$artist" ]
+ ) || (
+ [[ ${destinationrenamepath[$destination]} == \
+ *?([^[])%\{genre\}?([^\]])* ]] \
+ && [ -n "$genre" ]
+ ) || (
+ [[ ${destinationrenamepath[$destination]} == \
+ *?([^[])%\{title\}?([^\]])* ]] \
+ && [ -n "$title" ]
+ ) || (
+ [[ ${destinationrenamepath[$destination]} == \
+ *?([^[])%\{track\}?([^\]])* ]] \
+ && [ -n "$track" ]
+ ) || (
+ [[ ${destinationrenamepath[$destination]} == \
+ *?([^[])%\{year\}?([^\]])* ]] \
+ && [ -n "$year" ]
+ ) || (
+ [[ ${destinationrenamepath[$destination]} == \
+ *?([^[])%\{disc\}?([^\]])* ]] \
+ && [ -n "$disc" ]
)
)
then
+ destdir="${destinationpath[$destination]}/"
+ if (( ${destinationascii["$destination"]} ))
+ then
+ echo "$album" >&${toascii[1]}
+ read -r -u${toascii[0]} album
+ echo "$albumartist" >&${toascii[1]}
+ read -r -u${toascii[0]} albumartist
+ echo "$artist" >&${toascii[1]}
+ read -r -u${toascii[0]} artist
+ echo "$genre" >&${toascii[1]}
+ read -r -u${toascii[0]} genre
+ echo "$title" >&${toascii[1]}
+ read -r -u${toascii[0]} title
+ echo "$tracknumber" >&${toascii[1]}
+ read -r -u${toascii[0]} tracknumber
+ echo "$year" >&${toascii[1]}
+ read -r -u${toascii[0]} year
+ echo "$disc" >&${toascii[1]}
+ read -r -u${toascii[0]} disc
+ fi
replace=$(sanitizeFile "$album" dir)
- destdir+="${destinationrenamepath[$destination]//%\{album\}/$replace}"
+ destdir+="${destinationrenamepath[$destination]//?(\[)%\{album\}?(\])/$replace}"
replace=$(sanitizeFile "$albumartist" dir)
- destdir="${destdir//%\{albumartist\}/$replace}"
+ destdir="${destdir//?(\[)%\{albumartist\}?(\])/$replace}"
replace=$(sanitizeFile "$artist" dir)
- destdir="${destdir//%\{artist\}/$replace}"
+ destdir="${destdir//?(\[)%\{artist\}?(\])/$replace}"
replace=$(sanitizeFile "$genre" dir)
- destdir="${destdir//%\{genre\}/$replace}"
+ destdir="${destdir//?(\[)%\{genre\}?(\])/$replace}"
replace=$(sanitizeFile "$title" dir)
- destdir="${destdir//%\{title\}/$replace}"
+ destdir="${destdir//?(\[)%\{title\}?(\])/$replace}"
tracknumber="${track%/*}"
replace=$(sanitizeFile "$tracknumber" dir)
- destdir="${destdir//%\{track\}/$replace}"
+ destdir="${destdir//?(\[)%\{track\}?(\])/$replace}"
replace=$(sanitizeFile "$year" dir)
- destdir="${destdir//%\{year\}/$replace}"
+ destdir="${destdir//?(\[)%\{year\}?(\])/$replace}"
replace=$(sanitizeFile "$disc" dir)
- destdir="${destdir//%\{disc\}/$replace}"
+ destdir="${destdir//?(\[)%\{disc\}?(\])/$replace}"
else
+ destdir="${destinationpath[$destination]}/"
destdir+=$(sanitizeFile "${filename%%/*}" dir)
part=${filename#*/}
while [[ $part =~ / ]]
do
- destdir+="/$(sanitizeFile "${part%%/*}" dir)"
+ thispart="${part%%/*}"
+ if (( ${destinationascii["$destination"]} ))
+ then
+ echo "$thispart" >&${toascii[1]}
+ read -r -u${toascii[0]} thispart
+ fi
+ destdir+="/$(sanitizeFile "$thispart" dir)"
part=${part#*/}
done
fi
@@ -60,4 +94,5 @@ getDestDir() {
then
mkdir -p "$destdir"
fi
+ destdir="${destdir//+(\/)//}"
}
diff --git a/lib/files/getDestFile b/lib/files/getDestFile
index 1ec9087..aae78b8 100644
--- a/lib/files/getDestFile
+++ b/lib/files/getDestFile
@@ -1,46 +1,59 @@
#!/bin/bash
getDestFile() {
- if [ -n "${destinationrename[$destination]}" ] \
+ if [ -n "${destinationrename[$destination]}" ] \
&& (
(
- ! [[ ${destinationrename[$destination]} =~ %\{album\} ]] \
- || [ -n "$album" ]
- ) && (
- ! [[ ${destinationrename[$destination]} =~ %\{albumartist\} ]] \
- || [ -n "$albumartist" ]
- ) && (
- ! [[ ${destinationrename[$destination]} =~ %\{artist\} ]] \
- || [ -n "$artist" ]
- ) && (
- ! [[ ${destinationrename[$destination]} =~ %\{genre\} ]] \
- || [ -n "$genre" ]
- ) && (
- ! [[ ${destinationrename[$destination]} =~ %\{title\} ]] \
- || [ -n "$title" ]
- ) && (
- ! [[ ${destinationrename[$destination]} =~ %\{track\} ]] \
- || [ -n "$track" ]
- ) && (
- ! [[ ${destinationrename[$destination]} =~ %\{year\} ]] \
- || [ -n "$year" ]
- ) && (
- ! [[ ${destinationrename[$destination]} =~ %\{disc\} ]] \
- || [ -n "$disc" ]
+ [[ ${destinationrename[$destination]} == \
+ *?([^[])%\{album\}?([^\]])* ]] \
+ && [ -n "$album" ]
+ ) || (
+ [[ ${destinationrename[$destination]} == \
+ *?([^[])%\{albumartist\}?([^\]])* ]] \
+ && [ -n "$albumartist" ]
+ ) || (
+ [[ ${destinationrename[$destination]} == \
+ *?([^[])%\{artist\}?([^\]])* ]] \
+ && [ -n "$artist" ]
+ ) || (
+ [[ ${destinationrename[$destination]} == \
+ *?([^[])%\{genre\}?([^\]])* ]] \
+ && [ -n "$genre" ]
+ ) || (
+ [[ ${destinationrename[$destination]} == \
+ *?([^[])%\{title\}?([^\]])* ]] \
+ && [ -n "$title" ]
+ ) || (
+ [[ ${destinationrename[$destination]} == \
+ *?([^[])%\{track\}?([^\]])* ]] \
+ && [ -n "$track" ]
+ ) || (
+ [[ ${destinationrename[$destination]} == \
+ *?([^[])%\{year\}?([^\]])* ]] \
+ && [ -n "$year" ]
+ ) || (
+ [[ ${destinationrename[$destination]} == \
+ *?([^[])%\{disc\}?([^\]])* ]] \
+ && [ -n "$disc" ]
)
)
then
- destfile="${destinationrename[$destination]//%\{album\}/$album}"
- destfile="${destfile//%\{albumartist\}/$albumartist}"
- destfile="${destfile//%\{artist\}/$artist}"
- destfile="${destfile//%\{genre\}/$genre}"
- destfile="${destfile//%\{title\}/$title}"
+ destfile="${destinationrename[$destination]//?(\[)%\{album\}?(\])/$album}"
+ destfile="${destfile//?(\[)%\{albumartist\}?(\])/$albumartist}"
+ destfile="${destfile//?(\[)%\{artist\}?(\])/$artist}"
+ destfile="${destfile//?(\[)%\{genre\}?(\])/$genre}"
+ destfile="${destfile//?(\[)%\{title\}?(\])/$title}"
tracknumber="${track%/*}"
- destfile="${destfile//%\{track\}/$tracknumber}"
- destfile="${destfile//%\{year\}/$year}"
- destfile="${destfile//%\{disc\}/$disc}"
+ destfile="${destfile//?(\[)%\{track\}?(\])/$tracknumber}"
+ destfile="${destfile//?(\[)%\{year\}?(\])/$year}"
+ destfile="${destfile//?(\[)%\{disc\}?(\])/$disc}"
else
destfile="${filename##*/}"
destfile="${destfile%.*}"
fi
+ if (( ${destinationascii["$destination"]} ))
+ then
+ echo "$destfile" >&${toascii[1]}
+ read -r -u${toascii[0]} destfile
+ fi
destfile=$(sanitizeFile "$destfile")
}
diff --git a/lib/files/getFiles b/lib/files/getFiles
index 08a145f..51feaf3 100644
--- a/lib/files/getFiles
+++ b/lib/files/getFiles
@@ -5,7 +5,7 @@ getFiles() {
do
prunes+="-path $sourcepath$prune_expression -prune -o "
done
- echo -n "Scanning $sourcepath... "
+ (( cron )) || echo -n "Scanning $sourcepath... "
# We probably have thousands of files, don't waste time on disk writes
echo 'BEGIN TRANSACTION;' >&3
while read time size filename
@@ -17,9 +17,9 @@ getFiles() {
EOWhere
then
mimetype=$(file -b --mime-type "$sourcepath/$filename")
- if [[ $mimetype == application/ogg ]]
+ if [[ $mimetype == application/ogg ]] || [[ $mimetype == audio/ogg ]]
then
- case "$(head -n5 "$sourcepath/$filename")" in
+ case "$(head -n5 "$sourcepath/$filename" | tr -d '\0')" in
*'vorbis'*)
mimetype+=' vorbis'
;;
@@ -59,6 +59,7 @@ getFiles() {
find "$sourcepath" $prunes -type f -printf "%T@ %s %P\n"
)
echo 'COMMIT;' >&3
- echo -e "\r${count:-0} files found, ${new:=0} new or changed."
+ (( cron )) || echo -n $'\r'
+ echo "${count:-0} files found, ${new:=0} new or changed."
unset count
}
diff --git a/lib/files/sanitizeFile b/lib/files/sanitizeFile
index 07e8e50..164ca9b 100644
--- a/lib/files/sanitizeFile
+++ b/lib/files/sanitizeFile
@@ -13,7 +13,7 @@ sanitizeFile() {
string=${string//>/ }
string=${string//:/ }
string=${string//\*/ }
- string=${string//|/ }
+ string=${string//\|/ }
string=${string//\"/ }
# Filenames can't begin or end with ' '
diff --git a/lib/setup/destination b/lib/setup/destination
new file mode 100644
index 0000000..c6ea323
--- /dev/null
+++ b/lib/setup/destination
@@ -0,0 +1,507 @@
+#!/bin/bash
+
+setupDestination() {
+ cat <<-EODesc
+
+ Format:
+ copy, vorbis, opus or mp3. Other formats may appear in the future.
+ EODesc
+ comeagain() {
+ read \
+ -e \
+ ${destinationformat["$destination"]+-i"${destinationformat["$destination"]}"}\
+ -p 'Format: ' \
+ value
+ case "$value" in
+ 'mp3')
+ destinationformat["$destination"]=mp3
+ lameneeded=1
+ ;;
+ 'opus')
+ destinationformat["$destination"]=opus
+ opusencneeded=1
+ ;;
+ 'vorbis'|'ogg')
+ destinationformat["$destination"]=vorbis
+ oggencneeded=1
+ ;;
+ 'copy')
+ destinationformat["$destination"]=copy
+ ;;
+ *)
+ echo "Unsupported destination format: $value" >&2
+ comeagain
+ ;;
+ esac
+ }
+ comeagain
+ cat <<-EODesc
+
+ Path (path):
+ Where to store transcoded files (will be created if it does not
+ exist).
+ EODesc
+ read \
+ -e \
+ -p'Path: ' \
+ ${destinationpath["$destination"]+-i"${destinationpath["$destination"]}"}\
+ destinationpath["$destination"]
+ case ${destinationformat["$destination"]} in
+ copy)
+ :
+ ;;
+ vorbis)
+ cat <<-EODesc
+
+ Quality (integer):
+ The quality parameter of oggenc. See man oggenc for more info.
+ EODesc
+ expr='^[0-9]*$'
+ comeagain() {
+ read \
+ -p'Quality: ' \
+ -e \
+ -i \
+ ${destinationquality["$destination"]:-3}\
+ value
+ if ! [[ $value =~ $expr ]]
+ then
+ echo "Invalid quality value: $value" >&2
+ comeagain
+ fi
+ }
+ comeagain
+ if [ -n "${destinationquality["$destination"]}" ] \
+ && (( value != ${destinationquality["$destination"]} ))
+ then
+ setupRegen quality
+ fi
+ destinationquality["$destination"]=$value
+ ;;
+ opus)
+ cat <<-EODesc
+
+ Bitrate (kbps, integer):
+ Set (VBR) bitrate to . Note that while Opus allows for
+ decimal values, AtOM does not. The reason for this is simple: we do
+ numeric comparisons, and Bash only manipulates integers.
+ EODesc
+ expr='^[0-9]*$'
+ comeagain() {
+ read \
+ -e \
+ -i \
+ ${destinationquality["$destination"]:-128}\
+ -p 'Bitrate: ' \
+ value
+ if ! [[ $value =~ $expr ]]
+ then
+ echo "Invalid bitrate value: $value" >&2
+ comeagain
+ fi
+ }
+ comeagain
+ if [ -n "${destinationquality["$destination"]}" ] \
+ && (( value != ${destinationquality["$destination"]} ))
+ then
+ setupRegen bitrate
+ fi
+ destinationquality["$destination"]=$value
+ cat <<-EODesc
+
+ Loss (percent, integer):
+ If you intend to stream the resulting files over an unreliable
+ protocol, you may want to make use of Opus' Forward Error
+ Correction algorythm. See the Opus-codec.org website for details.
+ EODesc
+ comeagain() {
+ read \
+ -e \
+ -i \
+ ${destinationloss["$destination"]:-0}\
+ -p 'Loss: ' \
+ value
+ if ! [[ $value =~ $expr ]]
+ then
+ echo "Invalid loss value: $value" >&2
+ comeagain
+ fi
+ }
+ comeagain
+ if [ -n "${destinationloss["$destination"]}" ] \
+ && (( value != ${destinationloss["$destination"]} ))
+ then
+ setupRegen loss
+ fi
+ destinationloss["$destination"]=$value
+ ;;
+ mp3)
+ cat <<-EODesc
+
+ Bitrate (kbps, integer):
+ Set (ABR) bitrate to .
+ EODesc
+ expr='^[0-9]*$'
+ comeagain() {
+ read \
+ -e \
+ -i \
+ ${destinationquality["$destination"]:-128}\
+ -p 'Bitrate: ' \
+ value
+ if ! [[ $value =~ $expr ]]
+ then
+ echo "Invalid bitrate value: $value" >&2
+ comeagain
+ fi
+ }
+ comeagain
+ if [ -n "${destinationquality["$destination"]}" ] \
+ && (( value != ${destinationquality["$destination"]} ))
+ then
+ setupRegen bitrate
+ fi
+ destinationquality["$destination"]=$value
+ cat <<-EODesc
+
+ Prevent resampling (boolean):
+ LAME may decide to encode your file to a lower sampling-rate if you
+ use a low bitrate. Setting this to yes will append --resample
+ , preventing any resampling from happening.
+ EODesc
+ case ${destinationnoresample["$destination"]} in
+ 0) initialvalue=n ;;
+ 1) initialvalue=y ;;
+ *) unset initialvalue ;;
+ esac
+ comeagain() {
+ read \
+ -e \
+ ${initialvalue+-i $initialvalue}\
+ -p'Prevent resampling (y/N): ' \
+ value
+ case $value in
+ [yY])
+ [[ $initialvalue == n ]]\
+ && setupRegen noresample
+ destinationnoresample["$destination"]=1
+ ;;
+ ''|[nN])
+ [[ $initialvalue == y ]]\
+ && setupRegen noresample
+ destinationnoresample["$destination"]=0
+ ;;
+ *)
+ comeagain
+ ;;
+ esac
+ }
+ comeagain
+ ;;
+ esac
+ cat <<-EODesc
+
+ [Optional parameters]
+ Now you will have the opportunity to configure "advanced" parameters
+ for $destination. You may leave any of these fields blank.
+ EODesc
+ if [[ ${destinationformat["$destination"]} != copy ]]
+ then
+ cat <<-EODesc
+
+ Normalize (boolean):
+ Normalize output files.
+ EODesc
+ case ${destinationnormalize["$destination"]} in
+ 0) initialvalue=n ;;
+ 1) initialvalue=y ;;
+ *) unset initialvalue ;;
+ esac
+ comeagain() {
+ read \
+ -e \
+ ${initialvalue+-i $initialvalue}\
+ -p'Normalize (y/N): ' \
+ value
+ case $value in
+ [yY])
+ [[ $initialvalue == n ]] \
+ && setupRegen normalize
+ destinationnormalize["$destination"]=1
+ ;;
+ ''|[nN])
+ [[ $initialvalue == y ]] \
+ && setupRegen normalize
+ destinationnormalize["$destination"]=0
+ ;;
+ *)
+ comeagain
+ ;;
+ esac
+ }
+ comeagain
+ fi
+ cat <<-EODesc
+
+ Rename (string):
+ Destination files will be named according to , after
+ expansion of special strings:
+ %{album},
+ %{albumartist},
+ %{artist},
+ %{disc},
+ %{genre},
+ %{title},
+ %{track},
+ %{year}.
+ Untagged files or files in unrecognized formats will not be changed.
+ Surrounding a field with [] makes it optional, meaning renaming
+ will still happen if the corresponding tag is not defined.
+
+ Leave blank if you don't want file renaming.
+ EODesc
+ initialvalue="${destinationrenamepath["$destination"]}"
+ initialvalue+=/
+ [[ $initialvalue == / ]] && unset initialvalue
+ initialvalue+="${destinationrename["$destination"]}"
+ [ -z "$initialvalue" ] && unset initialvalue
+ read \
+ -e \
+ ${initialvalue+-i"$initialvalue"} \
+ -p'Rename pattern: ' \
+ value
+ if [[ $value =~ / ]]
+ then
+ destinationrenamepath["$destination"]="${value%/*}"
+ fi
+ destinationrename["$destination"]="${value##*/}"
+ cat <<-EODesc
+
+ FAT32 Compatibility (boolean):
+ Rename files for compatibility with FAT32 filesystems.
+ EODesc
+ case ${destinationfat32compat["$destination"]} in
+ 0) initialvalue=n ;;
+ 1) initialvalue=y ;;
+ *) unset initialvalue ;;
+ esac
+ comeagain() {
+ read \
+ -e \
+ ${initialvalue+-i $initialvalue}\
+ -p'FAT32 Compatibility (y/N): ' \
+ value
+ case $value in
+ [yY])
+ destinationfat32compat["$destination"]=1
+ ;;
+ ''|[nN])
+ destinationfat32compat["$destination"]=0
+ ;;
+ *)
+ comeagain
+ ;;
+ esac
+ }
+ comeagain
+ cat <<-EODesc
+
+ ASCII-only filenames (boolean):
+ Rename files for compatibility with ASCII-only systems (most car
+ radios).
+ Uses Perl with Text::Unidecode to replace cyrillic or kanji
+ with an ASCII representation.
+ EODesc
+ case ${destinationascii["$destination"]} in
+ 0) initialvalue=n ;;
+ 1) initialvalue=y ;;
+ *) unset initialvalue ;;
+ esac
+ comeagain() {
+ read \
+ -e \
+ ${initialvalue+-i $initialvalue}\
+ -p'ASCII-only filenames (y/N): '\
+ value
+ case $value in
+ [yY])
+ destinationascii["$destination"]=1
+ ;;
+ ''|[nN])
+ destinationascii["$destination"]=0
+ ;;
+ *)
+ comeagain
+ ;;
+ esac
+ }
+ comeagain
+ cat <<-EODesc
+
+ Skip mime-type (mime-type, string):
+ Files with mime-type will not be included in that
+ destination. The '*' character is a wildcard.
+
+ This prompt will loop until an empty string is encountered.
+ EODesc
+ while [[ ${destinationskipmime["$destination"]} =~ \| ]]
+ do
+ skippedmimes+=("${destinationskipmime["$destination"]%%|*}")
+ destinationskipmime["$destination"]="${destinationskipmime["$destination"]#*|}"
+ done
+ [ -n "${destinationskipmime["$destination"]}" ] \
+ && skippedmimes+=("${destinationskipmime["$destination"]}")
+ count=${#skippedmimes[@]}
+ unset destinationskipmime["$destination"]
+ for (( i=0 ; 1 ; i++ ))
+ do
+ read \
+ -e \
+ ${skippedmimes[i]+-i"${skippedmimes[i]}"} \
+ -p 'Skip mime-type: ' \
+ value
+ if [ -n "$value" ]
+ then
+ destinationskipmime[$destination]="${destinationskipmime[$destination]:+${destinationskipmime[$destination]}|}$value"
+ elif (( i < count ))
+ then
+ continue
+ else
+ break
+ fi
+ done
+ unset skippedmimes
+ if [[ ${destinationformat["$destination"]} != copy ]]
+ then
+ cat <<-EODesc
+
+ Copy mime-type (mime-type, string):
+ Files with mime-type will be copied as-is to the
+ destination. E.g. image/* will copy covers and other images to the
+ destination. The '*' character is a wildcard.
+
+ This prompt will loop until an empty string is encountered.
+ EODesc
+ while [[ ${destinationcopymime["$destination"]} =~ \| ]]
+ do
+ copiedmimes+=("${destinationcopymime["$destination"]%%|*}")
+ destinationcopymime["$destination"]="${destinationcopymime["$destination"]#*|}"
+ done
+ [ -n "${destinationcopymime["$destination"]}" ] \
+ && copiedmimes+=("${destinationcopymime["$destination"]}")
+ count=${#copiedmimes[@]}
+ unset destinationcopymime["$destination"]
+ for (( i=0 ; 1 ; i++ ))
+ do
+ read \
+ -e \
+ ${copiedmimes[i]+-i"${copiedmimes[i]}"} \
+ -p 'Copy mime-type: ' \
+ value
+ if [ -n "$value" ]
+ then
+ destinationcopymime[$destination]="${destinationcopymime[$destination]:+${destinationcopymime[$destination]}|}$value"
+ elif (( i < count ))
+ then
+ continue
+ else
+ break
+ fi
+ done
+ unset copiedmimes
+ cat <<-EODesc
+
+ Channels (integer):
+ Produced files should have this many channels, no more, no less.
+ EODesc
+ expr='^[0-9]*$'
+ comeagain() {
+ read \
+ -e \
+ ${destinationchannels["$destination"]+-i${destinationchannels["$destination"]}}\
+ -p'Channel count: ' \
+ value
+ if ! [[ $value =~ $expr ]]
+ then
+ echo "Invalid channel count: $value" >&2
+ comeagain
+ fi
+ }
+ comeagain
+ if [ -n "${destinationchannels["$destination"]}" ] \
+ && (( value != ${destinationchannels["$destination"]} ))
+ then
+ setupRegen channels
+ fi
+ destinationchannels["$destination"]=$value
+ cat <<-EODesc
+
+ Sampling rate (Hertz, integer):
+ Files will be resampled as needed to the specified sampling-rate.
+ Shoutcast/Icecast streams require a constant sampling-rate.
+ Telephony systems often require a sampling-rate of 8000Hz.
+ EODesc
+ if [[ ${destinationformat["$destination"]} == opus ]]
+ then
+ cat <<-EODesc
+
+ Please note that Opus supports only the following sample-rates:
+ 8000, 12000, 16000, 24000, and 48000 Hz. So don't set
+ resampling on an Opus destination to any other value or files
+ will be resampled twice.
+ EODesc
+ fi
+ comeagain() {
+ read \
+ -e \
+ ${destinationfrequency["$destination"]+-i${destinationfrequency["$destination"]}}\
+ -p'Sampling-rate: ' \
+ value
+ if ! [[ $value =~ $expr ]]
+ then
+ echo "Invalid frequency value: $value" >&2
+ comeagain
+ fi
+ }
+ comeagain
+ if [ -n "${destinationfrequency["$destination"]}" ] \
+ && (( value != ${destinationfrequency["$destination"]} ))
+ then
+ setupRegen frequency
+ fi
+ destinationfrequency["$destination"]=$value
+ cat <<-EODesc
+
+ Higher-Than (bitrate, integer):
+ Only reencode files with bitrates higher then kbps. This
+ only applies if sample-rate, channel count and of course format are
+ equal. If unset, only files with bitrates equal to that of the
+ target will be copied (actually, hardlinking will be attempted
+ first).
+
+ As Ogg Vorbis target quality is not defined by its bitrate, Ogg
+ Vorbis files will always be reencoded if unset.
+ EODesc
+ comeagain() {
+ read \
+ -e \
+ ${destinationmaxbps["$destination"]+-i${destinationmaxbps["$destination"]}}\
+ -p'Higher-Than: ' \
+ value
+ if ! [[ $value =~ $expr ]]
+ then
+ echo "Invalid higher-than bitrate value: $value" >&2
+ comeagain
+ fi
+ }
+ comeagain
+ if [ -n "${destinationmaxbps["$destination"]}" ] \
+ && (( value != ${destinationmaxbps["$destination"]} ))
+ then
+ setupRegen maxbps
+ fi
+ destinationmaxbps[$destination]="$value"
+ unset regen
+ unset expr
+ fi
+}
diff --git a/lib/setup/destinations b/lib/setup/destinations
new file mode 100644
index 0000000..4e1c2b3
--- /dev/null
+++ b/lib/setup/destinations
@@ -0,0 +1,80 @@
+#!/bin/bash
+
+setupDestinations() {
+ cat <<-EODesc
+
+
+[Destinations]
+ Finally, we'll setup your destination(s).
+ EODesc
+ if (( ${#destinations[@]} ))
+ then
+ cat <<-EODesc
+
+ [Existing destinations]
+ We will review your currently configured destinations. Clear the 'Name'
+ field to remove one.
+ EODesc
+ for destination in "${destinations[@]}"
+ do
+ cat <<-EODesc
+
+ Name (string):
+ A simple name for this destination. Clear to remove this destination.
+ EODesc
+ expr='^[A-z0-9]*$'
+ comeagain() {
+ read -p'Name: ' -e -i"$destination" value
+ if [ -z "$value" ]
+ then
+ read \
+ -p"Really remove destination $destination? [y/N]"
+ if [[ $REPLY == y ]]
+ then
+ removeDestination "$destination"
+ continue
+ else
+ value="$destination"
+ fi
+ elif ! [[ $value =~ $expr ]]
+ then
+ echo "Invalid name $value." \
+ 'Please use only' \
+ 'alphanumeric characters.' >&2
+ comeagain
+ fi
+ }
+ comeagain
+ destination="$value"
+ setupDestination
+ done
+ fi
+ cat <<-EODesc
+
+ [New destinations]
+ This section will loop until you enter an empty 'Name'.
+ EODesc
+ for (( i=0 ; 1 ; i++ ))
+ do
+ cat <<-EODesc
+
+ Name (string):
+ A simple name for this destination. Empty string to end this
+ configuration loop.
+ EODesc
+ expr='^[A-z0-9]*$'
+ comeagain() {
+ read -p'Name: ' value
+ [ -z "$value" ] && return 1
+ if ! [[ $value =~ $expr ]]
+ then
+ echo "Invalid name $value. Please use" \
+ 'only alphanumeric characters.' >&2
+ comeagain
+ fi
+ }
+ comeagain || break
+ destination="$value"
+ setupDestination
+ done
+}
diff --git a/lib/setup/general b/lib/setup/general
new file mode 100644
index 0000000..3d6d0c3
--- /dev/null
+++ b/lib/setup/general
@@ -0,0 +1,132 @@
+#!/bin/bash
+
+setupGeneral() {
+ cat <<-EODesc
+
+[General]
+ We will start by setting the general parameters defining the program's
+ behavior.
+ EODesc
+ cat <<-EODesc
+
+ Target load (integer):
+ Defines how parallel processing will behave. AtOM will try to keep the
+ 1 minute load average between and +1 by adjusting
+ concurrency. Initial concurrency will be set to half of that value.
+ EODesc
+ expr='^[0-9]*$'
+ comeagain() {
+ read \
+ -e \
+ -p'Target load: (integer) ' \
+ ${maxload+-i"$maxload"} \
+ value
+ if [ -n "$value" ] && [[ $value =~ $expr ]]
+ then
+ maxload="$value"
+ else
+ echo "Invalid max-load value: $value" >&2
+ comeagain
+ fi
+ }
+ comeagain
+ cat <<-EODesc
+
+ Load interval (seconds, integer):
+ How often should we check the load average and adjust concurrency. Set
+ this too low, and concurrency may be increased too quickly. Set this
+ too high, and AtOM will not adapt quickly enough to load increase. In
+ both cases, your hard drive will suffer. In my experience, 30 seconds
+ is a good value.
+ EODesc
+ comeagain() {
+ read \
+ -e \
+ -p'Load interval: (integer) ' \
+ -i${loadinterval:-30} \
+ value
+ if [[ $value =~ $expr ]]
+ then
+ loadinterval="$value"
+ else
+ echo "Invalid load-interval value: $value" >&2
+ comeagain
+ fi
+ }
+ comeagain
+ cat <<-EODesc
+
+ Ionice [niceness]:
+ IO-hungry processes will be run with ionice class and niceness
+ [niceness] (if applicable). See man ionice for details.
+ EODesc
+ comeagain() {
+ read \
+ -e \
+ -p'Ionice: <1-3> [0-7] ' \
+ -i"${class:-3} ${niceness}" \
+ class niceness
+ case $class in
+ 1)
+ # real-time class, only root can do that
+ if (( UID ))
+ then
+ echo "IO class 'realtime' is"\
+ "not available to unprivileged"\
+ "users" >&2
+ comeagain
+ fi
+ if [ -n "$niceness" ] \
+ && (( niceness >= 0 && niceness <= 7 ))
+ then
+ ionice="ionice -c1 -n$niceness "
+ else
+ echo "Invalid IO priority"\
+ "'$niceness'" >&2
+ comeagain
+ fi
+ ;;
+ 2)
+ if [ -n "$niceness" ] \
+ && (( niceness >= 0 && niceness <= 7 ))
+ then
+ ionice="ionice -c2 -n$niceness "
+ else
+ echo "Invalid IO priority"\
+ "'$niceness'" >&2
+ comeagain
+ fi
+ ;;
+ 3)
+ ionice="ionice -c3 "
+ ;;
+ *)
+ echo "Invalid ionice parameters $value"\
+ >&2
+ comeagain
+ ;;
+ esac
+ }
+ comeagain
+ cat <<-EODesc
+
+ Temporary Directory (path):
+ Name speaks for itself: this is where FIFOs (for communicating with
+ sqlite) and temporary WAVE files will be created. Note that debug logs
+ (if enabled) will go there too.
+ EODesc
+ read \
+ -e \
+ -i"${tempdir:-$HOME/.atom/tmp}" \
+ -p'Temporary directory ( for completion): '\
+ tempdir
+ cat <<-EODesc
+
+ Database (filename):
+ EODesc
+ read \
+ -e \
+ -i"${database:-$HOME/.atom/atom.db}" \
+ -p'Database file ( for completion): ' \
+ database
+}
diff --git a/lib/setup/regen b/lib/setup/regen
new file mode 100644
index 0000000..c6c9fe8
--- /dev/null
+++ b/lib/setup/regen
@@ -0,0 +1,28 @@
+#!/bin/bash
+
+setupRegen() {
+ (( regen )) && return 0
+ echo "Parameter $1 for destination $destination changed."
+ read -p'Regenerate all files? [y/n] '
+ case $REPLY in
+ y)
+ regen=1
+ openDatabase
+ if forcedestid=$(Select destinations id <<<"name = $destination")
+ then
+ echo "Resetting destination files timestamps on" \
+ "$destination ($forcedestid)..."
+ Update destination_files last_change 0 \
+ <<<"destination_id = $forcedestid"
+ else
+ echo "Destination $destination does not exist!" >&2
+ fi
+ closeDatabase
+ ;;
+ n)
+ ;;
+ *)
+ setupRegen $1
+ ;;
+ esac
+}
diff --git a/lib/setup/setup b/lib/setup/setup
new file mode 100644
index 0000000..a41601f
--- /dev/null
+++ b/lib/setup/setup
@@ -0,0 +1,82 @@
+#!/bin/bash
+
+setup() {
+ cat <<-EOStartConf
+You will now be asked (hopefully) simple questions to help you configure AtOM's
+behavior.
+
+Completion is available for prompts asking for a paths or filenames.
+ EOStartConf
+ setupGeneral
+ setupSource
+ setupDestinations
+ unset expr
+ writeConfig >"$cffile".tmp
+ unset \
+ sourcepath \
+ skippeddirectories \
+ maxload \
+ loadinterval \
+ ionice \
+ tempdir \
+ database \
+ debug \
+ destinationchannels \
+ destinationfat32compat \
+ destinationcopymime \
+ destinationformat \
+ destinationfrequency \
+ destinationid \
+ destinationloss \
+ destinationmaxbps \
+ destinationnormalize \
+ destinationpath \
+ destinationquality \
+ destinationrename \
+ destinationnoresample \
+ destinationrenamepath \
+ destinationskipmime
+ declare -A \
+ destinationchannels \
+ destinationfat32compat \
+ destinationcopymime \
+ destinationformat \
+ destinationfrequency \
+ destinationid \
+ destinationloss \
+ destinationmaxbps \
+ destinationnormalize \
+ destinationpath \
+ destinationquality \
+ destinationrename \
+ destinationnoresample \
+ destinationrenamepath \
+ destinationskipmime
+ oldcffile="$cffile"
+ cffile="$cffile".tmp
+ getConfig
+ {
+ echo $'Please review your new configuration:\n'
+ printConfig
+ }| less -F -e
+ cffile="$oldcffile"
+ read -p'Write config file? [y/N] ' do_write
+ case $do_write in
+ y)
+ mv -f "$cffile".tmp "$cffile"
+ ;;
+ *)
+ rm "$cffile".tmp
+ read -p'Re-run (s)etup, (q)uit [s/Q] ' do_rerun
+ case $do_rerun in
+ s) setup ;;
+ *) exit ;;
+ esac
+ ;;
+ esac
+ read -p'Run now? [Y/n] ' do_run
+ case $do_run in
+ n) exit ;;
+ *) ;;
+ esac
+}
diff --git a/lib/setup/source b/lib/setup/source
new file mode 100644
index 0000000..03c2c5a
--- /dev/null
+++ b/lib/setup/source
@@ -0,0 +1,58 @@
+#!/bin/bash
+
+setupSource() {
+ cat <<-EODesc
+
+[Source]
+ Here we will define which directory AtOM should look for media files, and
+ what it should completely ignore.
+ EODesc
+ cat <<-EODesc
+
+ Path (path):
+ Which directory to scan for new media files.
+ EODesc
+ comeagain() {
+ read \
+ -e \
+ -i"${sourcepath:-/var/lib/mpd/music}" \
+ -p'Music collection ( for completion): ' \
+ sourcepath
+ if ! [ -d "$sourcepath" ]
+ then
+ echo "$sourcepath does not exist or is not a" \
+ "directory!" >&2
+ comeagain
+ fi
+ }
+ comeagain
+ cat <<-EODesc
+
+ Skip (path):
+ Files in these directories will be ignored.
+ Path is relative to $sourcepath.
+
+ This prompt will loop until an empty string is encountered.
+ EODesc
+ cd "$sourcepath"
+ count=${#skippeddirectories[@]}
+ for (( i=0 ; 1 ; i++ ))
+ do
+ read \
+ -e \
+ ${skippeddirectories[i]+-i"${skippeddirectories[i]}"}\
+ -p'Skip: ' \
+ value
+ if [ -n "$value" ]
+ then
+ skippeddirectories[i]="$value"
+ elif (( i < count ))
+ then
+ unset skippeddirectories[i]
+ else
+ break
+ fi
+ done
+ unset count
+ cd - >/dev/null
+}
diff --git a/lib/tags/getInfos::ffmpeg b/lib/tags/ffmpeg
similarity index 85%
rename from lib/tags/getInfos::ffmpeg
rename to lib/tags/ffmpeg
index e03e879..da91fbc 100644
--- a/lib/tags/getInfos::ffmpeg
+++ b/lib/tags/ffmpeg
@@ -1,5 +1,5 @@
#!/bin/bash
-getInfosffmpeg_version='ffmpeg-2'
+getInfosffmpeg_version='ffmpeg-5'
tagreaders+=( "$getInfosffmpeg_version" )
getInfos::ffmpeg() {
tagreader="$getInfosffmpeg_version"
@@ -18,7 +18,7 @@ getInfos::ffmpeg() {
echo -e "$allinfos" \
|sed -n \
'/codec_type=audio/,/\[STREAM\]/{
- /^\(sample_rate\|bit_rate\|channels\)=/{
+ /^\(sample_fmt\|sample_rate\|bit_rate\|channels\)=/{
p
}
}'
@@ -28,6 +28,7 @@ getInfos::ffmpeg() {
album=$(gettag album)
artist=$(gettag artist)
composer=$(gettag composer)
+ disc=$(gettag disc)
genre=$(gettag genre)
performer=$(gettag TOPE)
title=$(gettag title)
@@ -42,6 +43,8 @@ getInfos::ffmpeg() {
channels=$(gettag channels)
rate=$(gettag 'sample_rate')
bitrate=$(gettag 'bit_rate')
+ bitdepth=$(gettag 'sample_fmt')
+ bitdepth=${bitdepth//[A-z]/}
if [[ $bitrate == N/A ]]
then
unset bitrate
diff --git a/lib/tags/getInfos::FLAC b/lib/tags/flac
similarity index 87%
rename from lib/tags/getInfos::FLAC
rename to lib/tags/flac
index 288eac7..32a37be 100644
--- a/lib/tags/getInfos::FLAC
+++ b/lib/tags/flac
@@ -1,5 +1,5 @@
#!/bin/bash
-getInfosFLAC_version='FLAC-1'
+getInfosFLAC_version='FLAC-3'
tagreaders+=( "$getInfosFLAC_version" )
getInfos::FLAC() {
tagreader="$getInfosFLAC_version"
@@ -26,7 +26,8 @@ getInfos::FLAC() {
genre=$(gettag genre)
performer=$(gettag performer)
title=$(gettag title)
- tracknum="$(gettag tracknumber)/$(gettag tracktotal)"
+ tracknum=$(gettag tracknumber)
+ tracktotal=$(gettag tracktotal)
year=$(gettag date)
if [ -n "$tracknum" -a -n "$tracktotal" ]
then
@@ -36,10 +37,12 @@ getInfos::FLAC() {
{
read rate
read channels
+ read bitdepth
} < <(
metaflac \
--show-sample-rate \
--show-channels \
+ --show-bps \
"$sourcepath/$filename"
)
}
diff --git a/lib/tags/getInfos::WebM b/lib/tags/getInfos::WebM
new file mode 100644
index 0000000..a40a6b7
--- /dev/null
+++ b/lib/tags/getInfos::WebM
@@ -0,0 +1,17 @@
+#!/bin/bash
+getInfoswebm_version='webm-3:'"$getInfosffmpeg_version"
+tagreaders+=( "$getInfoswebm_version" )
+getInfos::WebM() {
+ getInfos::ffmpeg_video
+ tagreader="$getInfoswebm_version"
+ local infos=$(
+ mkvextract tracks \
+ "$sourcepath/$filename" \
+ 1:>(ogginfo /dev/stdin) \
+ | sed 's/\t//;s/: /=/g'
+ )
+ rate=$(gettag rate|head -n1)
+ channels=$(gettag channels|head -n1)
+ bitrate=$(gettag 'average bitrate')
+ bitrate=${bitrate%%,*}
+}
diff --git a/lib/tags/getInfos::ffmpeg_other b/lib/tags/getInfos::ffmpeg_other
new file mode 100644
index 0000000..273627d
--- /dev/null
+++ b/lib/tags/getInfos::ffmpeg_other
@@ -0,0 +1,7 @@
+#!/bin/bash
+getInfosffmpeg_other_version='ffmpeg_other-3:'"$getInfosffmpeg_version"
+tagreaders+=( "$getInfosffmpeg_other_version" )
+getInfos::ffmpeg_other() {
+ getInfos::ffmpeg
+ tagreader="$getInfosffmpeg_other_version"
+}
diff --git a/lib/tags/getInfos::ffmpeg_video b/lib/tags/getInfos::ffmpeg_video
new file mode 100644
index 0000000..cb04459
--- /dev/null
+++ b/lib/tags/getInfos::ffmpeg_video
@@ -0,0 +1,7 @@
+#!/bin/bash
+getInfosffmpeg_video_version='ffmpeg_video-3:'"$getInfosffmpeg_version"
+tagreaders+=( "$getInfosffmpeg_video_version" )
+getInfos::ffmpeg_video() {
+ getInfos::ffmpeg
+ tagreader="$getInfosffmpeg_video_version"
+}
diff --git a/lib/tags/getTags b/lib/tags/getTags
deleted file mode 100644
index 3f6d9e1..0000000
--- a/lib/tags/getTags
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/bin/bash
-getTags_version='unknown-4'
-tagreaders+=( "$getTags_version" )
-getTags() {
- unset type
- case "$mimetype" in
- audio/mpeg)
- type=ffmpeg
- ;;
- 'application/ogg opus')
- type=Opus
- ;;
- application/ogg*)
- type=Ogg
- ;;
- audio/x-flac)
- type=FLAC
- ;;
- video/*)
- type=ffmpeg
- ;;
- *)
- type=ffmpeg
- ;;
- esac
- if [ -n "$type" ]
- then
- getInfos::$type
- else
- tagreader=$getTags_version
- fi
-}
diff --git a/lib/tags/gettags b/lib/tags/gettags
new file mode 100644
index 0000000..8afae86
--- /dev/null
+++ b/lib/tags/gettags
@@ -0,0 +1,49 @@
+#!/bin/bash
+getTags_version='unknown-4'
+getTags() {
+ unset type
+ case "$mimetype" in
+ audio/mpeg)
+ type=ffmpeg
+ (( disableffprobe )) && unset type
+ ;;
+ 'application/ogg opus')
+ type=Opus
+ (( disableopusinfo )) && unset type
+ ;;
+ application/ogg*)
+ type=soxi
+ (( disablesoxi )) && unset type
+ ;;
+ audio/ogg)
+ type=soxi
+ (( disablesoxi )) && unset type
+ ;;
+ audio/x-flac)
+ type=FLAC
+ (( disableflac )) && unset type
+ ;;
+ audio/flac)
+ type=FLAC
+ (( disableflac )) && unset type
+ ;;
+ video/webm)
+ type=WebM
+ (( disablemkvextract || disableogginfo )) && unset type
+ ;;
+ video/*)
+ type=ffmpeg_video
+ (( disableffprobe )) && unset type
+ ;;
+ *)
+ type=ffmpeg_other
+ (( disableffprobe )) && unset type
+ ;;
+ esac
+ if [ -n "$type" ]
+ then
+ getInfos::$type
+ else
+ tagreader=$getTags_version
+ fi
+}
diff --git a/lib/tags/getInfos::Opus b/lib/tags/opus
similarity index 100%
rename from lib/tags/getInfos::Opus
rename to lib/tags/opus
diff --git a/lib/tags/getInfos::Ogg b/lib/tags/soxi
similarity index 52%
rename from lib/tags/getInfos::Ogg
rename to lib/tags/soxi
index 658c3ab..ccd60fd 100644
--- a/lib/tags/getInfos::Ogg
+++ b/lib/tags/soxi
@@ -1,11 +1,10 @@
#!/bin/bash
-getInfosOgg_version='Ogg-2'
-tagreaders+=( "$getInfosOgg_version" )
-getInfos::Ogg() {
- tagreader="$getInfosOgg_version"
+getInfosSoxi_version='soxi-1'
+tagreaders+=( "$getInfosSoxi_version" )
+getInfos::soxi() {
+ tagreader="$getInfosSoxi_version"
infos=$(
- ogginfo "$sourcepath/$filename" \
- | sed 's/\t//'
+ soxi "$sourcepath/$filename"
)
albumartist=$(gettag albumartist)
album=$(gettag album)
@@ -22,9 +21,12 @@ getInfos::Ogg() {
tracknum="$tracknum/$tracktotal"
fi
year=$(gettag date)
- infos="${infos//: /=}"
- rate=$(gettag rate|head -n1)
- channels=$(gettag channels|head -n1)
- bitrate=$(gettag 'average bitrate')
- bitrate=${bitrate%%,*}
+ infos="${infos//*( ): /=}"
+ rate=$(gettag 'sample rate')
+ channels=$(gettag channels)
+ bitrate=$(gettag 'bit rate')
+ bitrate=${bitrate%k}
+ bitrate=${bitrate%%.*}
+ bitdepth=$(gettag precision)
+ bitdepth=${bitdepth%-bit}
}
diff --git a/lib/tags/updateTags b/lib/tags/update
similarity index 80%
rename from lib/tags/updateTags
rename to lib/tags/update
index 9881843..dad68c4 100644
--- a/lib/tags/updateTags
+++ b/lib/tags/update
@@ -14,6 +14,7 @@ updateTags() {
tags.albumartist,
tags.artist,
tags.composer,
+ tags.depth,
tags.disc,
tags.genre,
tags.performer,
@@ -39,7 +40,11 @@ updateTags() {
CAST(source_files.last_change AS TEXT)
OR ('"$tagreaderclause"')
)
- AND mime_type_actions.action = 1;
+ AND mime_type_actions.action = 1
+ ORDER BY source_files.id' >&3
+(( maxbatch )) && echo "LIMIT $maxbatch" >&3
+echo '
+;
SELECT "AtOM:NoMoreFiles";' >&3
read -u4 line
@@ -68,6 +73,8 @@ updateTags() {
rest=${rest#*::AtOM:SQL:Sep::}
oldcomposer=${rest%%::AtOM:SQL:Sep::*}
rest=${rest#*::AtOM:SQL:Sep::}
+ olddepth=${rest%%::AtOM:SQL:Sep::*}
+ rest=${rest#*::AtOM:SQL:Sep::}
olddisc=${rest%%::AtOM:SQL:Sep::*}
rest=${rest#*::AtOM:SQL:Sep::}
oldgenre=${rest%%::AtOM:SQL:Sep::*}
@@ -85,7 +92,8 @@ updateTags() {
oldchannels=${rest%%::AtOM:SQL:Sep::*}
rest=${rest#*::AtOM:SQL:Sep::}
oldbitrate=${rest%%::AtOM:SQL:Sep::*}
- echo -en "\rTags: $((++count*100/filecount))%"
+ ((++count))
+ (( cron )) || echo -en "\rTags: $((count*100/filecount))%"
if (( count % 1000 == 0 ))
then
echo 'COMMIT;BEGIN TRANSACTION;' >&3
@@ -98,6 +106,7 @@ updateTags() {
[[ $oldalbumartist != "$albumartist" ]]&&uaa=1
[[ $oldartist != "$artist" ]]&& uar=1
[[ $oldcomposer != "$composer" ]]&& uco=1
+ [[ $olddepth != "$bitdepth" ]]&& ude=1
[[ $olddisc != "$disc" ]]&& udi=1
[[ $oldgenre != "$genre" ]]&& uge=1
[[ $oldperformer != "$performer" ]]&& upe=1
@@ -108,15 +117,16 @@ updateTags() {
[[ $oldchannels != "$channels" ]]&& uch=1
[[ $oldbitrate != "$bitrate" ]]&& ubi=1
Update tags \
- ${ual:+album "::AtOM:FT::${album:-NULL}"}\
- ${uaa:+albumartist "::AtOM:FT::${albumartist:-NULL}"}\
- ${uar:+artist "::AtOM:FT::${artist:-NULL}"}\
- ${uco:+composer "::AtOM:FT::${composer:-NULL}"}\
+ ${ual:+album "${album:+::AtOM:FT::}${album:-NULL}"}\
+ ${uaa:+albumartist "${albumartist:+::AtOM:FT::}${albumartist:-NULL}"}\
+ ${uar:+artist "${artist:+::AtOM:FT::}${artist:-NULL}"}\
+ ${uco:+composer "${composer:+::AtOM:FT::}${composer:-NULL}"}\
+ ${ude:+depth "${bitdepth:-NULL}"} \
${udi:+disc "${disc:-NULL}"} \
${uge:+genre "${genre:-NULL}"} \
- ${upe:+performer "::AtOM:FT::${performer:-NULL}"}\
- ${uti:+title "::AtOM:FT::${title:-NULL}"}\
- ${utr:+track "::AtOM:FT::${tracknum:-NULL}"}\
+ ${upe:+performer "${performer:+::AtOM:FT::}${performer:-NULL}"}\
+ ${uti:+title "${title:+::AtOM:FT::}${title:-NULL}"}\
+ ${utr:+track "${track:+::AtOM:FT::}${tracknum:-NULL}"}\
${uye:+year "${year:-NULL}"} \
last_change "$lastchange" \
${ura:+rate "${rate:-NULL}"} \
@@ -135,12 +145,14 @@ updateTags() {
composer \
performer \
rate \
+ bitdepth \
bitrate \
channels \
ual \
uaa \
uar \
uco \
+ ude \
udi \
uge \
upe \
@@ -153,6 +165,9 @@ updateTags() {
fi
done
echo 'COMMIT;' >&3
- echo -e "\rRead tags from ${count:-0} files.\033[K"
+ (( cron )) || echo -n $'\r'
+ echo -n "Read tags from ${count:-0} files."
+ (( cron )) || echo -ne "\033[K"
+ echo
unset count tagfiles
}
diff --git a/lib/tools/ascii b/lib/tools/ascii
new file mode 100644
index 0000000..5f474af
--- /dev/null
+++ b/lib/tools/ascii
@@ -0,0 +1,15 @@
+#!/bin/bash
+
+ascii() {
+ coproc toascii {
+ perl -e '
+ use utf8;
+ use Encode;
+ use Text::Unidecode;
+ binmode STDIN, ":encoding(UTF-8)";
+ $| = 1;
+ while (<>) {
+ print(unidecode($_));
+ }'
+ }
+}
diff --git a/lib/tools/progressSpin b/lib/tools/progressSpin
index cecff55..40dbb52 100644
--- a/lib/tools/progressSpin
+++ b/lib/tools/progressSpin
@@ -1,10 +1,15 @@
#!/bin/bash
progressSpin() {
- case $(( ++count % 40 )) in
- 0) echo -ne '\b|' ;;
- 10) echo -ne '\b/' ;;
- 20) echo -en '\b-' ;;
- 30) echo -ne '\b\\' ;;
- *) ;;
- esac
+ if (( cron ))
+ then
+ (( ++count ))
+ else
+ case $(( ++count % 40 )) in
+ 0) echo -n $'\b|' ;;
+ 10) echo -n $'\b/' ;;
+ 20) echo -n $'\b-' ;;
+ 30) echo -n $'\b\\' ;;
+ *) ;;
+ esac
+ fi
}
diff --git a/lib/video/extractaudio b/lib/video/extractaudio
index d8d3344..b77a048 100644
--- a/lib/video/extractaudio
+++ b/lib/video/extractaudio
@@ -2,5 +2,5 @@
extractAudio() {
tmpfile="${fileid}ffmpeg"
commandline=(${ionice}ffmpeg -v 0 -vn -y)
- commandline+=(-i "$sourcepath/$filename" "$tempdir/$tmpfile.wav")
+ commandline+=(-i "$sourcepath/$filename" -map a:0 "$tempdir/$tmpfile".wav)
}
diff --git a/lib/workers/checkworkers b/lib/workers/check
similarity index 100%
rename from lib/workers/checkworkers
rename to lib/workers/check
diff --git a/lib/workers/cleaner b/lib/workers/cleaner
index 3db4e0a..b72a53e 100644
--- a/lib/workers/cleaner
+++ b/lib/workers/cleaner
@@ -10,8 +10,8 @@ cleaner() {
EOWhere
)
(( failed+=faildepends ))
+ (( ran+=faildepends ))
Update tasks status 2 <<<"id = $taskid"
- Update tasks status 2 <<<"requires = $taskid"
echo "SELECT COUNT(*)
FROM tasks
WHERE ( status = 0 OR status = 1 )
@@ -46,6 +46,16 @@ cleaner() {
" SELECT rename_pattern" \
" FROM tasks" \
" WHERE id=$taskid" \
+ " )," \
+ " fat32compat=(" \
+ " SELECT fat32compat" \
+ " FROM tasks" \
+ " WHERE id=$taskid" \
+ " )," \
+ " ascii=(" \
+ " SELECT ascii" \
+ " FROM tasks" \
+ " WHERE id=$taskid" \
" )" \
"WHERE id=$destfileid;" \
>&3
diff --git a/lib/workers/createworker b/lib/workers/create
similarity index 100%
rename from lib/workers/createworker
rename to lib/workers/create
diff --git a/lib/workers/destroyworker b/lib/workers/destroy
similarity index 100%
rename from lib/workers/destroyworker
rename to lib/workers/destroy
diff --git a/lib/workers/getworkerid b/lib/workers/getid
similarity index 100%
rename from lib/workers/getworkerid
rename to lib/workers/getid
diff --git a/lib/workers/master b/lib/workers/master
index a6d0c99..e24956f 100644
--- a/lib/workers/master
+++ b/lib/workers/master
@@ -48,6 +48,36 @@ master() {
cmd_arg27,
cmd_arg28,
cmd_arg29,
+ cmd_arg30,
+ cmd_arg31,
+ cmd_arg32,
+ cmd_arg33,
+ cmd_arg34,
+ cmd_arg35,
+ cmd_arg36,
+ cmd_arg37,
+ cmd_arg38,
+ cmd_arg39,
+ cmd_arg40,
+ cmd_arg41,
+ cmd_arg42,
+ cmd_arg43,
+ cmd_arg44,
+ cmd_arg45,
+ cmd_arg46,
+ cmd_arg47,
+ cmd_arg48,
+ cmd_arg49,
+ cmd_arg50,
+ cmd_arg51,
+ cmd_arg52,
+ cmd_arg53,
+ cmd_arg54,
+ cmd_arg55,
+ cmd_arg56,
+ cmd_arg57,
+ cmd_arg58,
+ cmd_arg59,
cleanup,
fileid,
filename
@@ -62,7 +92,31 @@ master() {
if (( remaining == 0 ))
then
sleep 0.1
- continue
+ return 0
+ elif (( active == 0 && ready == 0 ))
+ then
+ dumpfile=tasks-$(date +%Y%m%d%H%M%S).csv
+ cat <<-EOF
+
+
+ $remaining TASKS LEFT, NONE READY!
+
+ Something went wrong, dumping tasks table to $dumpfile
+ EOF
+ cat >&3 <<-EOSQL
+ .mode csv
+ .headers on
+ .output $dumpfile
+ SELECT * from tasks;
+ .mode list
+ .headers off
+ .output stdout
+ EOSQL
+ closeDatabase
+ echo "Waiting for children to come back home..."
+ wait
+ echo $'\nGood luck!'
+ exit 1
elif (( ready == 0 ))
then
sleep 0.1
@@ -135,6 +189,66 @@ master() {
rest=${rest#*::AtOM:SQL:Sep::}
cmd_arg+=("${rest%%::AtOM:SQL:Sep::*}")
rest=${rest#*::AtOM:SQL:Sep::}
+ cmd_arg+=("${rest%%::AtOM:SQL:Sep::*}")
+ rest=${rest#*::AtOM:SQL:Sep::}
+ cmd_arg+=("${rest%%::AtOM:SQL:Sep::*}")
+ rest=${rest#*::AtOM:SQL:Sep::}
+ cmd_arg+=("${rest%%::AtOM:SQL:Sep::*}")
+ rest=${rest#*::AtOM:SQL:Sep::}
+ cmd_arg+=("${rest%%::AtOM:SQL:Sep::*}")
+ rest=${rest#*::AtOM:SQL:Sep::}
+ cmd_arg+=("${rest%%::AtOM:SQL:Sep::*}")
+ rest=${rest#*::AtOM:SQL:Sep::}
+ cmd_arg+=("${rest%%::AtOM:SQL:Sep::*}")
+ rest=${rest#*::AtOM:SQL:Sep::}
+ cmd_arg+=("${rest%%::AtOM:SQL:Sep::*}")
+ rest=${rest#*::AtOM:SQL:Sep::}
+ cmd_arg+=("${rest%%::AtOM:SQL:Sep::*}")
+ rest=${rest#*::AtOM:SQL:Sep::}
+ cmd_arg+=("${rest%%::AtOM:SQL:Sep::*}")
+ rest=${rest#*::AtOM:SQL:Sep::}
+ cmd_arg+=("${rest%%::AtOM:SQL:Sep::*}")
+ rest=${rest#*::AtOM:SQL:Sep::}
+ cmd_arg+=("${rest%%::AtOM:SQL:Sep::*}")
+ rest=${rest#*::AtOM:SQL:Sep::}
+ cmd_arg+=("${rest%%::AtOM:SQL:Sep::*}")
+ rest=${rest#*::AtOM:SQL:Sep::}
+ cmd_arg+=("${rest%%::AtOM:SQL:Sep::*}")
+ rest=${rest#*::AtOM:SQL:Sep::}
+ cmd_arg+=("${rest%%::AtOM:SQL:Sep::*}")
+ rest=${rest#*::AtOM:SQL:Sep::}
+ cmd_arg+=("${rest%%::AtOM:SQL:Sep::*}")
+ rest=${rest#*::AtOM:SQL:Sep::}
+ cmd_arg+=("${rest%%::AtOM:SQL:Sep::*}")
+ rest=${rest#*::AtOM:SQL:Sep::}
+ cmd_arg+=("${rest%%::AtOM:SQL:Sep::*}")
+ rest=${rest#*::AtOM:SQL:Sep::}
+ cmd_arg+=("${rest%%::AtOM:SQL:Sep::*}")
+ rest=${rest#*::AtOM:SQL:Sep::}
+ cmd_arg+=("${rest%%::AtOM:SQL:Sep::*}")
+ rest=${rest#*::AtOM:SQL:Sep::}
+ cmd_arg+=("${rest%%::AtOM:SQL:Sep::*}")
+ rest=${rest#*::AtOM:SQL:Sep::}
+ cmd_arg+=("${rest%%::AtOM:SQL:Sep::*}")
+ rest=${rest#*::AtOM:SQL:Sep::}
+ cmd_arg+=("${rest%%::AtOM:SQL:Sep::*}")
+ rest=${rest#*::AtOM:SQL:Sep::}
+ cmd_arg+=("${rest%%::AtOM:SQL:Sep::*}")
+ rest=${rest#*::AtOM:SQL:Sep::}
+ cmd_arg+=("${rest%%::AtOM:SQL:Sep::*}")
+ rest=${rest#*::AtOM:SQL:Sep::}
+ cmd_arg+=("${rest%%::AtOM:SQL:Sep::*}")
+ rest=${rest#*::AtOM:SQL:Sep::}
+ cmd_arg+=("${rest%%::AtOM:SQL:Sep::*}")
+ rest=${rest#*::AtOM:SQL:Sep::}
+ cmd_arg+=("${rest%%::AtOM:SQL:Sep::*}")
+ rest=${rest#*::AtOM:SQL:Sep::}
+ cmd_arg+=("${rest%%::AtOM:SQL:Sep::*}")
+ rest=${rest#*::AtOM:SQL:Sep::}
+ cmd_arg+=("${rest%%::AtOM:SQL:Sep::*}")
+ rest=${rest#*::AtOM:SQL:Sep::}
+ cmd_arg+=("${rest%%::AtOM:SQL:Sep::*}")
+ rest=${rest#*::AtOM:SQL:Sep::}
cleanup=${rest%%::AtOM:SQL:Sep::*}
rest=${rest#*::AtOM:SQL:Sep::}
destfileid=${rest%%::AtOM:SQL:Sep::*}
diff --git a/lib/workers/worker b/lib/workers/worker
index c4b5954..5df3376 100644
--- a/lib/workers/worker
+++ b/lib/workers/worker
@@ -4,12 +4,3 @@ worker() {
(( debug >= 2 )) && echo "${cmd_arg[@]}" >&2
"${cmd_arg[@]}" >/dev/null
}
-createworker() {
- worker $1 &
- workers[$1]=$!
-}
-destroyworker() {
- dyingworker=${workers[$1]}
- unset workers[$1]
- wait $dyingworker
-}
diff --git a/share/schema.sql b/share/schema.sql
index 1859f55..62fa9d1 100644
--- a/share/schema.sql
+++ b/share/schema.sql
@@ -1,4 +1,7 @@
BEGIN TRANSACTION;
+CREATE TABLE IF NOT EXISTS atom (
+ version INTEGER
+);
CREATE TABLE IF NOT EXISTS source_files (
id INTEGER PRIMARY KEY,
filename TEXT UNIQUE NOT NULL,
@@ -12,13 +15,16 @@ CREATE TABLE IF NOT EXISTS source_files (
);
CREATE TABLE IF NOT EXISTS destinations (
id INTEGER PRIMARY KEY,
- name TEXT UNIQUE NOT NULL
+ name TEXT UNIQUE NOT NULL,
+ enabled INTEGER DEFAULT 1
);
CREATE TABLE IF NOT EXISTS destination_files (
id INTEGER PRIMARY KEY,
filename TEXT,
old_filename TEXT,
rename_pattern TEXT,
+ fat32compat INTEGER,
+ ascii INTEGER,
last_change FLOAT NOT NULL DEFAULT 0,
source_file_id INTEGER,
destination_id INTEGER,
@@ -52,6 +58,7 @@ CREATE TABLE IF NOT EXISTS tags (
title TEXT,
composer TEXT,
performer TEXT,
+ depth INTEGER,
rate INTEGER,
channels INTEGER,
bitrate INTEGER,
@@ -110,8 +117,7 @@ CREATE TRIGGER IF NOT EXISTS create_tags AFTER INSERT ON source_files
BEGIN
INSERT INTO tags (source_file,last_change) VALUES (new.id,0);
END;
-DROP TRIGGER IF EXISTS force_destination_update_on_tag_update;
-CREATE TRIGGER force_destination_update_on_tag_update
+CREATE TRIGGER IF NOT EXISTS force_destination_update_on_tag_update
AFTER UPDATE OF
genre,
albumartist,
@@ -125,7 +131,8 @@ CREATE TRIGGER force_destination_update_on_tag_update
performer,
rate,
channels,
- bitrate
+ bitrate,
+ bitdepth
ON tags
BEGIN
UPDATE destination_files SET last_change=0
diff --git a/toys/checkextensions b/toys/checkextensions
index 3a427ff..08da0b0 100755
--- a/toys/checkextensions
+++ b/toys/checkextensions
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
# config structures
declare -A \
diff --git a/toys/cleandestinations b/toys/cleandestinations
index b80e767..b5a35e8 100755
--- a/toys/cleandestinations
+++ b/toys/cleandestinations
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
# config structures
declare -A \
diff --git a/toys/createindex b/toys/createindex
index ccc4d27..6a7ddc0 100755
--- a/toys/createindex
+++ b/toys/createindex
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
#!/bin/bash
diff --git a/toys/lowquality b/toys/lowquality
index 0e3395b..1adfdab 100755
--- a/toys/lowquality
+++ b/toys/lowquality
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
# config structures
declare -A \
diff --git a/toys/missingtags b/toys/missingtags
index 72f723b..417444a 100755
--- a/toys/missingtags
+++ b/toys/missingtags
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
# config structures
declare -A \