Modify the repository version to v0.1.0

This commit is contained in:
2025-11-21 17:43:37 +08:00
parent e9b8231327
commit 70ee50f4d8
19 changed files with 472 additions and 1835 deletions

View File

@@ -1,22 +0,0 @@
WEBSITE_NAME_CN=MeowEmbeddedMusicServer # Your website name in chinese
WEBSITE_NAME_EN=MeowEmbeddedMusicServer # Your website name in English
WEBSITE_TITLE_CN=MeowEmbeddedMusicServer # Your website title in chinese
WEBSITE_TITLE_EN=MeowEmbeddedMusicServer # Your website title in English
WEBSITE_DESC_CN=A music server for embedded devices # Your website description in chinese
WEBSITE_DESC_EN=A music server for embedded devices # Your website description in English
WEBSITE_KEYWORDS_CN=music, embedded, server # Your website keywords in chinese
WEBSITE_KEYWORDS_EN=music, embedded, server # Your website keywords in English
WEBSITE_FAVICON=/favicon.ico # Your website favicon
WEBSITE_BACKGROUND=/background.webp # Your website background image
WEBSITE_URL=http://127.0.0.1:2233 # Your website URL
EMBEDDED_WEBSITE_URL=http://127.0.0.1:2233 # Your embedded website URL
PORT=2233 # Your website port
FONTAWESOME_CDN=https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css # Fontawesome CDN
# Yuafeng free API sources
API_SOURCES=kuwo
API_SOURCES_1=netease
API_SOURCES_2=migu
API_SOURCES_3=baidu

View File

@@ -1,28 +0,0 @@
name: build_and_test
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
jobs:
build_and_test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Update System
run: |
sudo apt update && sudo apt upgrade -y
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.25'
- name: Install Modules
run: go mod tidy
- name: Test
run: go test -v ./...

641
LICENSE
View File

