Modify the repository version to v0.1.0
This commit is contained in:
22
.env.example
22
.env.example
@@ -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
|
||||
28
.github/workflows/build-and-test.yml
vendored
28
.github/workflows/build-and-test.yml
vendored
@@ -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
641
LICENSE
@@ -1,201 +1,504 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 2.1, February 1999
|
||||
|
||||
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,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
Preamble
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
The licenses for most software are designed to take away your
|
||||
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
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
This license, the Lesser General Public License, applies to some
|
||||
specially designated software packages--typically libraries--of the
|
||||
Free Software Foundation and other authors who decide to use it. You
|
||||
can use it too, but we suggest you first think carefully about whether
|
||||
this license or the ordinary General Public License is the better
|
||||
strategy to use in any particular case, based on the explanations below.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
When we speak of free software, we are referring to freedom of use,
|
||||
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,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
distributors to deny you these rights or to ask you to surrender these
|
||||
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
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
For example, if you distribute copies of the library, whether gratis
|
||||
or for a fee, you must give the recipients all the rights that we gave
|
||||
you. You must make sure that they, too, receive or can get the source
|
||||
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
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
We protect your rights with a two-step method: (1) we copyright the
|
||||
library, and (2) we offer you this license, which gives you legal
|
||||
permission to copy, distribute and/or modify the library.
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
To protect each distributor, we want to make it very clear that
|
||||
there is no warranty for the free library. Also, if the library is
|
||||
modified by someone else and passed on, the recipients should know
|
||||
that what they have is not the original version, so that the original
|
||||
author's reputation will not be affected by problems that might be
|
||||
introduced by others.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
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."
|
||||
Finally, software patents pose a constant threat to the existence of
|
||||
any free program. We wish to make sure that a company cannot
|
||||
effectively restrict the users of a free program by obtaining a
|
||||
restrictive license from a patent holder. Therefore, we insist that
|
||||
any patent license obtained for a version of the library must be
|
||||
consistent with the full freedom of use specified in this license.
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
Most GNU software, including some libraries, is covered by the
|
||||
ordinary GNU General Public License. This license, the GNU Lesser
|
||||
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
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
When a program is linked with a library, whether statically or using
|
||||
a shared library, the combination of the two is legally speaking a
|
||||
combined work, a derivative of the original library. The ordinary
|
||||
General Public License therefore permits such linking only if the
|
||||
entire combination fits its criteria of freedom. The Lesser General
|
||||
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
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
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.
|
||||
We call this license the "Lesser" General Public License because it
|
||||
does Less to protect the user's freedom than the ordinary General
|
||||
Public License. It also provides other free software developers Less
|
||||
of an advantage over competing non-free programs. These disadvantages
|
||||
are the reason we use the ordinary General Public License for many
|
||||
libraries. However, the Lesser license provides advantages in certain
|
||||
special circumstances.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
For example, on rare occasions, there may be a special need to
|
||||
encourage the widest possible use of a certain library, so that it becomes
|
||||
a de-facto standard. To achieve this, non-free programs must be
|
||||
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
|
||||
Derivative Works a copy of this License; and
|
||||
In other cases, permission to use a particular library in non-free
|
||||
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
|
||||
stating that You changed the files; and
|
||||
Although the Lesser General Public License is Less protective of the
|
||||
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
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow. Pay close attention to the difference between a
|
||||
"work based on the library" and a "work that uses the library". The
|
||||
former contains code derived from the library, whereas the latter must
|
||||
be combined with the library in order to run.
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
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.
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
0. This License Agreement applies to any software library or other
|
||||
program which contains a notice placed by the copyright holder or
|
||||
other authorized party saying it may be distributed under the terms of
|
||||
this Lesser General Public License (also called "this License").
|
||||
Each licensee is addressed as "you".
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
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.
|
||||
A "library" means a collection of software functions and/or data
|
||||
prepared so as to be conveniently linked with application programs
|
||||
(which use some of those functions and data) to form executables.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
The "Library", below, refers to any such software library or work
|
||||
which has been distributed under these terms. A "work based on the
|
||||
Library" means either the Library or any derivative work under
|
||||
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
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
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.
|
||||
"Source code" for a work means the preferred form of the work for
|
||||
making modifications to it. For a library, complete source code means
|
||||
all the source code for all modules it contains, plus any associated
|
||||
interface definition files, plus the scripts used to control compilation
|
||||
and installation of the library.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
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.
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running a program using the Library is not restricted, and output from
|
||||
such a program is covered only if its contents constitute a work based
|
||||
on the Library (independent of the use of the Library in a tool for
|
||||
writing it). Whether that is true depends on what the Library does
|
||||
and what the program that uses the Library does.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
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.
|
||||
1. You may copy and distribute verbatim copies of the Library's
|
||||
complete source code as you receive it, in any medium, provided that
|
||||
you conspicuously and appropriately publish on each copy an
|
||||
appropriate copyright notice and disclaimer of warranty; keep intact
|
||||
all the notices that refer to this License and to the absence of any
|
||||
warranty; and distribute a copy of this License along with the
|
||||
Library.
|
||||
|
||||
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
|
||||
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.
|
||||
a) The modified work must itself be a software library.
|
||||
|
||||
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");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
c) You must cause the whole of the work to be licensed at no
|
||||
charge to all third parties under the terms of this License.
|
||||
|
||||
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
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
(For example, a function in a library to compute square roots has
|
||||
a purpose that is entirely well-defined independent of the
|
||||
application. Therefore, Subsection 2d requires that any
|
||||
application-supplied function or table used by this function must
|
||||
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
12
NOTICE
@@ -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.
|
||||
24
README.md
24
README.md
@@ -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>
|
||||
@@ -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
191
api.go
@@ -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
1
cache/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
|
||||
212
file.go
212
file.go
@@ -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
5
go.mod
@@ -1,5 +0,0 @@
|
||||
module MeowEmbedded-MusicServer
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require github.com/joho/godotenv v1.5.1
|
||||
436
helper.go
436
helper.go
@@ -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
|
||||
}
|
||||
17
httperr.go
17
httperr.go
@@ -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
407
index.go
@@ -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 = \"©\" + 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 = \"©\" + 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
84
main.go
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
15
struct.go
15
struct.go
@@ -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
1
themes/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user