@@ -1,201 +1,504 @@
Apache License GNU LESSER GENERAL PUBLIC LICENSE
Version 2.0, January 2004 Version 2.1, February 1999
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION Copyright (C) 1991, 1999 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
1. Definitions. [This is the first released version of the Lesser GPL. It also counts
as the successor of the GNU Library Public License, version 2, hence
the version number 2.1.]
"License" shall mean the terms and conditions for use, reproduction, Preamble
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by The licenses for most software are designed to take away your
the copyright owner that is granting the License. freedom to share and change it. By contrast, the GNU General Public
Licenses are intended to guarantee your freedom to share and change
free software--to make sure the software is free for all its users.
"Legal Entity" shall mean the union of the acting entity and all This license, the Lesser General Public License, applies to some
other entities that control, are controlled by, or are under common specially designated software packages--typically libraries--of the
control with that entity. For the purposes of this definition, Free Software Foundation and other authors who decide to use it. You
"control" means (i) the power, direct or indirect, to cause the can use it too, but we suggest you first think carefully about whether
direction or management of such entity, whether by contract or this license or the ordinary General Public License is the better
otherwise, or (ii) ownership of fifty percent (50%) or more of the strategy to use in any particular case, based on the explanations below.
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity When we speak of free software, we are referring to freedom of use,
exercising permissions granted by this License. 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 this service if you wish); that you receive source code or can get
it if you want it; that you can change the software and use pieces of
it in new free programs; and that you are informed that you can do
these things.
"Source" form shall mean the preferred form for making modifications, To protect your rights, we need to make restrictions that forbid
including but not limited to software source code, documentation distributors to deny you these rights or to ask you to surrender these
source, and configuration files. rights. These restrictions translate to certain responsibilities for
you if you distribute copies of the library or if you modify it.
"Object" form shall mean any form resulting from mechanical For example, if you distribute copies of the library, whether gratis
transformation or translation of a Source form, including but or for a fee, you must give the recipients all the rights that we gave
not limited to compiled object code, generated documentation, you. You must make sure that they, too, receive or can get the source
and conversions to other media types. code. If you link other code with the library, you must provide
complete object files to the recipients, so that they can relink them
with the library after making changes to the library and recompiling
it. And you must show them these terms so they know their rights.
"Work" shall mean the work of authorship, whether in Source or We protect your rights with a two-step method: (1) we copyright the
Object form, made available under the License, as indicated by a library, and (2) we offer you this license, which gives you legal
copyright notice that is included in or attached to the work permission to copy, distribute and/or modify the library.
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object To protect each distributor, we want to make it very clear that
form, that is based on (or derived from) the Work and for which the there is no warranty for the free library. Also, if the library is
editorial revisions, annotations, elaborations, or other modifications modified by someone else and passed on, the recipients should know
represent, as a whole, an original work of authorship. For the purposes that what they have is not the original version, so that the original
of this License, Derivative Works shall not include works that remain author's reputation will not be affected by problems that might be
separable from, or merely link (or bind by name) to the interfaces of, introduced by others.
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including Finally, software patents pose a constant threat to the existence of
the original version of the Work and any modifications or additions any free program. We wish to make sure that a company cannot
to that Work or Derivative Works thereof, that is intentionally effectively restrict the users of a free program by obtaining a
submitted to Licensor for inclusion in the Work by the copyright owner restrictive license from a patent holder. Therefore, we insist that
or by an individual or Legal Entity authorized to submit on behalf of any patent license obtained for a version of the library must be
the copyright owner. For the purposes of this definition, "submitted" consistent with the full freedom of use specified in this license.
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity Most GNU software, including some libraries, is covered by the
on behalf of whom a Contribution has been received by Licensor and ordinary GNU General Public License. This license, the GNU Lesser
subsequently incorporated within the Work. General Public License, applies to certain designated libraries, and
is quite different from the ordinary General Public License. We use
this license for certain libraries in order to permit linking those
libraries into non-free programs.
2. Grant of Copyright License. Subject to the terms and conditions of When a program is linked with a library, whether statically or using
this License, each Contributor hereby grants to You a perpetual, a shared library, the combination of the two is legally speaking a
worldwide, non-exclusive, no-charge, royalty-free, irrevocable combined work, a derivative of the original library. The ordinary
copyright license to reproduce, prepare Derivative Works of, General Public License therefore permits such linking only if the
publicly display, publicly perform, sublicense, and distribute the entire combination fits its criteria of freedom. The Lesser General
Work and such Derivative Works in Source or Object form. Public License permits more lax criteria for linking other code with
the library.
3. Grant of Patent License. Subject to the terms and conditions of We call this license the "Lesser" General Public License because it
this License, each Contributor hereby grants to You a perpetual, does Less to protect the user's freedom than the ordinary General
worldwide, non-exclusive, no-charge, royalty-free, irrevocable Public License. It also provides other free software developers Less
(except as stated in this section) patent license to make, have made, of an advantage over competing non-free programs. These disadvantages
use, offer to sell, sell, import, and otherwise transfer the Work, are the reason we use the ordinary General Public License for many
where such license applies only to those patent claims licensable libraries. However, the Lesser license provides advantages in certain
by such Contributor that are necessarily infringed by their special circumstances.
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the For example, on rare occasions, there may be a special need to
Work or Derivative Works thereof in any medium, with or without encourage the widest possible use of a certain library, so that it becomes
modifications, and in Source or Object form, provided that You a de-facto standard. To achieve this, non-free programs must be
meet the following conditions: allowed to use the library. A more frequent case is that a free
library does the same job as widely used non-free libraries. In this
case, there is little to gain by limiting the free library to free
software only, so we use the Lesser General Public License.
(a) You must give any other recipients of the Work or In other cases, permission to use a particular library in non-free
Derivative Works a copy of this License; and programs enables a greater number of people to use a large body of
free software. For example, permission to use the GNU C Library in
non-free programs enables many more people to use the whole GNU
operating system, as well as its variant, the GNU/Linux operating
system.
(b) You must cause any modified files to carry prominent notices Although the Lesser General Public License is Less protective of the
stating that You changed the files; and users' freedom, it does ensure that the user of a program that is
linked with the Library has the freedom and the wherewithal to run
that program using a modified version of the Library.
(c) You must retain, in the Source form of any Derivative Works The precise terms and conditions for copying, distribution and
that You distribute, all copyright, patent, trademark, and modification follow. Pay close attention to the difference between a
attribution notices from the Source form of the Work, "work based on the library" and a "work that uses the library". The
excluding those notices that do not pertain to any part of former contains code derived from the library, whereas the latter must
the Derivative Works; and be combined with the library in order to run.
(d) If the Work includes a "NOTICE" text file as part of its GNU LESSER GENERAL PUBLIC LICENSE
distribution, then any Derivative Works that You distribute must TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and 0. This License Agreement applies to any software library or other
may provide additional or different license terms and conditions program which contains a notice placed by the copyright holder or
for use, reproduction, or distribution of Your modifications, or other authorized party saying it may be distributed under the terms of
for any such Derivative Works as a whole, provided Your use, this Lesser General Public License (also called "this License").
reproduction, and distribution of the Work otherwise complies with Each licensee is addressed as "you".
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, A "library" means a collection of software functions and/or data
any Contribution intentionally submitted for inclusion in the Work prepared so as to be conveniently linked with application programs
by You to the Licensor shall be under the terms and conditions of (which use some of those functions and data) to form executables.
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade The "Library", below, refers to any such software library or work
names, trademarks, service marks, or product names of the Licensor, which has been distributed under these terms. A "work based on the
except as required for reasonable and customary use in describing the Library" means either the Library or any derivative work under
origin of the Work and reproducing the content of the NOTICE file. copyright law: that is to say, a work containing the Library or a
portion of it, either verbatim or with modifications and/or translated
straightforwardly into another language. (Hereinafter, translation is
included without limitation in the term "modification".)
7. Disclaimer of Warranty. Unless required by applicable law or "Source code" for a work means the preferred form of the work for
agreed to in writing, Licensor provides the Work (and each making modifications to it. For a library, complete source code means
Contributor provides its Contributions) on an "AS IS" BASIS, all the source code for all modules it contains, plus any associated
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or interface definition files, plus the scripts used to control compilation
implied, including, without limitation, any warranties or conditions and installation of the library.
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, Activities other than copying, distribution and modification are not
whether in tort (including negligence), contract, or otherwise, covered by this License; they are outside its scope. The act of
unless required by applicable law (such as deliberate and grossly running a program using the Library is not restricted, and output from
negligent acts) or agreed to in writing, shall any Contributor be such a program is covered only if its contents constitute a work based
liable to You for damages, including any direct, indirect, special, on the Library (independent of the use of the Library in a tool for
incidental, or consequential damages of any character arising as a writing it). Whether that is true depends on what the Library does
result of this License or out of the use or inability to use the and what the program that uses the Library does.
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing 1. You may copy and distribute verbatim copies of the Library's
the Work or Derivative Works thereof, You may choose to offer, complete source code as you receive it, in any medium, provided that
and charge a fee for, acceptance of support, warranty, indemnity, you conspicuously and appropriately publish on each copy an
or other liability obligations and/or rights consistent with this appropriate copyright notice and disclaimer of warranty; keep intact
License. However, in accepting such obligations, You may act only all the notices that refer to this License and to the absence of any
on Your own behalf and on Your sole responsibility, not on behalf warranty; and distribute a copy of this License along with the
of any other Contributor, and only if You agree to indemnify, Library.
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS You may charge a fee for the physical act of transferring a copy,
and you may at your option offer warranty protection in exchange for a
fee.
APPENDIX: How to apply the Apache License to your work. 2. You may modify your copy or copies of the Library or any portion
of it, thus forming a work based on the Library, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
To apply the Apache License to your work, attach the following a) The modified work must itself be a software library.
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [2025] [YooTrans] b) You must cause the files modified to carry prominent notices
stating that you changed the files and the date of any change.
Licensed under the Apache License, Version 2.0 (the "License"); c) You must cause the whole of the work to be licensed at no
you may not use this file except in compliance with the License. charge to all third parties under the terms of this License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 d) If a facility in the modified Library refers to a function or a
table of data to be supplied by an application program that uses
the facility, other than as an argument passed when the facility
is invoked, then you must make a good faith effort to ensure that,
in the event an application does not supply such function or
table, the facility still operates, and performs whatever part of
its purpose remains meaningful.
Unless required by applicable law or agreed to in writing, software (For example, a function in a library to compute square roots has
distributed under the License is distributed on an "AS IS" BASIS, a purpose that is entirely well-defined independent of the
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. application. Therefore, Subsection 2d requires that any
See the License for the specific language governing permissions and application-supplied function or table used by this function must
limitations under the License. be optional: if the application does not supply it, the square
root function must still compute square roots.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Library,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Library, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Library.
In addition, mere aggregation of another work not based on the Library
with the Library (or with a work based on the Library) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may opt to apply the terms of the ordinary GNU General Public
License instead of this License to a given copy of the Library. To do
this, you must alter all the notices that refer to this License, so
that they refer to the ordinary GNU General Public License, version 2,
instead of to this License. (If a newer version than version 2 of the
ordinary GNU General Public License has appeared, then you can specify
that version instead if you wish.) Do not make any other change in
these notices.
Once this change is made in a given copy, it is irreversible for
that copy, so the ordinary GNU General Public License applies to all
subsequent copies and derivative works made from that copy.
This option is useful when you wish to copy part of the code of
the Library into a program that is not a library.
4. You may copy and distribute the Library (or a portion or
derivative of it, under Section 2) in object code or executable form
under the terms of Sections 1 and 2 above provided that you accompany
it with the complete corresponding machine-readable source code, which
must be distributed under the terms of Sections 1 and 2 above on a
medium customarily used for software interchange.
If distribution of object code is made by offering access to copy
from a designated place, then offering equivalent access to copy the
source code from the same place satisfies the requirement to
distribute the source code, even though third parties are not
compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the
Library, but is designed to work with the Library by being compiled or
linked with it, is called a "work that uses the Library". Such a
work, in isolation, is not a derivative work of the Library, and
therefore falls outside the scope of this License.
However, linking a "work that uses the Library" with the Library
creates an executable that is a derivative of the Library (because it
contains portions of the Library), rather than a "work that uses the
library". The executable is therefore covered by this License.
Section 6 states terms for distribution of such executables.
When a "work that uses the Library" uses material from a header file
that is part of the Library, the object code for the work may be a
derivative work of the Library even though the source code is not.
Whether this is true is especially significant if the work can be
linked without the Library, or if the work is itself a library. The
threshold for this to be true is not precisely defined by law.
If such an object file uses only numerical parameters, data
structure layouts and accessors, and small macros and small inline
functions (ten lines or less in length), then the use of the object
file is unrestricted, regardless of whether it is legally a derivative
work. (Executables containing this object code plus portions of the
Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may
distribute the object code for the work under the terms of Section 6.
Any executables containing that work also fall under Section 6,
whether or not they are linked directly with the Library itself.
6. As an exception to the Sections above, you may also combine or
link a "work that uses the Library" with the Library to produce a
work containing portions of the Library, and distribute that work
under terms of your choice, provided that the terms permit
modification of the work for the customer's own use and reverse
engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the
Library is used in it and that the Library and its use are covered by
this License. You must supply a copy of this License. If the work
during execution displays copyright notices, you must include the
copyright notice for the Library among them, as well as a reference
directing the user to the copy of this License. Also, you must do one
of these things:
a) Accompany the work with the complete corresponding
machine-readable source code for the Library including whatever
changes were used in the work (which must be distributed under
Sections 1 and 2 above); and, if the work is an executable linked
with the Library, with the complete machine-readable "work that
uses the Library", as object code and/or source code, so that the
user can modify the Library and then relink to produce a modified
executable containing the modified Library. (It is understood
that the user who changes the contents of definitions files in the
Library will not necessarily be able to recompile the application
to use the modified definitions.)
b) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (1) uses at run time a
copy of the library already present on the user's computer system,
rather than copying library functions into the executable, and (2)
will operate properly with a modified version of the library, if
the user installs one, as long as the modified version is
interface-compatible with the version that the work was made with.
c) Accompany the work with a written offer, valid for at
least three years, to give the same user the materials
specified in Subsection 6a, above, for a charge no more
than the cost of performing this distribution.
d) If distribution of the work is made by offering access to copy
from a designated place, offer equivalent access to copy the above
specified materials from the same place.
e) Verify that the user has already received a copy of these
materials or that you have already sent this user a copy.
For an executable, the required form of the "work that uses the
Library" must include any data and utility programs needed for
reproducing the executable from it. However, as a special exception,
the materials to be distributed need not include anything that is
normally distributed (in either source or binary form) with the major
components (compiler, kernel, and so on) of the operating system on
which the executable runs, unless that component itself accompanies
the executable.
It may happen that this requirement contradicts the license
restrictions of other proprietary libraries that do not normally
accompany the operating system. Such a contradiction means you cannot
use both them and the Library together in an executable that you
distribute.
7. You may place library facilities that are a work based on the
Library side-by-side in a single library together with other library
facilities not covered by this License, and distribute such a combined
library, provided that the separate distribution of the work based on
the Library and of the other library facilities is otherwise
permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work
based on the Library, uncombined with any other library
facilities. This must be distributed under the terms of the
Sections above.
b) Give prominent notice with the combined library of the fact
that part of it is a work based on the Library, and explaining
where to find the accompanying uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute
the Library except as expressly provided under this License. Any
attempt otherwise to copy, modify, sublicense, link with, or
distribute the Library is void, and will automatically terminate your
rights under this License. However, parties who have received copies,
or rights, from you under this License will not have their licenses
terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Library or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Library (or any work based on the
Library), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the
Library), the recipient automatically receives a license from the
original licensor to copy, distribute, link with or modify the Library
subject to these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties with
this License.
11. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
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
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Library at all. For example, if a patent
license would not permit royalty-free redistribution of the Library by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Library under this License may add
an explicit geographical distribution limitation excluding those countries,
so that distribution is permitted only in or among countries not thus
excluded. In such case, this License incorporates the limitation as if
written in the body of this License.
13. The Free Software Foundation may publish revised and/or new
versions of the Lesser 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 Library
specifies a version number of this License which applies to it and
"any later version", you have the option of following the terms and
conditions either of that version or of any later version published by
the Free Software Foundation. If the Library does not specify a
license version number, you may choose any version ever published by
the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free
programs whose distribution conditions are incompatible with these,
write to the author to ask for permission. For software which is
copyrighted by the Free Software Foundation, write to the Free
Software Foundation; we sometimes make exceptions for this. Our
decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE LIBRARY "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
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
AND/OR REDISTRIBUTE THE LIBRARY 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
LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Libraries
If you develop a new library, and you want it to be of the greatest
possible use to the public, we recommend making it free software that
everyone can redistribute and change. You can do so by permitting
redistribution under these terms (or, alternatively, under the terms of the
ordinary General Public License).
To apply these terms, attach the following notices to the library. It is
safest to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.
<one line to give the library's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
USA
Also add information on how to contact you by electronic and paper mail.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the library, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the
library `Frob' (a library for tweaking knobs) written by James Random
Hacker.
<signature of Ty Coon>, 1 April 1990
Ty Coon, President of Vice
That's all there is to it!

12
NOTICE
View File

@@ -1,12 +0,0 @@
This project is licensed under the Apache 2.0 license.
However,
we need to provide you with some additional notices based on this license!
You may distribute copies of this work or its derivative works in any medium,
in source or object form, with or without modifications.
If you make modifications,
you must make your modified source copies publicly available.
You may add your own copyright notice to your modified works and may provide additional or
different licensing terms and conditions
for the use, copying, or distribution of your modified works,
or for any such derivative works as a whole,
but you must not remove the copyright notice from the original work.

View File

@@ -1,24 +0,0 @@
# MeowEmbeddedMusicServer
[English](README.md) | [简体中文](README_zh-CN.md)
Your Embedded Music Server for you.
## Features
- Play music from your server
- Music streaming for your embedded devices
- Music library management
- Music search and cache
# Tutorial document
Please refer to the [wiki](https://github.com/IntelligentlyEverything/MeowEmbeddedMusicServer/wiki).
## Star History
<a href="https://star-history.com/#IntelligentlyEverything/MeowEmbeddedMusicServer&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=IntelligentlyEverything/MeowEmbeddedMusicServer&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=IntelligentlyEverything/MeowEmbeddedMusicServer&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=IntelligentlyEverything/MeowEmbeddedMusicServer&type=Date" />
</picture>
</a>

View File

@@ -1,26 +0,0 @@
# Meow 为嵌入式设备制作的音乐串流服务
[English](README.md) | [简体中文](README_zh-CN.md)
MeowEmbeddedMusicServer 是一个为嵌入式设备制作的音乐串流服务。
它可以播放来自你的服务器的音乐,也可以为你的嵌入式设备提供音乐流媒体服务。
它还可以管理音乐库,并且可以搜索和下载音乐。
## 特性
- 在线听音乐
- 为嵌入式设备提供音乐串流服务
- 管理音乐库
- 搜索和缓存音乐
# 教程文档
请参阅 [维基](https://github.com/IntelligentlyEverything/MeowEmbeddedMusicServer/wiki).
## Star 历史
<a href="https://star-history.com/#IntelligentlyEverything/MeowEmbeddedMusicServer&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=IntelligentlyEverything/MeowEmbeddedMusicServer&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=IntelligentlyEverything/MeowEmbeddedMusicServer&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=IntelligentlyEverything/MeowEmbeddedMusicServer&type=Date" />
</picture>
</a>

191
api.go
View File

@@ -1,191 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"path/filepath"
"strings"
)
// APIHandler handles API requests.
func apiHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Server", "MeowMusicEmbeddedServer")
w.Header().Set("Content-Type", "application/json; charset=utf-8")
queryParams := r.URL.Query()
fmt.Printf("[Web Access] Handling request for %s?%s\n", r.URL.Path, queryParams.Encode())
song := queryParams.Get("song")
singer := queryParams.Get("singer")
ip, err := IPhandler(r)
if err != nil {
ip = "0.0.0.0"
}
if song == "" {
musicItem := MusicItem{
FromCache: false,
IP: ip,
}
json.NewEncoder(w).Encode(musicItem)
return
}
// Attempt to retrieve music items from sources.json
sources := readSources()
var musicItem MusicItem
var found bool = false
// Build request scheme
var scheme string
if r.TLS == nil {
scheme = "http"
} else {
scheme = "https"
}
for _, source := range sources {
if source.Title == song {
if singer == "" || source.Artist == singer {
// Determine the protocol for each URL and build accordingly
var audioURL, audioFullURL, m3u8URL, lyricURL, coverURL string
if strings.HasPrefix(source.AudioURL, "http://") {
audioURL = scheme + "://" + r.Host + "/url/http/" + url.QueryEscape(strings.TrimPrefix(source.AudioURL, "http://"))
} else if strings.HasPrefix(source.AudioURL, "https://") {
audioURL = scheme + "://" + r.Host + "/url/https/" + url.QueryEscape(strings.TrimPrefix(source.AudioURL, "https://"))
} else {
audioURL = scheme + "://" + r.Host + "/" + url.QueryEscape(source.AudioURL)
}
if strings.HasPrefix(source.AudioFullURL, "http://") {
audioFullURL = scheme + "://" + r.Host + "/url/http/" + url.QueryEscape(strings.TrimPrefix(source.AudioFullURL, "http://"))
} else if strings.HasPrefix(source.AudioFullURL, "https://") {
audioFullURL = scheme + "://" + r.Host + "/url/https/" + url.QueryEscape(strings.TrimPrefix(source.AudioFullURL, "https://"))
} else {
audioFullURL = scheme + "://" + r.Host + "/" + url.QueryEscape(source.AudioFullURL)
}
if strings.HasPrefix(source.M3U8URL, "http://") {
m3u8URL = scheme + "://" + r.Host + "/url/http/" + url.QueryEscape(strings.TrimPrefix(source.M3U8URL, "http://"))
} else if strings.HasPrefix(source.M3U8URL, "https://") {
m3u8URL = scheme + "://" + r.Host + "/url/https/" + url.QueryEscape(strings.TrimPrefix(source.M3U8URL, "https://"))
} else {
m3u8URL = scheme + "://" + r.Host + "/" + url.QueryEscape(source.M3U8URL)
}
if strings.HasPrefix(source.LyricURL, "http://") {
lyricURL = scheme + "://" + r.Host + "/url/http/" + url.QueryEscape(strings.TrimPrefix(source.LyricURL, "http://"))
} else if strings.HasPrefix(source.LyricURL, "https://") {
lyricURL = scheme + "://" + r.Host + "/url/https/" + url.QueryEscape(strings.TrimPrefix(source.LyricURL, "https://"))
} else {
lyricURL = scheme + "://" + r.Host + "/" + url.QueryEscape(source.LyricURL)
}
if strings.HasPrefix(source.CoverURL, "http://") {
coverURL = scheme + "://" + r.Host + "/url/http/" + url.QueryEscape(strings.TrimPrefix(source.CoverURL, "http://"))
} else if strings.HasPrefix(source.CoverURL, "https://") {
coverURL = scheme + "://" + r.Host + "/url/https/" + url.QueryEscape(strings.TrimPrefix(source.CoverURL, "https://"))
} else {
coverURL = scheme + "://" + r.Host + "/" + url.QueryEscape(source.CoverURL)
}
musicItem = MusicItem{
Title: source.Title,
Artist: source.Artist,
AudioURL: audioURL,
AudioFullURL: audioFullURL,
M3U8URL: m3u8URL,
LyricURL: lyricURL,
CoverURL: coverURL,
Duration: source.Duration,
FromCache: false,
}
found = true
break
}
}
}
// If not found in sources.json, attempt to retrieve from local folder
if !found {
musicItem = getLocalMusicItem(song, singer)
musicItem.FromCache = false
if musicItem.Title != "" {
if musicItem.AudioURL != "" {
musicItem.AudioURL = scheme + "://" + r.Host + musicItem.AudioURL
}
if musicItem.AudioFullURL != "" {
musicItem.AudioFullURL = scheme + "://" + r.Host + musicItem.AudioFullURL
}
if musicItem.M3U8URL != "" {
musicItem.M3U8URL = scheme + "://" + r.Host + musicItem.M3U8URL
}
if musicItem.LyricURL != "" {
musicItem.LyricURL = scheme + "://" + r.Host + musicItem.LyricURL
}
if musicItem.CoverURL != "" {
musicItem.CoverURL = scheme + "://" + r.Host + musicItem.CoverURL
}
found = true
}
}
// If still not found, attempt to retrieve from cache file
if !found {
fmt.Println("[Info] Reading music from cache.")
// Fuzzy matching for singer and song
files, err := filepath.Glob("./cache/*.json")
if err != nil {
fmt.Println("[Error] Error reading cache directory:", err)
return
}
for _, file := range files {
if strings.Contains(filepath.Base(file), song) && (singer == "" || strings.Contains(filepath.Base(file), singer)) {
musicItem, found = readFromCache(file)
if found {
if musicItem.AudioURL != "" {
musicItem.AudioURL = scheme + "://" + r.Host + musicItem.AudioURL
}
if musicItem.AudioFullURL != "" {
musicItem.AudioFullURL = scheme + "://" + r.Host + musicItem.AudioFullURL
}
if musicItem.M3U8URL != "" {
musicItem.M3U8URL = scheme + "://" + r.Host + musicItem.M3U8URL
}
if musicItem.LyricURL != "" {
musicItem.LyricURL = scheme + "://" + r.Host + musicItem.LyricURL
}
if musicItem.CoverURL != "" {
musicItem.CoverURL = scheme + "://" + r.Host + musicItem.CoverURL
}
musicItem.FromCache = true
break
}
}
}
}
// If still not found, request and cache the music item in a separate goroutine
if !found {
fmt.Println("[Info] Updating music item cache from API request.")
musicItem = requestAndCacheMusic(song, singer)
fmt.Println("[Info] Music item cache updated.")
musicItem.FromCache = false
musicItem.AudioURL = scheme + "://" + r.Host + musicItem.AudioURL
musicItem.AudioFullURL = scheme + "://" + r.Host + musicItem.AudioFullURL
musicItem.M3U8URL = scheme + "://" + r.Host + musicItem.M3U8URL
musicItem.LyricURL = scheme + "://" + r.Host + musicItem.LyricURL
musicItem.CoverURL = scheme + "://" + r.Host + musicItem.CoverURL
found = true
}
// If still not found, return an empty MusicItem
if !found {
musicItem = MusicItem{
FromCache: false,
IP: ip,
}
} else {
musicItem.IP = ip
}
json.NewEncoder(w).Encode(musicItem)
}

1
cache/.gitignore vendored
View File

@@ -1 +0,0 @@

212
file.go
View File

@@ -1,212 +0,0 @@
package main
import (
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
)
// ListFiles function: Traverse all files in the specified directory and return a slice of the file path
func ListFiles(dir string) ([]string, error) {
var files []string
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
files = append(files, path)
}
return nil
})
return files, err
}
// Get Content function: Read the content of a specified file and return it
func GetFileContent(filePath string) ([]byte, error) {
file, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer file.Close()
// Get File Size
fileInfo, err := file.Stat()
if err != nil {
return nil, err
}
fileSize := fileInfo.Size()
// Read File Content
fileContent := make([]byte, fileSize)
_, err = file.Read(fileContent)
if err != nil {
return nil, err
}
return fileContent, nil
}
// fileHandler function: Handle file requests
func fileHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Server", "MeowMusicEmbeddedServer")
// Obtain the path of the request
filePath := r.URL.Path
// Check if the request path starts with "/url/"
if strings.HasPrefix(filePath, "/url/") {
// Extract the URL after "/url/"
urlPath := filePath[len("/url/"):]
// Decode the URL path in case it's URL encoded
decodedURL, err := url.QueryUnescape(urlPath)
if err != nil {
NotFoundHandler(w, r)
return
}
// Determine the protocol based on the URL path
var protocol string
if strings.HasPrefix(decodedURL, "http/") {
protocol = "http://"
} else if strings.HasPrefix(decodedURL, "https/") {
protocol = "https://"
} else {
NotFoundHandler(w, r)
return
}
// Remove the protocol part from the decoded URL
decodedURL = strings.TrimPrefix(decodedURL, "http/")
decodedURL = strings.TrimPrefix(decodedURL, "https/")
// Correctly concatenate the protocol with the decoded URL
decodedURL = protocol + decodedURL
// Create a new HTTP request to the decoded URL, without copying headers
req, err := http.NewRequest("GET", decodedURL, nil)
if err != nil {
NotFoundHandler(w, r)
return
}
// Send the request and get the response
client := &http.Client{}
resp, err := client.Do(req)
if err != nil || resp.StatusCode != http.StatusOK {
NotFoundHandler(w, r)
return
}
defer resp.Body.Close()
// Read the response body into a byte slice
fileContent, err := io.ReadAll(resp.Body)
if err != nil {
NotFoundHandler(w, r)
return
}
// Set appropriate Content-Type based on file extension
ext := filepath.Ext(decodedURL)
switch ext {
case ".mp3":
w.Header().Set("Content-Type", "audio/mpeg")
case ".wav":
w.Header().Set("Content-Type", "audio/wav")
case ".flac":
w.Header().Set("Content-Type", "audio/flac")
case ".aac":
w.Header().Set("Content-Type", "audio/aac")
case ".ogg":
w.Header().Set("Content-Type", "audio/ogg")
case ".m4a":
w.Header().Set("Content-Type", "audio/mp4")
case ".amr":
w.Header().Set("Content-Type", "audio/amr")
case ".jpg", ".jpeg":
w.Header().Set("Content-Type", "image/jpeg")
case ".png":
w.Header().Set("Content-Type", "image/png")
case ".gif":
w.Header().Set("Content-Type", "image/gif")
case ".bmp":
w.Header().Set("Content-Type", "image/bmp")
case ".svg":
w.Header().Set("Content-Type", "image/svg+xml")
case ".webp":
w.Header().Set("Content-Type", "image/webp")
case ".txt":
w.Header().Set("Content-Type", "text/plain")
case ".lrc":
w.Header().Set("Content-Type", "text/plain")
case ".mrc":
w.Header().Set("Content-Type", "text/plain")
case ".json":
w.Header().Set("Content-Type", "application/json")
default:
w.Header().Set("Content-Type", "application/octet-stream")
}
// Write file content to response
w.Write(fileContent)
return
}
// Construct the complete file path
fullFilePath := filepath.Join("./files", filePath)
// Try replacing '+' with ' ' and check if the file exists
tempFilePath := strings.ReplaceAll(fullFilePath, "+", " ")
if _, err := os.Stat(tempFilePath); err == nil {
fullFilePath = tempFilePath
}
// Get file content
fileContent, err := GetFileContent(fullFilePath)
if err != nil {
// If file not found, try replacing ' ' with '+' and check again
tempFilePath = strings.ReplaceAll(fullFilePath, " ", "+")
fileContent, err = GetFileContent(tempFilePath)
if err != nil {
NotFoundHandler(w, r)
return
}
}
// Set appropriate Content-Type based on file extension
ext := filepath.Ext(filePath)
switch ext {
case ".mp3":
w.Header().Set("Content-Type", "audio/mpeg")
case ".wav":
w.Header().Set("Content-Type", "audio/wav")
case ".flac":
w.Header().Set("Content-Type", "audio/flac")
case ".aac":
w.Header().Set("Content-Type", "audio/aac")
case ".ogg":
w.Header().Set("Content-Type", "audio/ogg")
case ".m4a":
w.Header().Set("Content-Type", "audio/mp4")
case ".amr":
w.Header().Set("Content-Type", "audio/amr")
case ".jpg", ".jpeg":
w.Header().Set("Content-Type", "image/jpeg")
case ".png":
w.Header().Set("Content-Type", "image/png")
case ".gif":
w.Header().Set("Content-Type", "image/gif")
case ".bmp":
w.Header().Set("Content-Type", "image/bmp")
case ".svg":
w.Header().Set("Content-Type", "image/svg+xml")
case ".webp":
w.Header().Set("Content-Type", "image/webp")
case ".txt":
w.Header().Set("Content-Type", "text/plain")
case ".lrc":
w.Header().Set("Content-Type", "text/plain")
case ".mrc":
w.Header().Set("Content-Type", "text/plain")
case ".json":
w.Header().Set("Content-Type", "application/json")
default:
w.Header().Set("Content-Type", "application/octet-stream")
}
// Write file content to response
w.Write(fileContent)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 710 KiB

5
go.mod
View File

@@ -1,5 +0,0 @@
module MeowEmbedded-MusicServer
go 1.25.0
require github.com/joho/godotenv v1.5.1

436
helper.go
View File

@@ -1,436 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"io"
"mime"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
)
// Helper function to compress and segment audio file
func compressAndSegmentAudio(inputFile, outputDir string) error {
fmt.Printf("[Info] Compress and segment audio file %s\n", inputFile)
// Compress music files
outputFile := filepath.Join(outputDir, "music.mp3")
cmd := exec.Command("ffmpeg", "-i", inputFile, "-ac", "1", "-ab", "32k", "-ar", "24000", outputFile)
err := cmd.Run()
if err != nil {
return err
}
// Split music files
chunkDir := filepath.Join(outputDir, "chunk")
err = os.MkdirAll(chunkDir, 0755)
if err != nil {
return err
}
// Using ffmpeg for segmentation
segmentedFilePattern := filepath.Join(chunkDir, "%03d.mp3") // e.g. 001.mp3, 002.mp3, ...
cmd = exec.Command("ffmpeg", "-i", outputFile, "-ac", "1", "-ab", "32k", "-ar", "16000", "-f", "segment", "-segment_time", "10", segmentedFilePattern)
err = cmd.Run()
if err != nil {
return err
}
return nil
}
// Helper function to create M3U8 playlist file
func createM3U8Playlist(outputDir string) error {
fmt.Printf("[Info] Create M3U8 playlist file for %s\n", outputDir)
playlistFile := filepath.Join(outputDir, "music.m3u8")
file, err := os.Create(playlistFile)
if err != nil {
return err
}
defer file.Close()
_, err = file.WriteString("#EXTM3U\n")
if err != nil {
return err
}
_, err = file.WriteString("#EXT-X-VERSION:3\n")
if err != nil {
return err
}
_, err = file.WriteString("#EXT-X-TARGETDURATION:10\n")
if err != nil {
return err
}
chunkDir := filepath.Join(outputDir, "chunk")
files, err := os.ReadDir(chunkDir)
if err != nil {
return err
}
var chunkFiles []string
for _, file := range files {
if strings.HasSuffix(file.Name(), ".mp3") {
chunkFiles = append(chunkFiles, file.Name())
}
}
// Sort by file name
for i := 0; i < len(chunkFiles); i++ {
for j := i + 1; j < len(chunkFiles); j++ {
if chunkFiles[i] > chunkFiles[j] {
chunkFiles[i], chunkFiles[j] = chunkFiles[j], chunkFiles[i]
}
}
}
for _, chunkFile := range chunkFiles {
_, err = file.WriteString("#EXTINF:10.000\n")
if err != nil {
return err
}
url := fmt.Sprintf("%s/cache/music/%s/%s/%s\n", os.Getenv("EMBEDDED_WEBSITE_URL"), filepath.Base(outputDir), "chunk", chunkFile)
_, err = file.WriteString(url)
}
return err
}
// Helper function to download files from URL
func downloadFile(filename string, url string) error {
fmt.Printf("[Info] Download file %s from URL %s\n", filename, url)
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
out, err := os.Create(filename)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, resp.Body)
return err
}
// Helper function to get duration of obtaining music files
func getMusicDuration(filePath string) int {
fmt.Printf("[Info] Get duration of obtaining music file %s\n", filePath)
// Use ffprobe to get audio duration
output, err := exec.Command("ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", filePath).Output()
if err != nil {
fmt.Println("[Error] Error getting audio duration:", err)
return 0
}
duration, err := strconv.ParseFloat(strings.TrimSpace(string(output)), 64)
if err != nil {
fmt.Println("[Error] Error converting duration to float:", err)
return 0
}
return int(duration)
}
// Helper function for identifying file formats
func getMusicFileExtension(url string) (string, error) {
resp, err := http.Head(url)
if err != nil {
return "", err
}
// Get file format from Content-Type header
contentType := resp.Header.Get("Content-Type")
ext, _, err := mime.ParseMediaType(contentType)
if err != nil {
return "", err
}
// Identify file extension based on file format
switch ext {
case "audio/mpeg":
return ".mp3", nil
case "audio/flac":
return ".flac", nil
case "audio/x-flac":
return ".flac", nil
case "audio/wav":
return ".wav", nil
case "audio/aac":
return ".aac", nil
case "audio/ogg":
return ".ogg", nil
case "application/octet-stream":
// Try to guess file format from URL or other information
if strings.Contains(url, ".mp3") {
return ".mp3", nil
} else if strings.Contains(url, ".flac") {
return ".flac", nil
} else if strings.Contains(url, ".wav") {
return ".wav", nil
} else if strings.Contains(url, ".aac") {
return ".aac", nil
} else if strings.Contains(url, ".ogg") {
return ".ogg", nil
} else {
return "", fmt.Errorf("unknown file format from Content-Type and URL: %s", contentType)
}
default:
return "", fmt.Errorf("unknown file format: %s", ext)
}
}
// Helper function to obtain music data from local folder
func getLocalMusicItem(song, singer string) MusicItem {
musicDir := "./files/music"
fmt.Println("[Info] Reading local folder music.")
files, err := os.ReadDir(musicDir)
if err != nil {
fmt.Println("[Error] Failed to read local music directory:", err)
return MusicItem{}
}
for _, file := range files {
if file.IsDir() {
if singer == "" {
if strings.Contains(file.Name(), song) {
dirPath := filepath.Join(musicDir, file.Name())
// Extract artist and title from the directory name
parts := strings.SplitN(file.Name(), "-", 2)
if len(parts) != 2 {
continue // Skip if the directory name doesn't contain exactly one "-"
}
artist := parts[0]
title := parts[1]
musicItem := MusicItem{
Title: title,
Artist: artist,
AudioURL: "",
AudioFullURL: "",
M3U8URL: "",
LyricURL: "",
CoverURL: "",
Duration: 0,
}
musicFilePath := filepath.Join(dirPath, "music.mp3")
if _, err := os.Stat(musicFilePath); err == nil {
musicItem.AudioURL = "/music/" + url.QueryEscape(file.Name()) + "/music.mp3"
musicItem.Duration = getMusicDuration(musicFilePath)
}
for _, audioFormat := range []string{"music_full.mp3", "music_full.flac", "music_full.wav", "music_full.aac", "music_full.ogg"} {
audioFilePath := filepath.Join(dirPath, audioFormat)
if _, err := os.Stat(audioFilePath); err == nil {
musicItem.AudioFullURL = "/music/" + url.QueryEscape(file.Name()) + "/" + audioFormat
break
}
}
m3u8FilePath := filepath.Join(dirPath, "music.m3u8")
if _, err := os.Stat(m3u8FilePath); err == nil {
musicItem.M3U8URL = "/music/" + url.QueryEscape(file.Name()) + "/music.m3u8"
}
lyricFilePath := filepath.Join(dirPath, "lyric.lrc")
if _, err := os.Stat(lyricFilePath); err == nil {
musicItem.LyricURL = "/music/" + url.QueryEscape(file.Name()) + "/lyric.lrc"
}
coverJpgFilePath := filepath.Join(dirPath, "cover.jpg")
if _, err := os.Stat(coverJpgFilePath); err == nil {
musicItem.CoverURL = "/music/" + url.QueryEscape(file.Name()) + "/cover.jpg"
} else {
coverPngFilePath := filepath.Join(dirPath, "cover.png")
if _, err := os.Stat(coverPngFilePath); err == nil {
musicItem.CoverURL = "/music/" + url.QueryEscape(file.Name()) + "/cover.png"
}
}
return musicItem
}
} else {
if strings.Contains(file.Name(), singer) && strings.Contains(file.Name(), song) {
dirPath := filepath.Join(musicDir, file.Name())
// Extract artist and title from the directory name
parts := strings.SplitN(file.Name(), "-", 2)
if len(parts) != 2 {
continue // Skip if the directory name doesn't contain exactly one "-"
}
artist := parts[0]
title := parts[1]
musicItem := MusicItem{
Title: title,
Artist: artist,
AudioURL: "",
AudioFullURL: "",
M3U8URL: "",
LyricURL: "",
CoverURL: "",
Duration: 0,
}
musicFilePath := filepath.Join(dirPath, "music.mp3")
if _, err := os.Stat(musicFilePath); err == nil {
musicItem.AudioURL = "/music/" + url.QueryEscape(file.Name()) + "/music.mp3"
musicItem.Duration = getMusicDuration(musicFilePath)
}
for _, audioFormat := range []string{"music_full.mp3", "music_full.flac", "music_full.wav", "music_full.aac", "music_full.ogg"} {
audioFilePath := filepath.Join(dirPath, audioFormat)
if _, err := os.Stat(audioFilePath); err == nil {
musicItem.AudioFullURL = "/music/" + url.QueryEscape(file.Name()) + "/" + audioFormat
break
}
}
m3u8FilePath := filepath.Join(dirPath, "music.m3u8")
if _, err := os.Stat(m3u8FilePath); err == nil {
musicItem.M3U8URL = "/music/" + url.QueryEscape(file.Name()) + "/music.m3u8"
}
lyricFilePath := filepath.Join(dirPath, "lyric.lrc")
if _, err := os.Stat(lyricFilePath); err == nil {
musicItem.LyricURL = "/music/" + url.QueryEscape(file.Name()) + "/lyric.lrc"
}
coverJpgFilePath := filepath.Join(dirPath, "cover.jpg")
if _, err := os.Stat(coverJpgFilePath); err == nil {
musicItem.CoverURL = "/music/" + url.QueryEscape(file.Name()) + "/cover.jpg"
} else {
coverPngFilePath := filepath.Join(dirPath, "cover.png")
if _, err := os.Stat(coverPngFilePath); err == nil {
musicItem.CoverURL = "/music/" + url.QueryEscape(file.Name()) + "/cover.png"
}
}
return musicItem
}
}
}
}
return MusicItem{} // If no matching folder is found, return an empty MusicItem
}
// Helper function to obtain IP address of the client
func IPhandler(r *http.Request) (string, error) {
ip := r.Header.Get("X-Real-IP")
if ip != "" {
return ip, nil
}
ip = r.Header.Get("X-Forwarded-For")
if ip != "" {
ips := strings.Split(ip, ",")
return strings.TrimSpace(ips[0]), nil
}
ip = r.RemoteAddr
if ip != "" {
return strings.Split(ip, ":")[0], nil
}
return "", fmt.Errorf("unable to obtain IP address information")
}
// Helper function to read music sources from sources.json file
func readSources() []MusicItem {
data, err := os.ReadFile("./sources.json")
fmt.Println("[Info] Reading local sources.json")
if err != nil {
fmt.Println("[Error] Failed to read sources.json:", err)
return nil
}
var sources []MusicItem
err = json.Unmarshal(data, &sources)
if err != nil {
fmt.Println("[Error] Failed to parse sources.json:", err)
return nil
}
return sources
}
// Helper function to request and cache music from API sources
func requestAndCacheMusic(song, singer string) MusicItem {
fmt.Printf("[Info] Requesting and caching music for %s", song)
// Create cache directory if it doesn't exist
err := os.MkdirAll("./cache", 0755)
if err != nil {
fmt.Println("[Error] Error creating cache directory:", err)
return MusicItem{}
}
// Get API_SOURCES and any subsequent environment variables (e.g. API_SOURCES_1, API_SOURCES_2, etc.)
var sources []string
for i := 0; ; i++ {
var key string
if i == 0 {
key = "API_SOURCES"
} else {
key = "API_SOURCES_" + strconv.Itoa(i)
}
source := os.Getenv(key)
if source == "" {
break
}
sources = append(sources, source)
}
// Request and cache music from each source in turn
var musicItem MusicItem
for _, source := range sources {
fmt.Printf("[Info] Requesting music from source: %s\n", source)
musicItem = YuafengAPIResponseHandler(strings.TrimSpace(source), song, singer)
if musicItem.Title != "" {
// If music item is valid, stop searching for sources
break
}
}
// If no valid music item was found, return an empty MusicItem
if musicItem.Title == "" {
fmt.Printf("[Warning] No valid music item retrieved.\n")
return MusicItem{}
}
// Create cache file path based on artist and title
cacheFile := fmt.Sprintf("./cache/%s-%s.json", musicItem.Artist, musicItem.Title)
// Write cache data to cache file
cacheData, err := json.MarshalIndent(musicItem, "", " ")
if err != nil {
fmt.Println("[Error] Error marshalling cache data:", err)
return MusicItem{}
}
err = os.WriteFile(cacheFile, cacheData, 0644)
if err != nil {
fmt.Println("[Error] Error writing cache file:", err)
return MusicItem{}
}
fmt.Println("[Info] Music request and caching completed successfully.")
return musicItem
}
// Helper function to read music data from cache file
func readFromCache(filePath string) (MusicItem, bool) {
data, err := os.ReadFile(filePath)
if err != nil {
fmt.Println("[Error] Failed to read cache file:", err)
return MusicItem{}, false
}
var musicItem MusicItem
err = json.Unmarshal(data, &musicItem)
if err != nil {
fmt.Println("[Error] Failed to parse cache file:", err)
return MusicItem{}, false
}
return musicItem, true
}

View File

@@ -1,17 +0,0 @@
package main
import (
"fmt"
"net/http"
"os"
)
func NotFoundHandler(w http.ResponseWriter, r *http.Request) {
home_url := os.Getenv("HOME_URL")
w.WriteHeader(http.StatusNotFound)
w.Header().Set("Server", "MeowMusicServer")
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, "<head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\"><link rel=\"icon\" href=\"favicon.ico\"><title>404 Music Lost!</title><style>@import url('https://fonts.googleapis.com/css?family=Montserrat:400,600,700');@import url('https://fonts.googleapis.com/css?family=Catamaran:400,800');.error-container {text-align: center;font-size: 106px;font-family: 'Catamaran', sans-serif;font-weight: 800;margin: 70px 15px;}.error-container>span {display: inline-block;position: relative;}.error-container>span.four {width: 136px;height: 43px;border-radius: 999px;background:linear-gradient(140deg, rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0.07) 43%, transparent 44%, transparent 100%),linear-gradient(105deg, transparent 0%, transparent 40%, rgba(0, 0, 0, 0.06) 41%, rgba(0, 0, 0, 0.07) 76%, transparent 77%, transparent 100%),linear-gradient(to right, #d89ca4, #e27b7e);}.error-container>span.four:before,.error-container>span.four:after {content: '';display: block;position: absolute;border-radius: 999px;}.error-container>span.four:before {width: 43px;height: 156px;left: 60px;bottom: -43px;background:linear-gradient(128deg, rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0.07) 40%, transparent 41%, transparent 100%),linear-gradient(116deg, rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0.07) 50%, transparent 51%, transparent 100%),linear-gradient(to top, #99749D, #B895AB, #CC9AA6, #D7969E, #E0787F);}.error-container>span.four:after {width: 137px;height: 43px;transform: rotate(-49.5deg);left: -18px;bottom: 36px;background: linear-gradient(to right, #99749D, #B895AB, #CC9AA6, #D7969E, #E0787F);}.error-container>span.zero {vertical-align: text-top;width: 156px;height: 156px;border-radius: 999px;background: linear-gradient(-45deg, transparent 0%, rgba(0, 0, 0, 0.06) 50%, transparent 51%, transparent 100%),linear-gradient(to top right, #99749D, #99749D, #B895AB, #CC9AA6, #D7969E, #ED8687, #ED8687);overflow: hidden;animation: bgshadow 5s infinite;}.error-container>span.zero:before {content: '';display: block;position: absolute;transform: rotate(45deg);width: 90px;height: 90px;background-color: transparent;left: 0px;bottom: 0px;background:linear-gradient(95deg, transparent 0%, transparent 8%, rgba(0, 0, 0, 0.07) 9%, transparent 50%, transparent 100%),linear-gradient(85deg, transparent 0%, transparent 19%, rgba(0, 0, 0, 0.05) 20%, rgba(0, 0, 0, 0.07) 91%, transparent 92%, transparent 100%);}.error-container>span.zero:after {content: '';display: block;position: absolute;border-radius: 999px;width: 70px;height: 70px;left: 43px;bottom: 43px;background: #FDFAF5;box-shadow: -2px 2px 2px 0px rgba(0, 0, 0, 0.1);}.screen-reader-text {position: absolute;top: -9999em;left: -9999em;}@keyframes bgshadow {0% {box-shadow: inset -160px 160px 0px 5px rgba(0, 0, 0, 0.4);}45% {box-shadow: inset 0px 0px 0px 0px rgba(0, 0, 0, 0.1);}55% {box-shadow: inset 0px 0px 0px 0px rgba(0, 0, 0, 0.1);}100% {box-shadow: inset 160px -160px 0px 5px rgba(0, 0, 0, 0.4);}}* {-webkit-box-sizing: border-box;-moz-box-sizing: border-box;box-sizing: border-box;}body {background-color: #FDFAF5;margin-bottom: 50px;}html,button,input,select,textarea {font-family: 'Montserrat', Helvetica, sans-serif;color: #bbb;}h1 {text-align: center;margin: 30px 15px;}.zoom-area {max-width: 490px;margin: 30px auto 30px;font-size: 19px;text-align: center;}.link-container {text-align: center;}a.more-link {text-transform: uppercase;font-size: 13px;background-color: #de7e85;padding: 10px 15px;border-radius: 0;color: #fff;display: inline-block;margin-right: 5px;margin-bottom: 5px;line-height: 1.5;text-decoration: none;margin-top: 50px;letter-spacing: 1px;}</style></head><body><h1>404 Music Lost!</h1><p class=\"zoom-area\">We couldn't find the content you were looking for.</p><section class=\"error-container\"><span class=\"four\"><span class=\"screen-reader-text\">4</span></span><span class=\"zero\"><span class=\"screen-reader-text\">0</span></span><span class=\"four\"><span class=\"screen-reader-text\">4</span></span></section>")
fmt.Fprintf(w, "<div class=\"link-container\"><a href=\"%s\" class=\"more-link\">Go Home</a></div></body>", home_url)
fmt.Printf("[Web Access] Return 404 Not Found\n")
}

407
index.go
View File

@@ -1,407 +0,0 @@
package main
import (
"fmt"
"net/http"
"os"
"path/filepath"
)
func indexHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Server", "MeowMusicEmbeddedServer")
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Printf("[Web Access] Handling request for %s\n", r.URL.Path)
if r.URL.Path != "/" {
fileHandler(w, r)
return
}
// Serve index.html in theme directory
indexPath := filepath.Join("theme", "index.html")
// Check if index.html exists in theme directory
if _, err := os.Stat(indexPath); os.IsNotExist(err) {
defaultIndexPage(w)
} else if err != nil {
defaultIndexPage(w)
} else {
http.ServeFile(w, r, indexPath)
fmt.Printf("[Web Access] Return custom index pages\n")
}
}
func defaultIndexPage(w http.ResponseWriter) {
websiteVersion := "0.0.1"
websiteNameCN := os.Getenv("WEBSITE_NAME_CN")
if websiteNameCN == "" {
websiteNameCN = "🎵 音乐搜索"
}
websiteNameEN := os.Getenv("WEBSITE_NAME_EN")
if websiteNameEN == "" {
websiteNameEN = "🎵 Music Search"
}
websiteTitleCN := os.Getenv("WEBSITE_TITLE_CN")
if websiteTitleCN == "" {
websiteTitleCN = "为嵌入式设备设计的音乐搜索服务器"
}
websiteTitleEN := os.Getenv("WEBSITE_TITLE_EN")
if websiteTitleEN == "" {
websiteTitleEN = "Music Search Server for Embedded Devices"
}
websiteDescCN := os.Getenv("WEBSITE_DESC_CN")
if websiteDescCN == "" {
websiteDescCN = "搜索并播放您喜爱的音乐"
}
websiteDescEN := os.Getenv("WEBSITE_DESC_EN")
if websiteDescEN == "" {
websiteDescEN = "Search and play your favorite music"
}
websiteKeywordsCN := os.Getenv("WEBSITE_KEYWORDS_CN")
if websiteKeywordsCN == "" {
websiteKeywordsCN = "音乐, 搜索, 嵌入式"
}
websiteKeywordsEN := os.Getenv("WEBSITE_KEYWORDS_EN")
if websiteKeywordsEN == "" {
websiteKeywordsEN = "music, search, embedded"
}
websiteFavicon := os.Getenv("WEBSITE_FAVICON")
if websiteFavicon == "" {
websiteFavicon = "/favicon.ico"
}
websiteBackground := os.Getenv("WEBSITE_BACKGROUND")
if websiteBackground == "" {
websiteBackground = "/background.webp"
}
fontawesomeCDN := os.Getenv("FONTAWESOME_CDN")
if fontawesomeCDN == "" {
fontawesomeCDN = "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"
}
// Build HTML
fmt.Fprintf(w, "<!DOCTYPE html><html>")
fmt.Fprintf(w, "<head>")
fmt.Fprintf(w, "<meta charset=\"UTF-8\">")
fmt.Fprintf(w, "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">")
fmt.Fprintf(w, "<meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\">")
fmt.Fprintf(w, "<link rel=\"icon\" href=\"%s\">", websiteFavicon)
fmt.Fprintf(w, "<link rel=\"stylesheet\" href=\"%s\">", fontawesomeCDN)
fmt.Fprintf(w, "<title></title><style>")
// HTML style
fmt.Fprintf(w, "body {background-image: url('%s');background-size: cover;background-repeat: no-repeat;background-attachment: fixed;display: flex;justify-content: center;align-items: center;margin: 60px 0;}", websiteBackground)
fmt.Fprintf(w, ".container {background: rgba(255, 255, 255, 0.4);width: 65%%;border-radius: 20px;box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);backdrop-filter: blur(10px);display: flex;flex-direction: column;}")
fmt.Fprintf(w, ".title {font-size: 36px;font-weight: bold;margin: 25px auto 0px auto;text-align: center;}.description {font-size: 1.1rem;color: #4f596b;margin: 10px auto;text-align: center;}")
fmt.Fprintf(w, ".search-form {display: flex;justify-content: center;align-items: center;width: 100%%;margin-bottom: 20px;}.songContainer,.singerContainer,.searchContainer {display: flex;align-items: center;margin: 0 10px;}")
fmt.Fprintf(w, ".songInput {padding: 10px;border: 2px solid #ccc;border-radius: 20px;height: 45px;margin-left: -6%%;margin-right: 10px;font-size: 1.1rem;width: 110%%;background-color: rgba(255, 255, 255, 0.4);}")
fmt.Fprintf(w, ".artistInput {padding: 10px;border: 2px solid #ccc;border-radius: 20px;height: 45px;margin-left: 7%%;margin-right: 10px;font-size: 1.1rem;width: 80%%;background-color: rgba(255, 255, 255, 0.4);}")
fmt.Fprintf(w, ".searchBtn {padding: 10px 20px;border: none;background-image: linear-gradient(to right, pink, deeppink);color: white;margin-left: -20%%;font-size: 1.1rem;border-radius: 20px;width: 128%%;height: 60px;cursor: pointer;transition: all 0.3s ease;}")
fmt.Fprintf(w, "@media (max-width: 768px) {.search-form {flex-direction: column;align-items: flex-start;text-align: center;}.songContainer,.singerContainer,.searchContainer {display: block;margin: 4px 12%% 0 auto;width: 80%%;}.songInput {margin: 0;width: 100%%;}.artistInput {margin: 0;width: 100%%;}.searchBtn {margin: 0;width: 106%%;height: 40px;}}")
fmt.Fprintf(w, ".songInput:hover,.artistInput:hover,.songInput:focus,.artistInput:focus {outline: none;border: 2px solid deeppink;}.song-item:hover,.searchBtn:hover {box-shadow: 0 4px 8px rgba(255, 182, 193, 0.7);transform: translateY(-5px);}")
fmt.Fprintf(w, ".getError,.no-enter,.no-result {width: 80%%;margin: 4px auto;padding: 20px;background-color: rgba(255, 0, 38, 0.4);text-align: center;border: 1px solid rgb(255, 75, 75);border-radius: 15px;color: rgb(205, 0, 0);}")
fmt.Fprintf(w, ".loading {width: 80%%;margin: 4px auto;padding: 20px;text-align: center;color: deeppink;font-size: 45px;animation: spin 2s linear infinite;}@keyframes spin {from {transform: rotate(0deg);}to {transform: rotate(360deg);}}")
fmt.Fprintf(w, ".result {width: 85%%;margin: 4px auto;}.result-title {font-size: 24px;font-weight: bold;}.song-item {background-color: rgba(255, 255, 255, 0.4);border: 2px solid deeppink;border-radius: 15px;transition: all 0.3s ease;padding: 10px;}")
fmt.Fprintf(w, ".song-title-container {display: flex;align-items: center;}.song-name {font-size: 18px;font-weight: bold;}.cache {width: 45px;background-color: deepskyblue;color: #000;font-size: 14px;text-align: center;border-radius: 15px;}")
fmt.Fprintf(w, ".singer-name-icon,.lyric-icon {font-size: 18px;color: deeppink;}.singer-name,.lyric {font-size: 16px;color: #4f596b;}.downloadBtn,.playBtn,.pauseBtn {border: none;background-image: linear-gradient(to right, skyblue, deepskyblue);border-radius: 5px;padding: 5px 10px;font-size: 15px;transition: all 0.3s ease;}.downloadBtn:hover,.playBtn:hover,.pauseBtn:hover {box-shadow: 0 4px 8px rgba(182, 232, 255, 0.7);transform: translateY(-5px);}")
fmt.Fprintf(w, ".audio-player-container {display: flex;align-items: center;}.audio {display: none;}.progress-bar {width: 70%%;margin: 4px auto;padding: 8px;background-color: rgba(255, 255, 255, 0.4);border: 1px solid deeppink;border-radius: 5px;display: flex;justify-content: space-between;align-items: center;}.progress {width: 0;height: 10px;background-color: deeppink;}.time {margin-left: auto;}")
fmt.Fprintf(w, ".stream_pcm {width: 80%%;margin: 4px auto;padding: 20px;background-color: rgba(135, 206, 235, 0.4);border: 1px solid skyblue;border-radius: 15px;}.stream_pcm_title {color: rgb(0, 100, 100);font-size: 16px;font-weight: bold;}.stream_pcm_content {margin-top: 10px;font-size: 14px;color: #555;}")
fmt.Fprintf(w, ".stream_pcm_type_title,.stream_pcm_content_num_title,.stream_pcm_content_time_title,.stream_pcm_response_title {font-weight: bold;}.stream_pcm_response_value {width: 100%%;background-color: rgba(255, 255, 255, 0.4);display: block;white-space: pre-wrap;overflow: auto;height: 200px;border-radius: 6px;padding: 10px;}")
fmt.Fprintf(w, ".info {width: 80%%;margin: 4px auto;padding: 20px;text-align: center;color: #4f596b;}.info strong {font-weight: bolder;color: #000;}.showStreamPcmBtnContainer,.hideStreamPcmBtnContainer {margin: 0 auto;width: 80%%;display: flex;justify-content: center;}")
fmt.Fprintf(w, ".showStreamPcmBtn {border: 1px solid deepskyblue;color: deepskyblue;}.showStreamPcmBtn:hover {background-color: deepskyblue;color: #000;}.hideStreamPcmBtn {border: 1px solid deeppink;color: deeppink;}.hideStreamPcmBtn:hover {background-color: deeppink;color: #000;}.showStreamPcmBtn,.hideStreamPcmBtn {background: none;padding: 2px 6px;}")
fmt.Fprintf(w, ".footer {text-align: center;margin: 10px auto;justify-content: center;align-items: center;width: 80%%;border-top: 1px solid #ccc;}.language-select {background-color: rgba(255, 255, 255, 0.4);border: 1px solid #ccc;text-align: center;width: 120px;height: 40px;border-radius: 10px;margin: 10px auto;}")
fmt.Fprintf(w, ".language-select:focus,.language-select:hover {outline: none;border: 1px solid deeppink;}.copyright {font-size: 14px;color: #4f596b;}")
fmt.Fprintf(w, "</style></head>")
// Build body
fmt.Fprintf(w, "<body><div class=\"container\"><div id=\"title\" class=\"title\"></div><div id=\"description\" class=\"description\"></div>")
fmt.Fprintf(w, "<div class=\"search-form\"><div class=\"songContainer\"><div class=\"song\"><input type=\"text\" id=\"songInput\" class=\"songInput\" autocomplete=\"off\"></div></div>")
fmt.Fprintf(w, "<div class=\"singerContainer\"><div class=\"singer\"><input type=\"text\" id=\"artistInput\" class=\"artistInput\" autocomplete=\"off\"></div></div><div class=\"searchContainer\"><div class=\"search\"><button type=\"button\" id=\"searchBtn\" class=\"searchBtn\"></button></div></div></div>")
fmt.Fprintf(w, "<div class=\"getError\" id=\"getError\"></div><div class=\"no-enter\" id=\"noEnter\"></div><div class=\"no-result\" id=\"noResult\"></div><div class=\"loading\" id=\"loading\"><i class=\"fa fa-circle-o-notch\"></i></div>")
fmt.Fprintf(w, "<div class=\"result\" id=\"result\"><div class=\"result-title\" id=\"resultTitle\"></div><div class=\"result-list\"><div class=\"song-item\"><div class=\"song-title-container\"><div class=\"song-name\" id=\"songName\"></div><div class=\"cache\" id=\"cache\"></div></div><div class=\"singer-name\"><span class=\"singer-name-icon\" id=\"singerNameIcon\"><i class=\"fa fa-user-o\"></i></span><span class=\"singer-name-value\" id=\"singerName\"></span></div><div class=\"lyric\"><span class=\"lyric-icon\" id=\"lyricIcon\"><i class=\"fa fa-file-text-o\"></i></span><span class=\"lyric-value\" id=\"noLyric\"></span><span class=\"lyric-value\" id=\"lyric\"></span></div><button type=\"button\" class=\"downloadBtn\" id=\"downloadBtn\"></button><div class=\"audio-player-container\"><button type=\"button\" class=\"playBtn\" id=\"playBtn\"></button><button type=\"button\" class=\"pauseBtn\" id=\"pauseBtn\"></button><audio class=\"audio\" id=\"audio\"></audio><div class=\"progress-bar\"><div class=\"progress\" id=\"progress\"></div><div class=\"time\" id=\"time\"></div></div></div></div></div></div>")
fmt.Fprintf(w, "<div class=\"stream_pcm\" id=\"streamPcm\"><div class=\"stream_pcm_title\" id=\"streamPcmTitle\"></div><div class=\"stream_pcm_content\"><div class=\"stream_pcm_type\"><span class=\"stream_pcm_type_title\" id=\"streamPcmTypeTitle\"></span><span class=\"stream_pcm_type_value\" id=\"streamPcmTypeValue\"></span></div><div class=\"stream_pcm_content_num\"><span class=\"stream_pcm_content_num_title\" id=\"streamPcmContentNumTitle\"></span><span class=\"stream_pcm_content_num_value\">1</span></div><div class=\"stream_pcm_content_time\"><span class=\"stream_pcm_content_time_title\" id=\"streamPcmContentTimeTitle\"></span><span class=\"stream_pcm_content_time_value\" id=\"streamPcmContentTimeValue\"></span></div><div class=\"stream_pcm_response\"><span class=\"stream_pcm_response_title\" id=\"streamPcmResponseTitle\"></span><br><span class=\"stream_pcm_response_value\" id=\"streamPcmResponseValue\"></span></div></div></div>")
fmt.Fprintf(w, "<div class=\"info\" id=\"info\"></div><div class=\"showStreamPcmBtnContainer\" id=\"showStreamPcmBtnContainer\"><button type=\"button\" id=\"showStreamPcmBtn\" class=\"showStreamPcmBtn\"></button></div><div class=\"hideStreamPcmBtnContainer\" id=\"hideStreamPcmBtnContainer\"><button type=\"button\" id=\"hideStreamPcmBtn\" class=\"hideStreamPcmBtn\"></button></div><div class=\"footer\"><select id=\"languageSelect\" class=\"language-select\"><option value=\"zh-CN\">简体中文</option><option value=\"en\">English</option></select><div class=\"copyright\" id=\"copyright\"></div></div></div>")
fmt.Fprintf(w, "<script>")
// Set copyright year and read head meta tags
fmt.Fprintf(w, "const currentYear = new Date().getFullYear();var head = document.getElementsByTagName('head')[0];")
// language definition
fmt.Fprintf(w, "const titles = {'zh-CN': '%s','en': '%s'};", websiteNameCN, websiteNameEN)
fmt.Fprintf(w, "const titles2 = {'zh-CN': '%s','en': '%s'};", websiteTitleCN, websiteTitleEN)
fmt.Fprintf(w, "const descriptions = {'zh-CN': '%s','en': '%s'};", websiteDescCN, websiteDescEN)
fmt.Fprintf(w, "const keywords = {'zh-CN': '%s','en': '%s'};", websiteKeywordsCN, websiteKeywordsEN)
fmt.Fprintf(w, "const songInputs = {'zh-CN': '输入歌曲名称...','en': 'Enter song name...'};")
fmt.Fprintf(w, "const singerInputs = {'zh-CN': '歌手名称(可选)','en': 'Singer name(optional)'};")
fmt.Fprintf(w, "const searchBtns = {'zh-CN': '<i class=\"fa fa-search\"></i> 搜索','en': '<i class=\"fa fa-search\"></i> Search'};")
fmt.Fprintf(w, "const getErrors = {'zh-CN': '获取数据失败<br>可能是因为网络响应出错或其它原因<br>请检查您的网络并稍后再试','en': 'Failed to get data<br>It may be because of network response error or other reasons<br>Please check your network and try again later'};")
fmt.Fprintf(w, "const noEnters = {'zh-CN': '请输入歌曲名称','en': 'Please enter song name'};")
fmt.Fprintf(w, "const noResults = {'zh-CN': '没有找到相关歌曲','en': 'No related songs found'};")
fmt.Fprintf(w, "const resultTitles = {'zh-CN': '<i class=\"fa fa-list-ul\"></i> 搜索结果','en': '<i class=\"fa fa-list-ul\"></i> Search Result'};")
fmt.Fprintf(w, "const caches = {'zh-CN': '缓存','en': 'Cache'};")
fmt.Fprintf(w, "const noLyrics = {'zh-CN': '暂无歌词','en': 'No lyrics'};")
fmt.Fprintf(w, "const playBtns = {'zh-CN': '<i class=\"fa fa-play-circle-o\"></i> 播放','en': '<i class=\"fa fa-play-circle-o\"></i> Play'};")
fmt.Fprintf(w, "const pauseBtns = {'zh-CN': '<i class=\"fa fa-pause-circle-o\"></i> 暂停','en': '<i class=\"fa fa-pause-circle-o\"></i> Pause'};")
fmt.Fprintf(w, "const downloadBtns = {'zh-CN': '<i class=\"fa fa-download\"></i> 下载','en': '<i class=\"fa fa-download\"></i> Download'};")
fmt.Fprintf(w, "const streamPcmTitle = {'zh-CN': '<i class=\"fa fa-info-circle\"></i> stream_pcm 响应讯息:','en': '<i class=\"fa fa-info-circle\"></i> stream_pcm response: '};")
fmt.Fprintf(w, "const streamPcmTypeTitle = {'zh-CN': '响应类型:','en': 'Response type: '};")
fmt.Fprintf(w, "const streamPcmTypeValue = {'zh-CN': '单曲播放讯息','en': 'Single song playback message'};")
fmt.Fprintf(w, "const streamPcmContentNumTitle = {'zh-CN': '响应数量:','en': 'Response number: '};")
fmt.Fprintf(w, "const streamPcmContentTimeTitle = {'zh-CN': '响应时间:','en': 'Response time: '};")
fmt.Fprintf(w, "const streamPcmResponseTitle = {'zh-CN': '完整响应:','en': 'Full response: '};")
fmt.Fprintf(w, "const info = {'zh-CN': '<strong><i class=\"fa fa-info-circle\"></i> 系统讯息</strong><br>嵌入式音乐搜索服务器 | Ver %s<br>支持云端/本地音乐搜索,支持多种音乐格式播放,支持多种语言<br>基于聚合API支持本地音乐缓存','en': '<strong><i class=\"fa fa-info-circle\"></i> System Information</strong><br>Embedded Music Search Server | Ver %s<br>Support cloud/local music search, support various music formats, support various languages<br>Based on aggregation API, support local music cache'};", websiteVersion, websiteVersion)
fmt.Fprintf(w, "const showStreamPcmBtns = {'zh-CN': '<i class=\"fa fa-eye\"></i> 显示 stream_pcm 响应','en': '<i class=\"fa fa-eye\"></i> Show stream_pcm response'};")
fmt.Fprintf(w, "const hideStreamPcmBtns = {'zh-CN': '<i class=\"fa fa-eye-slash\"></i> 隐藏 stream_pcm 响应','en': '<i class=\"fa fa-eye-slash\"></i> Hide stream_pcm response'};")
// Get browser language, set HTML lang attribute and Set default language
fmt.Fprintf(w, "const browserLang = navigator.language || 'en';document.documentElement.lang = browserLang || \"en\";document.getElementById('languageSelect').value = browserLang;")
// Initialize title
fmt.Fprintf(w, "document.title = (titles[browserLang] || '%s') + \" - \" + (titles2[browserLang] || '%s');", websiteNameEN, websiteTitleEN)
// Initialize meta description
fmt.Fprintf(w, "var existingMetaDescription = document.querySelector('meta[name=\"description\"]');if (existingMetaDescription) {existingMetaDescription.content = descriptions[browserLang] || '%s';} else {var metaDescription = document.createElement('meta');metaDescription.name = 'description';metaDescription.content = descriptions[browserLang] || '%s';head.appendChild(metaDescription);};", websiteDescEN, websiteDescEN)
// Initialize meta keywords
fmt.Fprintf(w, "var existingMetaKeywords = document.querySelector('meta[name=\"keywords\"]');if (existingMetaKeywords) {existingMetaKeywords.content = keywords[browserLang] || '%s';} else {var metaKeywords = document.createElement('meta');metaKeywords.name = 'keywords';metaKeywords.content = keywords[browserLang] || '%s';head.appendChild(metaKeywords);};", websiteKeywordsEN, websiteKeywordsEN)
// Set default language content
fmt.Fprintf(w, "document.getElementById('title').innerHTML = titles[browserLang] || '%s';", websiteNameEN)
fmt.Fprintf(w, "document.getElementById('copyright').innerHTML = \"&copy;\" + currentYear + \" \" + (titles[browserLang] || '%s');", websiteNameEN)
fmt.Fprintf(w, "document.getElementById('description').innerHTML = descriptions[browserLang] || '%s';", websiteDescEN)
fmt.Fprintf(w, "document.getElementById('songInput').placeholder = songInputs[browserLang] || 'Enter song name...';")
fmt.Fprintf(w, "document.getElementById('artistInput').placeholder = singerInputs[browserLang] || 'Singer name(optional)';")
fmt.Fprintf(w, "document.getElementById('searchBtn').innerHTML = searchBtns[browserLang] || '<i class=\"fa fa-search\"></i> Search';")
fmt.Fprintf(w, "document.getElementById('getError').innerHTML = getErrors[browserLang] || 'Failed to get data<br>It may be because of network response error or other reasons<br>Please check your network and try again later';")
fmt.Fprintf(w, "document.getElementById('noEnter').innerHTML = noEnters[browserLang] || 'Please enter song name';")
fmt.Fprintf(w, "document.getElementById('noResult').innerHTML = noResults[browserLang] || 'No related songs found';")
fmt.Fprintf(w, "document.getElementById('resultTitle').innerHTML = resultTitles[browserLang] || '<i class=\"fa fa-list-ul\"></i> Search Result';")
fmt.Fprintf(w, "document.getElementById('cache').innerHTML = caches[browserLang] || 'Cache';")
fmt.Fprintf(w, "document.getElementById('noLyric').innerHTML = noLyrics[browserLang] || 'No lyrics';")
fmt.Fprintf(w, "document.getElementById('downloadBtn').innerHTML = downloadBtns[browserLang] || '<i class=\"fa fa-download\"></i> Download';")
fmt.Fprintf(w, "document.getElementById('playBtn').innerHTML = playBtns[browserLang] || '<i class=\"fa fa-play-circle-o\"></i> Play';")
fmt.Fprintf(w, "document.getElementById('pauseBtn').innerHTML = pauseBtns[browserLang] || '<i class=\"fa fa-pause-circle-o\"></i> Pause';")
fmt.Fprintf(w, "document.getElementById('streamPcmTitle').innerHTML = streamPcmTitle[browserLang] || '<i class=\"fa fa-info-circle\"></i> stream_pcm response: ';")
fmt.Fprintf(w, "document.getElementById('streamPcmTypeTitle').innerHTML = streamPcmTypeTitle[browserLang] || 'Response type: ';")
fmt.Fprintf(w, "document.getElementById('streamPcmTypeValue').innerHTML = streamPcmTypeValue[browserLang] || 'Single song playback message';")
fmt.Fprintf(w, "document.getElementById('streamPcmContentNumTitle').innerHTML = streamPcmContentNumTitle[browserLang] || 'Response number: ';")
fmt.Fprintf(w, "document.getElementById('streamPcmContentTimeTitle').innerHTML = streamPcmContentTimeTitle[browserLang] || 'Response time: ';")
fmt.Fprintf(w, "document.getElementById('streamPcmResponseTitle').innerHTML = streamPcmResponseTitle[browserLang] || 'Full response: ';")
fmt.Fprintf(w, "document.getElementById('info').innerHTML = info[browserLang] || '<strong><i class=\"fa fa-info-circle\"></i> System Information</strong><br>Embedded Music Search Server | Ver %s<br>Support cloud/local music search, support various music formats, support various languages<br>Based on aggregation API, support local music cache';", websiteVersion)
fmt.Fprintf(w, "document.getElementById('showStreamPcmBtn').innerHTML = showStreamPcmBtns[browserLang] || '<i class=\"fa fa-eye\"></i> Show stream_pcm response';")
fmt.Fprintf(w, "document.getElementById('hideStreamPcmBtn').innerHTML = hideStreamPcmBtns[browserLang] || '<i class=\"fa fa-eye-slash\"></i> Hide stream_pcm response';")
// Listen language selection change and update title
fmt.Fprintf(w, "document.getElementById('languageSelect').addEventListener('change', function () {")
fmt.Fprintf(w, "const selectedLang = this.value;")
// Set HTML lang attribute
fmt.Fprintf(w, "document.documentElement.lang = selectedLang || \"en\";")
// Set title
fmt.Fprintf(w, "document.title = (titles[selectedLang] || '%s') + \" - \" + (titles2[selectedLang] || '%s');", websiteNameEN, websiteTitleEN)
// Initialize meta description
fmt.Fprintf(w, "var existingMetaDescription = document.querySelector('meta[name=\"description\"]');if (existingMetaDescription) {existingMetaDescription.content = descriptions[selectedLang] || '%s';} else {var metaDescription = document.createElement('meta');metaDescription.name = 'description';metaDescription.content = descriptions[selectedLang] || '%s';head.appendChild(metaDescription);};", websiteDescEN, websiteDescEN)
// Initialize meta keywords
fmt.Fprintf(w, "var existingMetaKeywords = document.querySelector('meta[name=\"keywords\"]');if (existingMetaKeywords) {existingMetaKeywords.content = keywords[selectedLang] || '%s';} else {var metaKeywords = document.createElement('meta');metaKeywords.name = 'keywords';metaKeywords.content = keywords[selectedLang] || '%s';head.appendChild(metaKeywords);};", websiteKeywordsEN, websiteKeywordsEN)
// Set default language content
fmt.Fprintf(w, "document.getElementById('title').innerHTML = titles[selectedLang] || '%s';", websiteNameEN)
fmt.Fprintf(w, "document.getElementById('copyright').innerHTML = \"&copy;\" + currentYear + \" \" + (titles[selectedLang] || '%s');", websiteNameEN)
fmt.Fprintf(w, "document.getElementById('description').innerHTML = descriptions[selectedLang] || '%s';", websiteDescEN)
fmt.Fprintf(w, "document.getElementById('songInput').placeholder = songInputs[selectedLang] || 'Enter song name...';")
fmt.Fprintf(w, "document.getElementById('artistInput').placeholder = singerInputs[selectedLang] || 'Singer name(optional)';")
fmt.Fprintf(w, "document.getElementById('searchBtn').innerHTML = searchBtns[selectedLang] || '<i class=\"fa fa-search\"></i> Search';")
fmt.Fprintf(w, "document.getElementById('getError').innerHTML = getErrors[selectedLang] || 'Failed to get data<br>It may be because of network response error or other reasons<br>Please check your network and try again later';")
fmt.Fprintf(w, "document.getElementById('noEnter').innerHTML = noEnters[selectedLang] || 'Please enter song name';")
fmt.Fprintf(w, "document.getElementById('noResult').innerHTML = noResults[selectedLang] || 'No related songs found';")
fmt.Fprintf(w, "document.getElementById('resultTitle').innerHTML = resultTitles[selectedLang] || '<i class=\"fa fa-list-ul\"></i> Search Result';")
fmt.Fprintf(w, "document.getElementById('cache').innerHTML = caches[selectedLang] || 'Cache';")
fmt.Fprintf(w, "document.getElementById('noLyric').innerHTML = noLyrics[selectedLang] || 'No lyrics';")
fmt.Fprintf(w, "document.getElementById('downloadBtn').innerHTML = downloadBtns[selectedLang] || '<i class=\"fa fa-download\"></i> Download';")
fmt.Fprintf(w, "document.getElementById('playBtn').innerHTML = playBtns[selectedLang] || '<i class=\"fa fa-play-circle-o\"></i> Play';")
fmt.Fprintf(w, "document.getElementById('pauseBtn').innerHTML = pauseBtns[selectedLang] || '<i class=\"fa fa-pause-circle-o\"></i> Pause';")
fmt.Fprintf(w, "document.getElementById('streamPcmTitle').innerHTML = streamPcmTitle[selectedLang] || '<i class=\"fa fa-info-circle\"></i> stream_pcm response: ';")
fmt.Fprintf(w, "document.getElementById('streamPcmTypeTitle').innerHTML = streamPcmTypeTitle[selectedLang] || 'Response type: ';")
fmt.Fprintf(w, "document.getElementById('streamPcmTypeValue').innerHTML = streamPcmTypeValue[selectedLang] || 'Single song playback message';")
fmt.Fprintf(w, "document.getElementById('streamPcmContentNumTitle').innerHTML = streamPcmContentNumTitle[selectedLang] || 'Response number: ';")
fmt.Fprintf(w, "document.getElementById('streamPcmContentTimeTitle').innerHTML = streamPcmContentTimeTitle[selectedLang] || 'Response time: ';")
fmt.Fprintf(w, "document.getElementById('streamPcmResponseTitle').innerHTML = streamPcmResponseTitle[selectedLang] || 'Full response: ';")
fmt.Fprintf(w, "document.getElementById('info').innerHTML = info[selectedLang] || '<strong><i class=\"fa fa-info-circle\"></i> System Information</strong><br>Embedded Music Search Server | Ver %s<br>Support cloud/local music search, support various music formats, support various languages<br>Based on aggregation API, support local music cache';", websiteVersion)
fmt.Fprintf(w, "document.getElementById('showStreamPcmBtn').innerHTML = showStreamPcmBtns[selectedLang] || '<i class=\"fa fa-eye\"></i> Show stream_pcm response';")
fmt.Fprintf(w, "document.getElementById('hideStreamPcmBtn').innerHTML = hideStreamPcmBtns[selectedLang] || '<i class=\"fa fa-eye-slash\"></i> Hide stream_pcm response';")
fmt.Fprintf(w, "});")
// Getting Elements
fmt.Fprintf(w, "const songInput = document.getElementById('songInput');")
fmt.Fprintf(w, "const artistInput = document.getElementById('artistInput');")
fmt.Fprintf(w, "const searchBtn = document.getElementById('searchBtn');")
fmt.Fprintf(w, "const getError = document.getElementById('getError');")
fmt.Fprintf(w, "const noEnter = document.getElementById('noEnter');")
fmt.Fprintf(w, "const noResult = document.getElementById('noResult');")
fmt.Fprintf(w, "const loading = document.getElementById('loading');")
fmt.Fprintf(w, "const result = document.getElementById('result');")
fmt.Fprintf(w, "const songName = document.getElementById('songName');")
fmt.Fprintf(w, "const cache = document.getElementById('cache');")
fmt.Fprintf(w, "const singerName = document.getElementById('singerName');")
fmt.Fprintf(w, "const noLyric = document.getElementById('noLyric');")
fmt.Fprintf(w, "const downloadBtn = document.getElementById('downloadBtn');")
fmt.Fprintf(w, "const playBtn = document.getElementById('playBtn');")
fmt.Fprintf(w, "const pauseBtn = document.getElementById('pauseBtn');")
fmt.Fprintf(w, "const lyric = document.getElementById('lyric');")
fmt.Fprintf(w, "const streamPcm = document.getElementById('streamPcm');")
fmt.Fprintf(w, "const streamPcmContentTimeValue = document.getElementById('streamPcmContentTimeValue');")
fmt.Fprintf(w, "const streamPcmResponseValue = document.getElementById('streamPcmResponseValue');")
fmt.Fprintf(w, "const showStreamPcmBtn = document.getElementById('showStreamPcmBtn');")
fmt.Fprintf(w, "const hideStreamPcmBtn = document.getElementById('hideStreamPcmBtn');")
// Hide content that should not be displayed before searching
fmt.Fprintf(w, "getError.style.display = 'none';")
fmt.Fprintf(w, "noEnter.style.display = 'none';")
fmt.Fprintf(w, "noResult.style.display = 'none';")
fmt.Fprintf(w, "loading.style.display = 'none';")
fmt.Fprintf(w, "result.style.display = 'none';")
fmt.Fprintf(w, "cache.style.display = 'none';")
fmt.Fprintf(w, "noLyric.style.display = 'none';")
fmt.Fprintf(w, "downloadBtn.style.display = 'none';")
fmt.Fprintf(w, "playBtn.style.display = 'none';")
fmt.Fprintf(w, "pauseBtn.style.display = 'none';")
fmt.Fprintf(w, "streamPcm.style.display = 'none';")
fmt.Fprintf(w, "showStreamPcmBtn.style.display = 'none';")
fmt.Fprintf(w, "hideStreamPcmBtn.style.display = 'none';")
// Empty song name processing
fmt.Fprintf(w, "searchBtn.addEventListener('click', function () {if (songInput.value.trim() === '') {noEnter.style.display = 'block';} else {noEnter.style.display = 'none';search();}});")
fmt.Fprintf(w, "songInput.addEventListener('keydown', function (event) {if (event.key === 'Enter') {if (songInput.value.trim() === '') {noEnter.style.display = 'block';} else {noEnter.style.display = 'none';search();}}});")
fmt.Fprintf(w, "artistInput.addEventListener('keydown', function (event) {if (event.key === 'Enter') {if (songInput.value.trim() === '') {noEnter.style.display = 'block';} else {noEnter.style.display = 'none';search();}}});")
// Searching for songs
fmt.Fprintf(w, "function search() {")
// Show loading
fmt.Fprintf(w, "loading.style.display = 'block';")
// Hide error
fmt.Fprintf(w, "getError.style.display = 'none';")
// Build request URL, urlencode song name and artist name
fmt.Fprintf(w, "const song = encodeURIComponent(songInput.value);")
fmt.Fprintf(w, "const artist = encodeURIComponent(artistInput.value);")
fmt.Fprintf(w, "const requestUrl = `/stream_pcm?song=${song}&artist=${artist}`;")
// Send request to server
fmt.Fprintf(w, "fetch(requestUrl)")
fmt.Fprintf(w, ".then(response => {if (!response.ok) {getError.style.display = 'block';throw new Error('Network response was not ok');}return response.json();})")
fmt.Fprintf(w, ".then(data => {")
// Fill in all the obtained content into streamPcmResponseValue
fmt.Fprintf(w, "streamPcmResponseValue.innerHTML = JSON.stringify(data, null, 2);")
// Get the current time and fill in streamPcmCntentTimeValue
fmt.Fprintf(w, "streamPcmContentTimeValue.innerHTML = new Date().toISOString();")
// Display result
fmt.Fprintf(w, "result.style.display = 'block';")
// Display download button
fmt.Fprintf(w, "downloadBtn.style.display = 'block';")
// Display Play button
fmt.Fprintf(w, "playBtn.style.display = 'block';")
// Display showStreamPcmBtn
fmt.Fprintf(w, "showStreamPcmBtn.style.display = 'block';")
// Hide hideStreamPcmBtn
fmt.Fprintf(w, "hideStreamPcmBtn.style.display = 'none';")
// Fill the title into the songName field
fmt.Fprintf(w, "if (data.title === \"\") {noResult.style.display = 'block';result.style.display = 'none';} else {noResult.style.display = 'none';songName.textContent = data.title;};")
// Fill the artist into the singerName field
fmt.Fprintf(w, "singerName.textContent = data.artist;")
// Set parsed lyrics to an empty array
fmt.Fprintf(w, "let parsedLyrics = [];")
// Check if the link 'lyric_url' is empty
fmt.Fprintf(w, "if (data.lyric_url) {")
// Visit lyric_url
fmt.Fprintf(w, "fetch(data.lyric_url)")
fmt.Fprintf(w, ".then(response => {if (!response.ok) {throw new Error('Lyrics request error');}return response.text();})")
fmt.Fprintf(w, ".then(lyricText => {")
// Show lyric
fmt.Fprintf(w, "lyric.style.display = 'block';")
// Parse lyrics
fmt.Fprintf(w, "parsedLyrics = parseLyrics(lyricText);")
fmt.Fprintf(w, "})")
fmt.Fprintf(w, ".catch(error => {")
// Show noLyric
fmt.Fprintf(w, "noLyric.style.display = 'block';")
// Hide lyric
fmt.Fprintf(w, "lyric.style.display = 'none';")
fmt.Fprintf(w, "});")
fmt.Fprintf(w, "} else {")
// If lyric_url is empty, display noLyric
fmt.Fprintf(w, "noLyric.style.display = 'block';")
// Hide lyric
fmt.Fprintf(w, "lyric.style.display = 'none';")
fmt.Fprintf(w, "};")
// Check cache
fmt.Fprintf(w, "if (data.from_cache === true) {")
// If the song is obtained from cache, display cache
fmt.Fprintf(w, "cache.style.display = 'block';")
fmt.Fprintf(w, "} else {")
// If the song is not obtained from cache, hide cache
fmt.Fprintf(w, "cache.style.display = 'none';")
fmt.Fprintf(w, "};")
// Create audio player
fmt.Fprintf(w, "const audioPlayer = document.getElementById('audio');")
fmt.Fprintf(w, "const customProgress = document.getElementById('progress');")
fmt.Fprintf(w, "const timeDisplay = document.getElementById('time');")
// Set audio source
fmt.Fprintf(w, "audioPlayer.src = data.audio_full_url;")
fmt.Fprintf(w, "audio.addEventListener('timeupdate', function () {")
fmt.Fprintf(w, "const currentTime = formatTime(audioPlayer.currentTime);")
fmt.Fprintf(w, "const duration = formatTime(audioPlayer.duration);")
fmt.Fprintf(w, "timeDisplay.textContent = `${currentTime}/${duration}`;")
fmt.Fprintf(w, "const progress = (audioPlayer.currentTime / audioPlayer.duration) * 100;")
fmt.Fprintf(w, "customProgress.style.width = progress + '%%';")
// Find current lyric
fmt.Fprintf(w, "let currentLyric = parsedLyrics.find((lyric, index, arr) =>")
fmt.Fprintf(w, "lyric.timestamp > audioPlayer.currentTime - 3 && (index === 0 || arr[index - 1].timestamp <= audioPlayer.currentTime));")
fmt.Fprintf(w, "lyric.textContent = currentLyric ? currentLyric.lyricLine : '';")
fmt.Fprintf(w, "});")
// Save current time before playing audio
fmt.Fprintf(w, "let savedCurrentTime = 0;")
// DownloadBtn click event
fmt.Fprintf(w, "downloadBtn.addEventListener('click', async function () {")
// try
fmt.Fprintf(w, "try {")
// Get audio_full_url
fmt.Fprintf(w, "const audioDownloadUrl = data.audio_full_url;")
// Get audio file name
fmt.Fprintf(w, "const audioDownloadFileName = data.title + ' - ' + data.artist + '.flac';")
// Fetch audio file
fmt.Fprintf(w, "const downloadResponse = await fetch(audioDownloadUrl);")
fmt.Fprintf(w, "if (!downloadResponse.ok) {throw new Error('Failed to fetch audio file');}")
fmt.Fprintf(w, "const audioBlob = await downloadResponse.blob();")
// Create download link
fmt.Fprintf(w, "const downloadUrl = URL.createObjectURL(audioBlob);")
// Go to download link
fmt.Fprintf(w, "const a = document.createElement('a');a.href = downloadUrl;a.download = audioDownloadFileName;a.style.display = 'none';document.body.appendChild(a);a.click();")
// Clean up download link
fmt.Fprintf(w, "setTimeout(() => {document.body.removeChild(a);URL.revokeObjectURL(downloadUrl);}, 0);")
// catch error
fmt.Fprintf(w, "} catch (error) {")
// Show error message
fmt.Fprintf(w, "console.error('Download failed:', error);alert('Failed to download audio file: ', error);")
fmt.Fprintf(w, "}});")
// PlayBtn click event
fmt.Fprintf(w, "playBtn.addEventListener('click', function () {")
// Save current time
fmt.Fprintf(w, "audioPlayer.currentTime = savedCurrentTime;")
// Play audio
fmt.Fprintf(w, "audioPlayer.play();")
// Hide playBtn
fmt.Fprintf(w, "playBtn.style.display = 'none';")
// Show pauseBtn
fmt.Fprintf(w, "pauseBtn.style.display = 'block';")
fmt.Fprintf(w, "});")
// PauseBtn click event
fmt.Fprintf(w, "pauseBtn.addEventListener('click', function () {")
// Save current time
fmt.Fprintf(w, "savedCurrentTime = audioPlayer.currentTime;")
// Pause audio
fmt.Fprintf(w, "audioPlayer.pause();")
// Hide pauseBtn
fmt.Fprintf(w, "pauseBtn.style.display = 'none';")
// Show playBtn
fmt.Fprintf(w, "playBtn.style.display = 'block';")
fmt.Fprintf(w, "});")
fmt.Fprintf(w, "})")
fmt.Fprintf(w, ".catch(error => {")
fmt.Fprintf(w, "console.error('Error requesting song information:', error);")
// When there is an error in the request, you can also consider displaying a prompt message or other content
fmt.Fprintf(w, "getError.style.display = 'block';")
fmt.Fprintf(w, "})")
fmt.Fprintf(w, ".finally(() => {")
// Regardless of the request result, the loading display should be turned off in the end
fmt.Fprintf(w, "loading.style.display = 'none';")
fmt.Fprintf(w, "});};")
// Format time
fmt.Fprintf(w, "function formatTime(seconds) {const minutes = Math.floor(seconds / 60);const secondsRemainder = Math.floor(seconds %% 60);return minutes.toString().padStart(2, '0') + ':' +secondsRemainder.toString().padStart(2, '0');};")
// Function to parse lyrics
fmt.Fprintf(w, "function parseLyrics(lyricText) {const lines = lyricText.split('\\n');const lyrics = [];for (let line of lines) {const match = line.match(/\\[(\\d{2}:\\d{2})(?:\\.\\d{2})?\\](.*)/);if (match) {const timestamp = match[1]; const lyricLine = match[2].trim();const [minutes, seconds] = timestamp.split(':');const timeInSeconds = (parseInt(minutes) * 60) + parseInt(seconds);lyrics.push({ timestamp: timeInSeconds, lyricLine });}}return lyrics;};")
// Show stream_pcm response
fmt.Fprintf(w, "showStreamPcmBtn.addEventListener('click', function () {streamPcm.style.display = 'block';showStreamPcmBtn.style.display = 'none';hideStreamPcmBtn.style.display = 'block';});")
// Hide stream_pcm response
fmt.Fprintf(w, "hideStreamPcmBtn.addEventListener('click', function () {streamPcm.style.display = 'none';showStreamPcmBtn.style.display = 'block';hideStreamPcmBtn.style.display = 'none';});")
fmt.Fprintf(w, "</script></body></html>")
fmt.Printf("[Web Access] Return default index pages\n")
}

84
main.go
View File

@@ -1,84 +0,0 @@
package main
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/joho/godotenv"
)
const (
TAG = "MeowEmbeddedMusicServer"
)
func main() {
err := godotenv.Load()
if err != nil {
fmt.Printf("[Warning] %s Loading .env file failed: %v\nUse the default configuration instead.\n", TAG, err)
}
port := os.Getenv("PORT")
if port == "" {
fmt.Printf("[Warning] %s PORT environment variable not set\nUse the default port 2233 instead.\n", TAG)
port = "2233"
}
http.HandleFunc("/", indexHandler)
http.HandleFunc("/stream_pcm", apiHandler)
fmt.Printf("[Info] %s Started.\n喵波音律-音乐家园QQ交流群:865754861\n", TAG)
fmt.Printf("[Info] Starting music server at port %s\n", port)
// Create a channel to listen for signals
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
// Create a server instance
srv := &http.Server{
Addr: ":" + port,
ReadTimeout: 30 * time.Second,
WriteTimeout: 0, // Disable the timeout for the response writer
IdleTimeout: 30 * time.Minute, // Set the maximum duration for idle connections
ReadHeaderTimeout: 10 * time.Second, // Limit the maximum duration for reading the headers of the request
MaxHeaderBytes: 1 << 16, // Limit the maximum request header size to 64KB
}
// Start the server
go func() {
if err := srv.ListenAndServe(); err != nil {
fmt.Println(err)
sigChan <- syscall.SIGINT // Send a signal to shut down the server
}
}()
// Create a channel to listen for standard input
exitChan := make(chan struct{})
go func() {
for {
var input string
fmt.Scanln(&input)
if input == "exit" {
exitChan <- struct{}{}
return
}
}
}()
// Monitor signals or exit signals from standard inputs
select {
case <-sigChan:
fmt.Printf("[Info] Server is shutting down.\nGoodbye!\n")
case <-exitChan:
fmt.Printf("[Info] Server is shutting down.\nGoodbye!\n")
}
// Shut down the server
if err := srv.Shutdown(context.Background()); err != nil {
fmt.Println(err)
}
}

View File

@@ -1,22 +0,0 @@
[
{
"title": "",
"artist": "",
"audio_url": "",
"audio_full_url": "",
"m3u8_url": "",
"lyric_url": "",
"cover_url": "",
"duration": 0
},
{
"title": "",
"artist": "",
"audio_url": "",
"audio_full_url": "",
"m3u8_url": "",
"lyric_url": "",
"cover_url": "",
"duration": 0
}
]

View File

@@ -1,15 +0,0 @@
package main
// MusicItem represents a music item.
type MusicItem struct {
Title string `json:"title"`
Artist string `json:"artist"`
AudioURL string `json:"audio_url"`
AudioFullURL string `json:"audio_full_url"`
M3U8URL string `json:"m3u8_url"`
LyricURL string `json:"lyric_url"`
CoverURL string `json:"cover_url"`
Duration int `json:"duration"`
FromCache bool `json:"from_cache"`
IP string `json:"ip"`
}

1
themes/.gitignore vendored
View File

@@ -1 +0,0 @@

View File

@@ -1,163 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
)
type YuafengAPIFreeResponse struct {
Data struct {
Song string `json:"song"`
Singer string `json:"singer"`
Cover string `json:"cover"`
AlbumName string `json:"album_name"`
Music string `json:"music"`
Lyric string `json:"lyric"`
} `json:"data"`
}
// 枫雨API response handler.
func YuafengAPIResponseHandler(sources, song, singer string) MusicItem {
fmt.Printf("[Info] Fetching music data from 枫林 free API for %s by %s\n", song, singer)
var APIurl string
switch sources {
case "kuwo":
APIurl = "https://api.yuafeng.cn/API/ly/kwmusic.php"
case "netease":
APIurl = "https://api.yuafeng.cn/API/ly/wymusic.php"
case "migu":
APIurl = "https://api.yuafeng.cn/API/ly/mgmusic.php"
case "baidu":
APIurl = "https://api.yuafeng.cn/API/ly/bdmusic.php"
default:
return MusicItem{}
}
resp, err := http.Get(APIurl + "?msg=" + song + "&n=1")
if err != nil {
fmt.Println("[Error] Error fetching the data from Yuafeng free API:", err)
return MusicItem{}
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("[Error] Error reading the response body from Yuafeng free API:", err)
return MusicItem{}
}
var response YuafengAPIFreeResponse
err = json.Unmarshal(body, &response)
if err != nil {
fmt.Println("[Error] Error unmarshalling the data from Yuafeng free API:", err)
return MusicItem{}
}
// Create directory
dirName := fmt.Sprintf("./files/cache/music/%s-%s", response.Data.Singer, response.Data.Song)
err = os.MkdirAll(dirName, 0755)
if err != nil {
fmt.Println("[Error] Error creating directory:", err)
return MusicItem{}
}
if response.Data.Music == "" {
fmt.Println("[Warning] Music URL is empty")
return MusicItem{}
}
// Identify music file format
musicExt, err := getMusicFileExtension(response.Data.Music)
if err != nil {
fmt.Println("[Error] Error identifying music file format:", err)
return MusicItem{}
}
// Download music files
err = downloadFile(filepath.Join(dirName, "music_full"+musicExt), response.Data.Music)
if err != nil {
fmt.Println("[Error] Error downloading music file:", err)
}
// Retrieve music file duration
musicFilePath := filepath.Join(dirName, "music_full"+musicExt)
duration := getMusicDuration(musicFilePath)
// Download cover image
ext := filepath.Ext(response.Data.Cover)
err = downloadFile(filepath.Join(dirName, "cover"+ext), response.Data.Cover)
if err != nil {
fmt.Println("[Error] Error downloading cover image:", err)
}
// Check if the lyrics format is in link format
lyricData := response.Data.Lyric
if lyricData == "获取歌词失败" {
// If it is "获取歌词失败", do nothing
fmt.Println("[Warning] Lyric retrieval failed, skipping lyric file creation and download.")
} else if !strings.HasPrefix(lyricData, "http://") && !strings.HasPrefix(lyricData, "https://") {
// If it is not in link format, write the lyrics to the file line by line
lines := strings.Split(lyricData, "\n")
lyricFilePath := filepath.Join(dirName, "lyric.lrc")
file, err := os.Create(lyricFilePath)
if err != nil {
fmt.Println("[Error] Error creating lyric file:", err)
return MusicItem{}
}
defer file.Close()
timeTagRegex := regexp.MustCompile(`^\[(\d+(?:\.\d+)?)\]`)
for _, line := range lines {
// Check if the line starts with a time tag
match := timeTagRegex.FindStringSubmatch(line)
if match != nil {
// Convert the time tag to [mm:ss.ms] format
timeInSeconds, _ := strconv.ParseFloat(match[1], 64)
minutes := int(timeInSeconds / 60)
seconds := int(timeInSeconds) % 60
milliseconds := int((timeInSeconds-float64(seconds))*1000) / 100 % 100
formattedTimeTag := fmt.Sprintf("[%02d:%02d.%02d]", minutes, seconds, milliseconds)
line = timeTagRegex.ReplaceAllString(line, formattedTimeTag)
}
_, err := file.WriteString(line + "\r\n")
if err != nil {
fmt.Println("[Error] Error writing to lyric file:", err)
return MusicItem{}
}
}
} else {
// If it is in link format, download the lyrics file
err = downloadFile(filepath.Join(dirName, "lyric.lrc"), lyricData)
if err != nil {
fmt.Println("[Error] Error downloading lyric file:", err)
}
}
// Compress and segment audio file
err = compressAndSegmentAudio(filepath.Join(dirName, "music_full"+musicExt), dirName)
if err != nil {
fmt.Println("[Error] Error compressing and segmenting audio:", err)
}
// Create m3u8 playlist
err = createM3U8Playlist(dirName)
if err != nil {
fmt.Println("[Error] Error creating m3u8 playlist:", err)
}
return MusicItem{
Title: response.Data.Song,
Artist: response.Data.Singer,
CoverURL: "/cache/music/" + url.QueryEscape(response.Data.Singer+"-"+response.Data.Song) + "/cover" + ext,
LyricURL: "/cache/music/" + url.QueryEscape(response.Data.Singer+"-"+response.Data.Song) + "/lyric.lrc",
AudioFullURL: "/cache/music/" + url.QueryEscape(response.Data.Singer+"-"+response.Data.Song) + "/music_full" + musicExt,
AudioURL: "/cache/music/" + url.QueryEscape(response.Data.Singer+"-"+response.Data.Song) + "/music.mp3",
M3U8URL: "/cache/music/" + url.QueryEscape(response.Data.Singer+"-"+response.Data.Song) + "/music.m3u8",
Duration: duration,
}
}