├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── SyncMusic.php ├── conf └── syncmusic.conf ├── docker-compose.yml ├── face.html ├── face ├── 1.gif ├── 10.gif ├── 11.jpg ├── 12.jpg ├── 13.jpg ├── 14.jpg ├── 15.jpg ├── 16.jpg ├── 17.jpg ├── 18.jpg ├── 19.jpg ├── 2.jpg ├── 20.png ├── 21.jpg ├── 22.gif ├── 23.jpg ├── 24.jpg ├── 25.jpg ├── 26.jpg ├── 27.jpg ├── 28.jpg ├── 29.jpg ├── 3.png ├── 4.jpg ├── 5.png ├── 6.png ├── 7.png ├── 8.jpg └── 9.jpg ├── getlength.py ├── index.html ├── random.txt ├── search.php └── server.php /.gitignore: -------------------------------------------------------------------------------- 1 | tmp/ 2 | 3 | music*.json -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM swoft/alphp:fpm 2 | RUN apk add mutagen 3 | ENTRYPOINT /run.sh & php /var/www/server.php -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SyncMusic 2 | 🎵 PHP Swoole 开发的在线同步点歌台,支持自由点歌,切歌,调整排序,删除指定音乐以及基础权限分级 3 | 4 | ![img](https://i.loli.net/2019/11/07/LWSAIwPiYjnH7zT.png) 5 | 6 | 代码写了很详细的注释,非常适合新人学习 PHP WebSocket 应用程序开发。 7 | 8 | ## 功能特性 9 | - 支持在线点歌 10 | - 支持多人实时聊天 11 | - 支持投票切掉当前音乐 12 | - 管理员可切歌 13 | - 管理员可删除指定音乐 14 | - 管理员可将指定音乐提前播放 15 | - 管理员可禁言指定用户 16 | - 美观的界面 (Material Design) 17 | - 无需登录,任何人都可以点歌 18 | - 无需数据库,由 Swoole 内存表储存数据 19 | 20 | 有个地方就是获取音乐时间长度是用了 python,原本我是想直接用 PHP 来获取的,但是有点麻烦,还要导入一个单独的库,想了想还是用最简单的办法来解决,于是就用 python 整了个简单的脚本。 21 | 22 | 如果你有更好的读取音乐时间的实现方法,欢迎提 pr 或通过 issues 告诉我。 23 | 24 | ## 安装教程 25 | 26 | 请访问 Wiki 页面:[Installation](https://github.com/kasuganosoras/SyncMusic/wiki/Installation) 27 | 28 | 如果安装时遇到问题,可以通过 Issues 提问。 29 | 30 | ## 在线预览 31 | 32 | ZeroDream:[Akkariin 点歌台](https://music.tql.ink/) 33 | 34 | > 如果你想将你的点歌台列在这里,请开一个 Issues 并写上你的点歌台地址。 35 | 36 | ## 开源协议 37 | 38 | 本项目使用 GPL v3 协议开源 39 | -------------------------------------------------------------------------------- /SyncMusic.php: -------------------------------------------------------------------------------- 1 | bindHost = $bindHost; 34 | $this->bindPort = $bindPort; 35 | $this->adminPass = $adminPass; 36 | $this->workersNum = $workersNum; 37 | $this->debug = $debug; 38 | $this->getIpMethod = $getIpMethod; 39 | $this->musicApi = $musicApi; 40 | } 41 | 42 | /** 43 | * 44 | * checkDataFolder 检查并创建数据目录 45 | * 46 | */ 47 | public function checkDataFolder() 48 | { 49 | if(!file_exists(ROOT . "/tmp/")) mkdir(ROOT . "/tmp/"); 50 | if(!file_exists(ROOT . "/random.txt")) { 51 | $data = @file_get_contents("https://cdn.zerodream.net/download/music/random.txt"); 52 | @file_put_contents(ROOT . "/random.txt", $data); 53 | } 54 | } 55 | 56 | /** 57 | * 58 | * Init 初始化并配置服务器 59 | * 60 | */ 61 | public function init() 62 | { 63 | $this->checkDataFolder(); 64 | 65 | $this->server = new Swoole\WebSocket\Server($this->bindHost, $this->bindPort); 66 | $this->server->set([ 67 | 'task_worker_num' => $this->workersNum, 68 | 'worker_num' => 16, 69 | ]); 70 | 71 | // Table 表,用于储存服务器的信息 72 | $table = new Swoole\Table(1024); 73 | $table->column('music_time', Swoole\Table::TYPE_FLOAT, 8); 74 | $table->column('music_play', Swoole\Table::TYPE_FLOAT, 8); 75 | $table->column('music_long', Swoole\Table::TYPE_FLOAT, 8); 76 | $table->column('downloaded', Swoole\Table::TYPE_FLOAT, 8); 77 | $table->column('needswitch', Swoole\Table::TYPE_STRING, 32768); 78 | $table->column('music_list', Swoole\Table::TYPE_STRING, 32768); 79 | $table->column('music_show', Swoole\Table::TYPE_STRING, 32768); 80 | $table->column('banned_ips', Swoole\Table::TYPE_STRING, 32768); 81 | $table->create(); 82 | 83 | // Chats 表,用于储存用户的信息 84 | $chats = new Swoole\Table(1024); 85 | $chats->column('ip', Swoole\Table::TYPE_STRING, 256); 86 | $chats->column('last', Swoole\Table::TYPE_FLOAT, 8); 87 | $chats->create(); 88 | 89 | // 初始化信息 90 | $this->server->table = $table; 91 | $this->server->chats = $chats; 92 | $this->server->started = false; 93 | $this->server->randomed = false; 94 | $this->server->adding = false; 95 | 96 | /** 97 | * 98 | * Open Event 当客户端与服务器建立连接时触发此事件 99 | * 100 | */ 101 | $this->server->on('open', function (Swoole\WebSocket\Server $server, $request) { 102 | 103 | // 当第一个客户端连接到服务器的时候就触发 Task 去处理事件 104 | if(!$server->started) { 105 | $server->task(["action" => "Start"]); 106 | $server->started = true; 107 | } 108 | 109 | // 获取客户端的 IP 地址 110 | if($this->getIpMethod) { 111 | $clientIp = $request->header['x-real-ip'] ?? "127.0.0.1"; 112 | } else { 113 | $clientIp = $server->getClientInfo($request->fd)['remote_ip'] ?? "127.0.0.1"; 114 | } 115 | 116 | // 将客户端 IP 储存到表中 117 | $server->chats->set($request->fd, ["ip" => $clientIp]); 118 | 119 | $this->consoleLog("客户端 {$request->fd} [{$clientIp}] 已连接到服务器", 1, true); 120 | 121 | $server->push($request->fd, json_encode([ 122 | "type" => "msg", 123 | "data" => "你已经成功连接到服务器!" 124 | ])); 125 | 126 | $musicPlay = $this->getMusicPlay(); 127 | $musicList = $this->getMusicShow(); 128 | 129 | // 如果当前列表中有音乐可播放 130 | if($musicList && !empty($musicList)) { 131 | 132 | // 获取音乐的信息和歌词 133 | $musicInfo = $musicList[0]; 134 | $lrcs = $this->getMusicLrcs($musicInfo['id']); 135 | 136 | // 推送给客户端 137 | $server->push($request->fd, json_encode([ 138 | "type" => "music", 139 | "id" => $musicInfo['id'], 140 | "name" => $musicInfo['name'], 141 | "file" => $this->getMusicUrl($musicInfo['id']), 142 | "album" => $musicInfo['album'], 143 | "artists" => $musicInfo['artists'], 144 | "image" => $musicInfo['image'], 145 | "current" => $musicPlay + 1, 146 | "lrcs" => $lrcs, 147 | "user" => $musicInfo['user'] 148 | ])); 149 | 150 | // 播放列表更新 151 | $playList = $this->getPlayList($musicList); 152 | $server->push($request->fd, json_encode([ 153 | "type" => "list", 154 | "data" => $playList 155 | ])); 156 | } 157 | }); 158 | 159 | /** 160 | * 161 | * Message Event 当客户端发送数据到服务器时触发此事件 162 | * 163 | */ 164 | $this->server->on('message', function (Swoole\WebSocket\Server $server, $frame) { 165 | 166 | $clients = $server->connections; 167 | $clientIp = $this->getClientIp($frame->fd); 168 | $adminIp = $this->getAdminIp(); 169 | 170 | // 判断客户端是否已被封禁 171 | if($this->isBanned($clientIp)) { 172 | $server->push($frame->fd, json_encode([ 173 | "type" => "msg", 174 | "data" => "你没有权限发言" 175 | ])); 176 | } else { 177 | 178 | // 把客户端 IP 地址的 C 段和 D 段打码作为用户名显示 179 | $username = $this->getMarkName($clientIp); 180 | 181 | // 解析客户端发过来的消息 182 | $message = $frame->data; 183 | $json = json_decode($message, true); 184 | 185 | if($json && isset($json['type'])) { 186 | switch($json['type']) { 187 | case "msg": 188 | 189 | // 获取客户端最后发言的时间戳 190 | $lastChat = $this->getLastChat($frame->fd); 191 | 192 | //防止客户端刷屏 193 | if($lastChat && time() - $lastChat <= MIN_CHATWAIT) { 194 | $server->push($frame->fd, json_encode([ 195 | "type" => "msg", 196 | "data" => "发言太快,请稍后再发送" 197 | ])); 198 | } else { 199 | 200 | // 储存用户的最后发言时间 201 | $this->setLastChat($frame->fd, time()); 202 | $this->consoleLog("客户端 {$frame->fd} 发送消息:{$json['data']}", 1, true); 203 | 204 | if($json['data'] == "切歌") { 205 | 206 | // 如果是切歌的命令,先判断是否是管理员 207 | if($this->isAdmin($clientIp)) { 208 | 209 | // 执行切歌操作,这里的 time + 1 是为了防止 bug 210 | $this->setMusicTime(time() + 1); 211 | $this->setMusicPlay(0); 212 | $this->setMusicLong(time()); 213 | 214 | $server->push($frame->fd, json_encode([ 215 | "type" => "msg", 216 | "data" => "成功切歌" 217 | ])); 218 | } else { 219 | $server->push($frame->fd, json_encode([ 220 | "type" => "msg", 221 | "data" => "你没有权限这么做" 222 | ])); 223 | } 224 | } elseif($json['data'] == "投票切歌") { 225 | 226 | // 由所有用户投票切掉当前歌曲 227 | $needSwitch = $this->getNeedSwitch(); 228 | $totalUsers = $this->getTotalUsers(); 229 | 230 | // 判断用户是否已经投过票 231 | if($this->isAlreadySwtich($clientIp)) { 232 | $server->push($frame->fd, json_encode([ 233 | "type" => "msg", 234 | "data" => "你已经投票过了" 235 | ])); 236 | } else { 237 | 238 | // 如果是第一次投票 239 | if($needSwitch == 1) { 240 | 241 | // 广播给所有客户端 242 | foreach($server->connections as $id) { 243 | $server->push($id, json_encode([ 244 | "type" => "msg", 245 | "data" => "有人希望切歌,支持请输入 “投票切歌”" 246 | ])); 247 | } 248 | } 249 | 250 | // 判断投票的用户数是否超过在线用户数的 30% 251 | if($needSwitch / $totalUsers >= 0.3) { 252 | 253 | // 执行切歌操作 254 | $this->setMusicTime(time() + 1); 255 | $this->setMusicPlay(0); 256 | $this->setMusicLong(time()); 257 | $this->setNeedSwitch(""); 258 | 259 | $server->push($frame->fd, json_encode([ 260 | "type" => "msg", 261 | "data" => "成功切歌" 262 | ])); 263 | } else { 264 | $this->addNeedSwitch($clientIp); 265 | $server->push($frame->fd, json_encode([ 266 | "type" => "msg", 267 | "data" => "投票成功" 268 | ])); 269 | 270 | // 广播给所有客户端 271 | foreach($server->connections as $id) { 272 | $server->push($id, json_encode([ 273 | "type" => "msg", 274 | "data" => "当前投票人数:{$needSwitch}/{$totalUsers}" 275 | ])); 276 | } 277 | } 278 | 279 | // 广播给所有客户端 280 | $userNickName = $this->getUserNickname($clientIp); 281 | foreach($clients as $id) { 282 | $showUserName = $this->getClientIp($id) == $adminIp ? $clientIp : $username; 283 | if($userNickName) { 284 | $showUserName = "{$userNickName} ({$showUserName})"; 285 | } 286 | $server->push($id, json_encode([ 287 | "type" => "chat", 288 | "user" => htmlspecialchars($showUserName), 289 | "time" => date("Y-m-d H:i:s"), 290 | "data" => htmlspecialchars($json['data']) 291 | ])); 292 | } 293 | } 294 | } elseif($json['data'] == "禁言列表") { 295 | 296 | // 查看已禁言的用户列表,先判断是否是管理员 297 | if($this->isAdmin($clientIp)) { 298 | $server->push($frame->fd, json_encode([ 299 | "type" => "chat", 300 | "user" => "System", 301 | "time" => date("Y-m-d H:i:s"), 302 | "data" => htmlspecialchars("禁言 IP 列表:" . $this->getBannedIp()) 303 | ])); 304 | } else { 305 | $server->push($frame->fd, json_encode([ 306 | "type" => "msg", 307 | "data" => "你没有权限这么做" 308 | ])); 309 | } 310 | } elseif(mb_substr($json['data'], 0, 3) == "禁言 " && mb_strlen($json['data']) > 3) { 311 | 312 | // 如果是禁言客户端的命令,先判断是否是管理员 313 | if($this->isAdmin($clientIp)) { 314 | $banName = trim(mb_substr($json['data'], 3, 99999)); 315 | if(!empty($banName)) { 316 | 317 | // 判断是否已经被禁言 318 | if($this->isBanned($banName)) { 319 | $server->push($frame->fd, json_encode([ 320 | "type" => "msg", 321 | "data" => "这个 IP 已经被禁言了" 322 | ])); 323 | } else { 324 | $this->banIp($banName); 325 | $server->push($frame->fd, json_encode([ 326 | "type" => "msg", 327 | "data" => "成功禁止此 IP 点歌和发言" 328 | ])); 329 | } 330 | } else { 331 | $server->push($frame->fd, json_encode([ 332 | "type" => "msg", 333 | "data" => "禁言的 IP 不能为空!" 334 | ])); 335 | } 336 | } else { 337 | $server->push($frame->fd, json_encode([ 338 | "type" => "msg", 339 | "data" => "你没有权限这么做" 340 | ])); 341 | } 342 | } elseif(mb_substr($json['data'], 0, 3) == "解禁 " && mb_strlen($json['data']) > 3) { 343 | 344 | // 如果是解禁客户端的命令,先判断是否是管理员 345 | if($this->isAdmin($clientIp)) { 346 | $banName = trim(mb_substr($json['data'], 3, 99999)); 347 | if(!empty($banName)) { 348 | 349 | // 如果用户没有被封禁 350 | if(!$this->isBanned($banName)) { 351 | $server->push($frame->fd, json_encode([ 352 | "type" => "msg", 353 | "data" => "这个 IP 没有被禁言" 354 | ])); 355 | } else { 356 | $this->unbanIp($banName); 357 | $server->push($frame->fd, json_encode([ 358 | "type" => "msg", 359 | "data" => "成功解禁此 IP 的禁言" 360 | ])); 361 | } 362 | } else { 363 | $server->push($frame->fd, json_encode([ 364 | "type" => "msg", 365 | "data" => "解禁的 IP 不能为空!" 366 | ])); 367 | } 368 | } else { 369 | $server->push($frame->fd, json_encode([ 370 | "type" => "msg", 371 | "data" => "你没有权限这么做" 372 | ])); 373 | } 374 | 375 | } elseif(mb_substr($json['data'], 0, 3) == "换歌 " && mb_strlen($json['data']) > 3) { 376 | 377 | // 如果是交换歌曲顺序的命令,先判断是否是管理员 378 | if($this->isAdmin($clientIp)) { 379 | $switchMusic = trim(mb_substr($json['data'], 3, 99999)); 380 | if(!empty($switchMusic)) { 381 | $switchMusic = Intval($switchMusic); 382 | 383 | // 不可以切换正在播放的歌曲 384 | if($switchMusic == 0) { 385 | $server->push($frame->fd, json_encode([ 386 | "type" => "msg", 387 | "data" => "正在播放的音乐不能切换" 388 | ])); 389 | } else { 390 | 391 | // 取得列表 392 | $musicList = $this->getMusicList(); 393 | $sourceList = $this->getMusicShow(); 394 | 395 | // 储存并交换两首音乐 396 | $waitSwitch = $musicList[$switchMusic - 1]; 397 | $needSwitch = $musicList[0]; 398 | $musicList[0] = $waitSwitch; 399 | $sourceList[1] = $waitSwitch; 400 | $musicList[$switchMusic - 1] = $needSwitch; 401 | $sourceList[$switchMusic] = $needSwitch; 402 | 403 | // 播放列表更新 404 | $playList = $this->getPlayList($sourceList); 405 | $this->setMusicList($musicList); 406 | $this->setMusicShow($sourceList); 407 | 408 | // 广播给所有客户端 409 | foreach($server->connections as $id) { 410 | $server->push($id, json_encode([ 411 | "type" => "list", 412 | "data" => $playList 413 | ])); 414 | } 415 | 416 | // 发送通知 417 | $server->push($frame->fd, json_encode([ 418 | "type" => "msg", 419 | "data" => "音乐切换成功" 420 | ])); 421 | } 422 | } else { 423 | $server->push($frame->fd, json_encode([ 424 | "type" => "msg", 425 | "data" => "要切换的歌曲不能为空" 426 | ])); 427 | } 428 | } else { 429 | $server->push($frame->fd, json_encode([ 430 | "type" => "msg", 431 | "data" => "你没有权限这么做" 432 | ])); 433 | } 434 | } elseif(mb_substr($json['data'], 0, 5) == "删除音乐 " && mb_strlen($json['data']) > 5) { 435 | 436 | // 如果是删除某首音乐的命令 437 | $deleteMusic = trim(mb_substr($json['data'], 5, 99999)); 438 | $deleteMusic = Intval($deleteMusic); 439 | 440 | // 判断操作者是否是管理员 441 | if($this->isAdmin($clientIp)) { 442 | 443 | // 如果正在播放的音乐是第一首 444 | if($deleteMusic <= 0) { 445 | $server->push($frame->fd, json_encode([ 446 | "type" => "msg", 447 | "data" => "正在播放的音乐不能删除" 448 | ])); 449 | } else { 450 | 451 | // 获取播放列表 452 | $musicList = $this->getMusicList(); 453 | $sourceList = $this->getMusicShow(); 454 | 455 | // 从列表中删除这首歌 456 | unset($musicList[$deleteMusic - 1]); 457 | unset($sourceList[$deleteMusic]); 458 | 459 | // 重新整理列表 460 | $musicList = array_values($musicList); 461 | $sourceList = array_values($sourceList); 462 | 463 | // 播放列表更新 464 | $playList = $this->getPlayList($sourceList); 465 | $this->setMusicList($musicList); 466 | $this->setMusicShow($sourceList); 467 | 468 | // 广播给所有客户端 469 | foreach($server->connections as $id) { 470 | $server->push($id, json_encode([ 471 | "type" => "list", 472 | "data" => $playList 473 | ])); 474 | } 475 | 476 | // 发送通知 477 | $server->push($frame->fd, json_encode([ 478 | "type" => "msg", 479 | "data" => "音乐删除成功" 480 | ])); 481 | } 482 | } else { 483 | 484 | // 如果正在播放的音乐是第一首 485 | if($deleteMusic <= 0) { 486 | $server->push($frame->fd, json_encode([ 487 | "type" => "msg", 488 | "data" => "正在播放的音乐不能删除" 489 | ])); 490 | } else { 491 | 492 | // 获取播放列表 493 | $musicList = $this->getMusicList(); 494 | $sourceList = $this->getMusicShow(); 495 | 496 | if(isset($musicList[$deleteMusic - 1]) && $musicList[$deleteMusic - 1]['user'] == $clientIp) { 497 | 498 | // 从列表中删除这首歌 499 | unset($musicList[$deleteMusic - 1]); 500 | unset($sourceList[$deleteMusic]); 501 | 502 | // 重新整理列表 503 | $musicList = array_values($musicList); 504 | $sourceList = array_values($sourceList); 505 | 506 | // 播放列表更新 507 | $playList = $this->getPlayList($sourceList); 508 | $this->setMusicList($musicList); 509 | $this->setMusicShow($sourceList); 510 | 511 | // 广播给所有客户端 512 | foreach($server->connections as $id) { 513 | $server->push($id, json_encode([ 514 | "type" => "list", 515 | "data" => $playList 516 | ])); 517 | } 518 | 519 | // 发送通知 520 | $server->push($frame->fd, json_encode([ 521 | "type" => "msg", 522 | "data" => "音乐删除成功" 523 | ])); 524 | } else { 525 | $server->push($frame->fd, json_encode([ 526 | "type" => "msg", 527 | "data" => "你只能删除自己点的歌" 528 | ])); 529 | } 530 | } 531 | } 532 | 533 | } elseif(mb_substr($json['data'], 0, 5) == "房管登录 " && mb_strlen($json['data']) > 5) { 534 | 535 | // 如果是房管登录操作 536 | $userPass = trim(mb_substr($json['data'], 5, 99999)); 537 | 538 | // 判断密码是否正确 539 | if($userPass == $this->adminPass) { 540 | $this->setAdminIp($clientIp); 541 | $server->push($frame->fd, json_encode([ 542 | "type" => "msg", 543 | "data" => "房管登录成功" 544 | ])); 545 | } else { 546 | $server->push($frame->fd, json_encode([ 547 | "type" => "msg", 548 | "data" => "房管密码错误" 549 | ])); 550 | } 551 | 552 | } elseif(mb_substr($json['data'], 0, 5) == "加黑名单 " && mb_strlen($json['data']) > 5) { 553 | 554 | // 如果是房管登录操作 555 | $blackList = trim(mb_substr($json['data'], 5, 99999)); 556 | 557 | // 判断密码是否正确 558 | if($this->isAdmin($clientIp)) { 559 | $this->addBlackList($blackList); 560 | $server->push($frame->fd, json_encode([ 561 | "type" => "msg", 562 | "data" => "已增加新的黑名单" 563 | ])); 564 | } else { 565 | $server->push($frame->fd, json_encode([ 566 | "type" => "msg", 567 | "data" => "你没有权限这么做" 568 | ])); 569 | } 570 | 571 | } elseif(mb_substr($json['data'], 0, 5) == "设置昵称 " && mb_strlen($json['data']) > 5) { 572 | 573 | // 如果是设置昵称 574 | $userNick = trim(mb_substr($json['data'], 5, 99999)); 575 | 576 | // 正则判断用户名是否合法 577 | if(preg_match("/^[\x{4e00}-\x{9fa5}A-Za-z0-9_]+[^_]{3,20}$/u", $userNick)) { 578 | if($this->isBlackList($userNick)) { 579 | $server->push($frame->fd, json_encode([ 580 | "type" => "msg", 581 | "data" => "不允许的昵称" 582 | ])); 583 | } elseif(mb_Strlen($userNick) <= 20) { 584 | $this->setUserNickname($clientIp, $userNick); 585 | $server->push($frame->fd, json_encode([ 586 | "type" => "msg", 587 | "data" => "昵称设置成功" 588 | ])); 589 | $server->push($frame->fd, json_encode([ 590 | "type" => "setname", 591 | "data" => $userNick 592 | ])); 593 | } else { 594 | $server->push($frame->fd, json_encode([ 595 | "type" => "msg", 596 | "data" => "昵称最多 20 个字符" 597 | ])); 598 | } 599 | } else { 600 | $server->push($frame->fd, json_encode([ 601 | "type" => "msg", 602 | "data" => "只允许中英文数字下划线,最少 4 个字" 603 | ])); 604 | } 605 | 606 | } elseif(mb_substr($json['data'], 0, 3) == "点歌 " && mb_strlen($json['data']) > 3) { 607 | 608 | // 如果是点歌命令 609 | $musicName = trim(mb_substr($json['data'], 3, 99999)); 610 | if(!empty($musicName)) { 611 | 612 | // 判断是否已经有人在点歌中 613 | if(count($this->getUserMusic($clientIp)) > MAX_USERMUSIC) { 614 | $server->push($frame->fd, json_encode([ 615 | "type" => "msg", 616 | "data" => "你已经点了很多歌了,请先听完再点" 617 | ])); 618 | } elseif($this->isLockedSearch()) { 619 | $server->push($frame->fd, json_encode([ 620 | "type" => "msg", 621 | "data" => "当前有任务正在执行,请稍后再试" 622 | ])); 623 | } else { 624 | if(mb_strlen($json['data']) > MAX_CHATLENGTH) { 625 | $server->push($frame->fd, json_encode([ 626 | "type" => "msg", 627 | "data" => "消息过长,最多 " . MAX_CHATLENGTH . " 字符" 628 | ])); 629 | } else { 630 | 631 | // 提交任务给服务器 632 | $server->task(["id" => $frame->fd, "action" => "Search", "data" => $musicName]); 633 | 634 | // 广播给所有客户端 635 | $userNickName = $this->getUserNickname($clientIp); 636 | foreach($clients as $id) { 637 | $showUserName = $this->getClientIp($id) == $adminIp ? $clientIp : $username; 638 | if($userNickName) { 639 | $showUserName = "{$userNickName} ({$showUserName})"; 640 | } 641 | $server->push($id, json_encode([ 642 | "type" => "chat", 643 | "user" => htmlspecialchars($showUserName), 644 | "time" => date("Y-m-d H:i:s"), 645 | "data" => htmlspecialchars($json['data']) 646 | ])); 647 | } 648 | } 649 | } 650 | } else { 651 | $server->push($frame->fd, json_encode([ 652 | "type" => "msg", 653 | "data" => "歌曲名不能为空!" 654 | ])); 655 | } 656 | } else { 657 | 658 | // 默认消息内容,即普通聊天,广播给所有客户端 659 | if(mb_strlen($json['data']) > MAX_CHATLENGTH) { 660 | $server->push($frame->fd, json_encode([ 661 | "type" => "msg", 662 | "data" => "消息过长,最多 " . MAX_CHATLENGTH . " 字符" 663 | ])); 664 | } else { 665 | if($this->isAdmin($clientIp)) { 666 | $username = "管理员"; 667 | } 668 | $userNickName = $this->getUserNickname($clientIp); 669 | foreach($clients as $id) { 670 | $showUserName = $this->isAdmin($this->getClientIp($id)) ? $clientIp : $username; 671 | if($userNickName) { 672 | $showUserName = "{$userNickName} ({$showUserName})"; 673 | } 674 | $server->push($id, json_encode([ 675 | "type" => "chat", 676 | "user" => htmlspecialchars($showUserName), 677 | "time" => date("Y-m-d H:i:s"), 678 | "data" => htmlspecialchars($json['data']) 679 | ])); 680 | } 681 | } 682 | } 683 | } 684 | break; 685 | case "heartbeat": 686 | // 处理客户端发过来心跳包的操作,返回在线人数给客户端 687 | $server->push($frame->fd, json_encode([ 688 | "type" => "online", 689 | "data" => count($server->connections) 690 | ])); 691 | break; 692 | default: 693 | // 如果客户端发过来未知的消息类型 694 | $this->consoleLog("客户端 {$frame->fd} 发送了未知消息:{$message}", 2, true); 695 | } 696 | } 697 | } 698 | }); 699 | 700 | /** 701 | * 702 | * Close Event 当客户端断开与服务器的连接时触发此事件 703 | * 704 | */ 705 | $this->server->on('close', function ($server, $fd) { 706 | $this->consoleLog("客户端 {$fd} 已断开连接", 1, true); 707 | }); 708 | 709 | /** 710 | * 711 | * Task Event 当服务器运行任务时触发此事件 712 | * 713 | */ 714 | $this->server->on('Task', function (Swoole\Server $server, $task_id, $from_id, $data) { 715 | 716 | // 如果是服务器初始化任务 717 | if($data['action'] == "Start") { 718 | 719 | // 设定死循环的目的是为了建立一个单独的线程用于执行数据更新 720 | while(true) { 721 | 722 | $musicList = $this->getMusicList(); 723 | $musicShow = $this->getMusicShow(); 724 | 725 | // 如果列表为空 726 | if(empty($musicList) || empty($musicShow)) { 727 | $musicList = empty($musicList) ? $this->getSavedMusicList() : $musicList; 728 | $musicShow = empty($musicShow) ? $this->getSavedMusicShow() : $musicShow; 729 | } 730 | 731 | // 如果音乐列表不为空 732 | if(!empty($musicList)) { 733 | 734 | $musicTime = $this->getMusicTime(); 735 | 736 | // 如果音乐的结束时间小于当前时间,即播放完毕 737 | if($musicTime < time() + 3) { 738 | 739 | $server->randomed = false; 740 | 741 | // 获得下一首歌的信息 742 | $musicInfo = $musicList[0]; 743 | $sourceList = $musicList; 744 | 745 | // 从播放列表里移除第一首,因为已经开始播放了 746 | unset($musicList[0]); 747 | $musicList = array_values($musicList); 748 | 749 | $this->consoleLog("正在播放音乐:{$musicInfo['name']}", 1, true); 750 | 751 | // 储存信息 752 | $this->setMusicList($musicList); 753 | $this->setMusicShow($sourceList); 754 | $this->setMusicTime(time() + round($musicInfo['time'])); 755 | $this->setMusicLong(time()); 756 | $this->setMusicPlay(0); 757 | $this->setNeedSwitch(""); 758 | 759 | // 获得播放列表 760 | $playList = $this->getPlayList($sourceList); 761 | $musicLrc = $this->getMusicLrcs($musicInfo['id']); 762 | 763 | // 广播给所有客户端 764 | if($server->connections) { 765 | $currentURL = $this->getMusicUrl($musicInfo['id']); 766 | foreach($server->connections as $id) { 767 | $server->push($id, json_encode([ 768 | "type" => "music", 769 | "id" => $musicInfo['id'], 770 | "name" => $musicInfo['name'], 771 | "file" => $currentURL, 772 | "album" => $musicInfo['album'], 773 | "artists" => $musicInfo['artists'], 774 | "image" => $musicInfo['image'], 775 | "lrcs" => $musicLrc, 776 | "user" => $musicInfo['user'] 777 | ])); 778 | $server->push($id, json_encode([ 779 | "type" => "list", 780 | "data" => $playList 781 | ])); 782 | } 783 | } 784 | } 785 | } else { 786 | 787 | // 如果列表已经空了,先获取当前音乐是否还在播放 788 | $musicTime = $this->getMusicTime(); 789 | 790 | // 判断音乐的结束时间是否小于当前时间,如果是则表示已经播放完了 791 | if($musicTime && $musicTime < time() + 3) { 792 | 793 | // 获取随机的音乐 ID 794 | $rlist = $this->getRandomList(); 795 | if($rlist && !$server->randomed) { 796 | 797 | // 判断是否还有人在线,如果没人就不播放了,有人才播放 798 | if($server->connections && count($server->connections) > 0) { 799 | 800 | // 开始播放随机音乐 801 | $this->searchMusic($server, ["id" => false, "action" => "Search", "data" => $rlist]); 802 | $server->randomed = true; 803 | } 804 | } 805 | } 806 | } 807 | 808 | // 记录音乐已经播放的时间 809 | $musicLong = $this->getMusicLong(); 810 | if($musicLong && is_numeric($musicLong)) { 811 | $this->setMusicPlay(time() - $musicLong); 812 | } 813 | 814 | // 将播放列表储存到硬盘 815 | $this->setSavedMusicList($musicList); 816 | $this->setSavedMusicShow($musicShow); 817 | 818 | // 每秒钟执行一次任务 819 | sleep(1); 820 | } 821 | 822 | } elseif($data['action'] == "Search") { 823 | // 如果是搜索音乐的任务 824 | $this->searchMusic($server, $data); 825 | } 826 | }); 827 | 828 | /** 829 | * 830 | * Finish Event 当服务器任务完成时触发此事件 831 | * 832 | */ 833 | $this->server->on('Finish', function (Swoole\Server $server, $task_id, $data) { 834 | if($data['action'] == "msg" && $data['id']) { 835 | $server->push($data['id'], json_encode([ 836 | "type" => "msg", 837 | "data" => $data['data'] 838 | ])); 839 | } 840 | }); 841 | } 842 | 843 | /** 844 | * 845 | * Run 启动服务器 846 | * 847 | */ 848 | public function run() 849 | { 850 | $this->server->start(); 851 | } 852 | 853 | /** 854 | * 855 | * SearchMusic 搜索音乐 856 | * 857 | */ 858 | private function searchMusic(Swoole\Server $server, $data) 859 | { 860 | $this->consoleLog("正在点歌:{$data['data']}", 1, true); 861 | 862 | $musicList = $this->getMusicList(); 863 | $sourceList = $this->getMusicShow(); 864 | $this->lockSearch(); 865 | 866 | // 开始搜索音乐 867 | $json = $this->fetchMusicApi($data['data']); 868 | 869 | if($json && !empty($json)) { 870 | if(isset($json[0]['id'])) { 871 | $m = $json[0]; 872 | // 判断是否已经点过这首歌了 873 | if($this->isInArray($musicList, $m['id'])) { 874 | $this->unlockSearch(); 875 | $this->server->finish(["id" => $data['id'], "action" => "msg", "data" => "这首歌已经在列表里了"]); 876 | } else { 877 | $artists = $this->getArtists($m['artist']); 878 | $musicUrl = $this->getMusicUrl($m['id']); 879 | // 如果能够正确获取到音乐 URL 880 | if($this->isBlackList($m['id']) || $this->isBlackList($m['name']) || $this->isBlackList($artists)) { 881 | $this->unlockSearch(); 882 | $this->server->finish(["id" => $data['id'], "action" => "msg", "data" => "这首歌被设置不允许点播"]); 883 | } elseif($musicUrl !== "") { 884 | $musicId = Intval($m['id']); 885 | // 开始下载音乐 886 | $musicData = $this->fetchMusic($m, $musicUrl); 887 | $musicImage = $this->getMusicImage($m['pic_id']); 888 | // 如果音乐的文件大小不为 0 889 | if(strlen($musicData) > 0) { 890 | $musicTime = $this->getMusicLength($m['id']); 891 | // 如果音乐的长度为 0(说明下载失败或其他原因) 892 | if($musicTime == 0) { 893 | $this->unlockSearch(); 894 | $this->server->finish(["id" => $data['id'], "action" => "msg", "data" => "歌曲下载失败,错误代码:ERROR_TIME0"]); 895 | } elseif($musicTime > MAX_MUSICLENGTH) { 896 | $this->unlockSearch(); 897 | $this->server->finish(["id" => $data['id'], "action" => "msg", "data" => "歌曲太长影响他人体验,不能超过 " . MAX_MUSICLENGTH . " 秒"]); 898 | } else { 899 | // 保存列表 900 | $clientIp = $data['id'] ? $this->getClientIp($data['id']) : "127.0.0.1"; 901 | $musicList[] = [ 902 | "id" => $musicId, 903 | "name" => $m['name'], 904 | "file" => $musicUrl, 905 | "time" => $musicTime, 906 | "album" => $m['album'], 907 | "artists" => $artists, 908 | "image" => $musicImage, 909 | "user" => $clientIp 910 | ]; 911 | $sourceList[] = [ 912 | "id" => $musicId, 913 | "name" => $m['name'], 914 | "file" => $musicUrl, 915 | "time" => $musicTime, 916 | "album" => $m['album'], 917 | "artists" => $artists, 918 | "image" => $musicImage, 919 | "user" => $clientIp 920 | ]; 921 | $this->setMusicList($musicList); 922 | $this->setMusicShow($sourceList); 923 | // 播放列表更新 924 | $playList = $this->getPlayList($sourceList); 925 | // 广播给所有客户端 926 | if($data['id'] && $this->server->connections) { 927 | foreach($this->server->connections as $id) { 928 | $this->server->push($id, json_encode([ 929 | "type" => "list", 930 | "data" => $playList 931 | ])); 932 | } 933 | } 934 | $this->unlockSearch(); 935 | $this->server->finish(["id" => $data['id'], "action" => "msg", "data" => "点歌成功"]); 936 | } 937 | } else { 938 | $this->unlockSearch(); 939 | $this->server->finish(["id" => $data['id'], "action" => "msg", "data" => "歌曲下载失败,错误代码:ERROR_FILE_EMPTY"]); 940 | } 941 | } else { 942 | $this->unlockSearch(); 943 | $this->server->finish(["id" => $data['id'], "action" => "msg", "data" => "歌曲下载失败,错误代码:ERROR_URL_EMPTY"]); 944 | } 945 | } 946 | } else { 947 | $this->unlockSearch(); 948 | $this->server->finish(["id" => $data['id'], "action" => "msg", "data" => "歌曲下载失败,错误代码:ERROR_ID_EMPTY"]); 949 | } 950 | } else { 951 | $this->unlockSearch(); 952 | $this->server->finish(["id" => $data['id'], "action" => "msg", "data" => "未搜索到此歌曲"]); 953 | } 954 | } 955 | 956 | /** 957 | * 958 | * BanIp 封禁指定 IP 地址 959 | * 960 | */ 961 | private function banIp($ip) 962 | { 963 | $bannedIp = $this->getBannedIp() . "{$ip};"; 964 | $this->server->table->set(0, ["banned_ips" => $bannedIp]); 965 | } 966 | 967 | /** 968 | * 969 | * UnbanIp 解封指定 IP 地址 970 | * 971 | */ 972 | private function unbanIp($ip) 973 | { 974 | $bannedIp = str_replace("{$ip};", "", $this->getBannedIp()); 975 | $this->setBannedIp($bannedIp); 976 | } 977 | 978 | /** 979 | * 980 | * LockSearch 禁止点歌 981 | * 982 | */ 983 | private function lockSearch() 984 | { 985 | $this->server->table->set(0, ["downloaded" => 1]); 986 | } 987 | 988 | /** 989 | * 990 | * UnlockSearch 允许点歌 991 | * 992 | */ 993 | private function unlockSearch() 994 | { 995 | $this->server->table->set(0, ["downloaded" => 0]); 996 | } 997 | 998 | /** 999 | * 1000 | * AddNewSwitch 增加新的投票成员 1001 | * 1002 | */ 1003 | private function addNeedSwitch($ip) 1004 | { 1005 | $switchList = $this->server->table->get(0, "needswitch") . "{$ip};"; 1006 | $this->server->table->set(0, ["needswitch" => $switchList]); 1007 | } 1008 | 1009 | /** 1010 | * 1011 | * AddBlackList 增加新的黑名单关键字 1012 | * 1013 | */ 1014 | private function addBlackList($data) 1015 | { 1016 | $blackList = $this->getBlackList(); 1017 | $blackList[] = trim($data); 1018 | $this->setBlackList($blackList); 1019 | } 1020 | 1021 | /** 1022 | * 1023 | * IsAdmin 判断是否是管理员 1024 | * 1025 | */ 1026 | private function isAdmin($ip) 1027 | { 1028 | $adminIp = $this->getAdminIp(); 1029 | return ($adminIp !== "" && $adminIp !== "127.0.0.1" && $adminIp == $ip); 1030 | } 1031 | 1032 | /** 1033 | * 1034 | * IsBanned 判断是否已被封禁 1035 | * 1036 | */ 1037 | private function isBanned($ip) 1038 | { 1039 | $bannedIp = $this->getBannedIp(); 1040 | return ($bannedIp && stristr($bannedIp, "{$ip};")); 1041 | } 1042 | 1043 | /** 1044 | * 1045 | * IsBlackList 判断是否在黑名单音乐中 1046 | * 1047 | */ 1048 | private function isBlackList($key) 1049 | { 1050 | $blackList = $this->getBlackList(); 1051 | for($i = 0;$i < count($blackList);$i++) { 1052 | if(stristr($key, $blackList[$i])) { 1053 | return true; 1054 | } 1055 | } 1056 | return false; 1057 | } 1058 | 1059 | /** 1060 | * 1061 | * IsLockedSearch 判断是否禁止点歌 1062 | * 1063 | */ 1064 | private function isLockedSearch() 1065 | { 1066 | return Intval($this->server->table->get(0, "downloaded")) == 1; 1067 | } 1068 | 1069 | /** 1070 | * 1071 | * IsAlreadySwitch 判断是否已经投票过了 1072 | * 1073 | */ 1074 | private function isAlreadySwtich($ip) 1075 | { 1076 | $switchList = $this->server->table->get(0, "needswitch"); 1077 | return stristr($switchList, "{$ip};") ? true : false; 1078 | } 1079 | 1080 | /** 1081 | * 1082 | * IsInArray 判断指定元素是否在数组中 1083 | * 1084 | */ 1085 | private function isInArray($array, $need, $key = 'id') 1086 | { 1087 | $found = false; 1088 | foreach($array as $smi) { 1089 | if($smi[$key] == $need) { 1090 | $found = true; 1091 | break; 1092 | } 1093 | } 1094 | return $found; 1095 | } 1096 | 1097 | /** 1098 | * 1099 | * GetBlackList 获取音乐的黑名单列表 1100 | * 1101 | */ 1102 | private function getBlackList() 1103 | { 1104 | $data = @file_get_contents(ROOT . "/blacklist.txt"); 1105 | $exp = explode("\n", $data); 1106 | $result = []; 1107 | for($i = 0;$i < count($exp);$i++) { 1108 | $tmpData = trim($exp[$i]); 1109 | if(!empty($tmpData)) { 1110 | $result[] = $tmpData; 1111 | } 1112 | } 1113 | return $result; 1114 | } 1115 | 1116 | /** 1117 | * 1118 | * GetClientIp 获取客户端 IP 地址 1119 | * 1120 | */ 1121 | private function getClientIp($id) 1122 | { 1123 | return $this->server->chats->get($id, "ip") ?? "127.0.0.1"; 1124 | } 1125 | 1126 | /** 1127 | * 1128 | * GetLastChat 获取客户端最后一次发言时间 1129 | * 1130 | */ 1131 | private function getLastChat($id) 1132 | { 1133 | return $this->server->chats->get($id, "last") ?? 0; 1134 | } 1135 | 1136 | /** 1137 | * 1138 | * GetMaskName 获取和谐过的客户端 IP 地址 1139 | * 1140 | */ 1141 | private function getMarkName($ip) 1142 | { 1143 | $username = $ip ?? "127.0.0.1"; 1144 | $uexp = explode(".", $username); 1145 | if(count($uexp) >= 4) { 1146 | $username = "{$uexp[0]}.{$uexp[1]}." . str_repeat("*", strlen($uexp[2])) . "." . str_repeat("*", strlen($uexp[3])); 1147 | } else { 1148 | $username = "Unknown"; 1149 | } 1150 | return $username; 1151 | } 1152 | 1153 | /** 1154 | * 1155 | * GetRandomList 获取随机的音乐 ID 1156 | * 1157 | */ 1158 | private function getRandomList() 1159 | { 1160 | $data = @file_get_contents(ROOT . "/random.txt"); 1161 | $exp = explode("\n", $data); 1162 | if(count($exp) > 0) { 1163 | $rand = trim($exp[mt_rand(0, count($exp) - 1)]); 1164 | } else { 1165 | $rand = false; 1166 | } 1167 | return $rand; 1168 | } 1169 | 1170 | /** 1171 | * 1172 | * GetMusicUrl 获取音乐的下载地址 1173 | * 1174 | */ 1175 | private function getMusicUrl($id) 1176 | { 1177 | echo $this->debug ? $this->consoleLog("Http Request >> {$this->musicApi}/api.php?source=netease&types=url&id={$id}", 0) : ""; 1178 | $rawdata = @file_get_contents("{$this->musicApi}/api.php?source=netease&types=url&id={$id}"); 1179 | $json = json_decode($rawdata, true); 1180 | echo $this->debug ? $this->consoleLog("Http Request << {$rawdata}", 0) : ""; 1181 | if($json && isset($json["url"])) { 1182 | return str_replace("http://", "https://", $json["url"]); 1183 | } else { 1184 | return ""; 1185 | } 1186 | } 1187 | 1188 | /** 1189 | * 1190 | * GetMusicLrcs 获取音乐的歌词 1191 | * 1192 | */ 1193 | private function getMusicLrcs($id) 1194 | { 1195 | if(!file_exists(ROOT . "/tmp/{$id}.lrc")) { 1196 | echo $this->debug ? $this->consoleLog("Http Request >> https://music.163.com/api/song/lyric?os=pc&lv=-1&id={$id}", 0) : ""; 1197 | $musicLrcs = @file_get_contents("https://music.163.com/api/song/lyric?os=pc&lv=-1&id={$id}"); 1198 | echo $this->debug ? $this->consoleLog("Http Request << " . substr($musicLrcs, 0, 256), 0) : ""; 1199 | if(strlen($musicLrcs) > 0) { 1200 | @file_put_contents(ROOT . "/tmp/{$id}.lrc", $musicLrcs); 1201 | } 1202 | } else { 1203 | $musicLrcs = @file_get_contents(ROOT . "/tmp/{$id}.lrc"); 1204 | } 1205 | $lrcs = "[00:01.00]暂无歌词"; 1206 | $lrc = json_decode($musicLrcs, true); 1207 | if($lrc) { 1208 | if(isset($lrc['lrc'])) { 1209 | $lrcs = $lrc['lrc']['lyric']; 1210 | } else { 1211 | $lrcs = "[00:01.00]暂无歌词"; 1212 | } 1213 | } 1214 | return $lrcs; 1215 | } 1216 | 1217 | /** 1218 | * 1219 | * GetMusicImage 获取音乐的专辑封面图片地址 1220 | * 1221 | */ 1222 | private function getMusicImage($picId) 1223 | { 1224 | echo $this->debug ? $this->consoleLog("Http Request >> {$this->musicApi}/api.php?source=netease&types=pic&id={$picId}", 0) : ""; 1225 | $rawdata = @file_get_contents("{$this->musicApi}/api.php?source=netease&types=pic&id={$picId}"); 1226 | $imgdata = json_decode($rawdata, true); 1227 | echo $this->debug ? $this->consoleLog("Http Request << {$rawdata}", 0) : ""; 1228 | return $imgdata['url'] ?? ""; 1229 | } 1230 | 1231 | /** 1232 | * 1233 | * GetPlayList 获取格式化过的播放列表 1234 | * 1235 | */ 1236 | private function getPlayList($sourceList) 1237 | { 1238 | // 播放列表更新 1239 | $playList = << 1241 | ID 1242 | 歌名 1243 | 歌手 1244 | 专辑 1245 | 点歌人 1246 | 1247 | EOF; 1248 | foreach($sourceList as $mid => $mi) { 1249 | $userNick = $this->getUserNickname($mi['user']) ?? "匿名用户"; 1250 | $user = "{$userNick} (" . $this->getMarkName($mi['user']) . ")"; 1251 | $musicName = (mb_strlen($mi['name']) > 32) ? mb_substr($mi['name'], 0, 30) . "..." : $mi['name']; 1252 | $playList .= << 1254 | {$mid} 1255 | {$musicName} 1256 | {$mi['artists']} 1257 | {$mi['album']} 1258 | {$user} 1259 | 1260 | EOF; 1261 | } 1262 | return $playList; 1263 | } 1264 | 1265 | /** 1266 | * 1267 | * GetUserMusic 获取用户点播的音乐数量 1268 | * 1269 | */ 1270 | private function getUserMusic($ip) 1271 | { 1272 | $musicList = $this->getMusicList(); 1273 | $userMusic = []; 1274 | foreach($musicList as $music) { 1275 | if($music['user'] == $ip) { 1276 | $userMusic[] = $music; 1277 | } 1278 | } 1279 | return $userMusic; 1280 | } 1281 | 1282 | /** 1283 | * 1284 | * GetMusicList 获取等待播放的音乐列表 1285 | * 1286 | */ 1287 | private function getMusicList() 1288 | { 1289 | if(USE_REDIS) { 1290 | $redis = new Redis(); 1291 | $redis->connect(REDIS_HOST, REDIS_PORT); 1292 | if(!empty(REDIS_PASS)) { 1293 | $redis->auth(REDIS_PASS); 1294 | } 1295 | $data = $redis->get("syncmusic-list"); 1296 | $musicList = json_decode($data, true); 1297 | } else { 1298 | $musicList = json_decode($this->server->table->get(0, "music_list"), true); 1299 | } 1300 | if(!$musicList || empty($musicList)) { 1301 | $musicList = []; 1302 | } 1303 | return $musicList; 1304 | } 1305 | 1306 | /** 1307 | * 1308 | * GetMusicShow 获取用于显示在网页上的音乐列表 1309 | * 1310 | */ 1311 | private function getMusicShow() 1312 | { 1313 | if(USE_REDIS) { 1314 | $redis = new Redis(); 1315 | $redis->connect(REDIS_HOST, REDIS_PORT); 1316 | if(!empty(REDIS_PASS)) { 1317 | $redis->auth(REDIS_PASS); 1318 | } 1319 | $data = $redis->get("syncmusic-show"); 1320 | $sourceList = json_decode($data, true); 1321 | } else { 1322 | $sourceList = json_decode($this->server->table->get(0, "music_show"), true); 1323 | } 1324 | if(!$sourceList || empty($sourceList)) { 1325 | $sourceList = []; 1326 | } 1327 | return $sourceList; 1328 | } 1329 | 1330 | /** 1331 | * 1332 | * GetSavedMusicList 获取已经保存在硬盘的音乐列表 1333 | * 1334 | */ 1335 | private function getSavedMusicList() 1336 | { 1337 | $data = @file_get_contents(ROOT . "/musiclist.json"); 1338 | return empty($data) ? [] : json_decode($data, true); 1339 | } 1340 | 1341 | /** 1342 | * 1343 | * GetSavedMusicShow 获取已经保存在硬盘的音乐显示列表 1344 | * 1345 | */ 1346 | private function getSavedMusicShow() 1347 | { 1348 | $data = @file_get_contents(ROOT . "/musicshow.json"); 1349 | return empty($data) ? [] : json_decode($data, true); 1350 | } 1351 | 1352 | /** 1353 | * 1354 | * GetMusicTime 获取当前正在播放的音乐的结束时间 1355 | * 1356 | */ 1357 | private function getMusicTime() 1358 | { 1359 | return $this->server->table->get(0, "music_time") ?? 0; 1360 | } 1361 | 1362 | /** 1363 | * 1364 | * GetMusicLong 获取音乐开始播放的时间 1365 | * 1366 | */ 1367 | private function getMusicLong() 1368 | { 1369 | return $this->server->table->get(0, "music_long") ?? time(); 1370 | } 1371 | 1372 | /** 1373 | * 1374 | * GetMusicPlay 获取音乐已经播放的时间 1375 | * 1376 | */ 1377 | private function getMusicPlay() 1378 | { 1379 | return $this->server->table->get(0, "music_play") ?? 0; 1380 | } 1381 | 1382 | /** 1383 | * 1384 | * GetAdminIp 获取管理员的 IP 1385 | * 1386 | */ 1387 | private function getAdminIp() 1388 | { 1389 | $adminIp = @file_get_contents(ROOT . "/admin.ip"); 1390 | return $adminIp ?? "127.0.0.1"; 1391 | } 1392 | 1393 | /** 1394 | * 1395 | * GetBannedIp 获取已经被封禁的 IP 1396 | * 1397 | */ 1398 | private function getBannedIp() 1399 | { 1400 | return $this->server->table->get(0, "banned_ips") ?? ""; 1401 | } 1402 | 1403 | /** 1404 | * 1405 | * GetMusicLength 获取音乐的总长度时间 1406 | * 1407 | */ 1408 | private function getMusicLength($id) 1409 | { 1410 | return FloatVal(shell_exec(PYTHON_EXEC . " getlength.py " . ROOT . "/tmp/{$id}.mp3")); 1411 | } 1412 | 1413 | /** 1414 | * 1415 | * GetArtists 获取音乐的歌手信息 1416 | * 1417 | */ 1418 | private function getArtists($data) 1419 | { 1420 | if(count($data) > 1) { 1421 | $artists = ""; 1422 | foreach($data as $artist) { 1423 | $artists .= $artist . ","; 1424 | } 1425 | $artists = $artists == "" ? "未知歌手" : mb_substr($artists, 0, mb_strlen($artists) - 1); 1426 | } else { 1427 | $artists = $data[0]; 1428 | } 1429 | return $artists; 1430 | } 1431 | 1432 | /** 1433 | * 1434 | * GetLoggerLevel 获取输出日志的等级 1435 | * 1436 | */ 1437 | private function getLoggerLevel($level) 1438 | { 1439 | $levelGroup = ["DEBUG", "INFO", "WARNING", "ERROR"]; 1440 | return $levelGroup[$level] ?? "INFO"; 1441 | } 1442 | 1443 | /** 1444 | * 1445 | * GetNeedSwitch 获取需要切歌的投票用户列表 1446 | * 1447 | */ 1448 | private function getNeedSwitch() 1449 | { 1450 | $switchList = $this->server->table->get(0, "needswitch"); 1451 | return is_string($switchList) ? count(explode(";", $switchList)) : 0; 1452 | } 1453 | 1454 | /** 1455 | * 1456 | * GetTotalUsers 获取当前所有在线的客户端数量 1457 | * 1458 | */ 1459 | private function getTotalUsers() 1460 | { 1461 | return $this->server->connections ? count($this->server->connections) : 0; 1462 | } 1463 | 1464 | /** 1465 | * 1466 | * GetUserNickname 获取用户的昵称 1467 | * 1468 | */ 1469 | private function getUserNickname($ip) 1470 | { 1471 | $data = $this->getUserNickData(); 1472 | return $data[$ip] ?? false; 1473 | } 1474 | 1475 | /** 1476 | * 1477 | * GetUserNickData 获取所有用户的昵称数据 1478 | * 1479 | */ 1480 | private function getUserNickData() 1481 | { 1482 | $data = @file_get_contents(ROOT . "/username.json"); 1483 | $json = json_decode($data, true); 1484 | return $json ?? []; 1485 | } 1486 | 1487 | /** 1488 | * 1489 | * SetUserNickname 设置用户的昵称 1490 | * 1491 | */ 1492 | private function setUserNickname($ip, $name) 1493 | { 1494 | $data = $this->getUserNickData(); 1495 | $data[$ip] = $name; 1496 | $this->setUserNickData($data); 1497 | } 1498 | 1499 | /** 1500 | * 1501 | * SetUserNickData 将昵称数据写入到硬盘 1502 | * 1503 | */ 1504 | private function setUserNickData($data) 1505 | { 1506 | @file_put_contents(ROOT . "/username.json", json_encode($data)); 1507 | } 1508 | 1509 | /** 1510 | * 1511 | * SetBlackList 将黑名单数据写入到硬盘 1512 | * 1513 | */ 1514 | private function setBlackList($data) 1515 | { 1516 | $result = ""; 1517 | for($i = 0;$i < count($data);$i++) { 1518 | $result .= $data[$i] . "\n"; 1519 | } 1520 | @file_put_contents(ROOT . "/blacklist.txt", $result); 1521 | } 1522 | 1523 | /** 1524 | * 1525 | * SetLastChat 设置客户端的最后发言时间 1526 | * 1527 | */ 1528 | private function setLastChat($id, $time = 0) 1529 | { 1530 | $this->server->chats->set($id, ["last" => $time]); 1531 | } 1532 | 1533 | /** 1534 | * 1535 | * SetMusicList 设置等待播放的音乐列表 1536 | * 1537 | */ 1538 | private function setMusicList($data) 1539 | { 1540 | if(USE_REDIS) { 1541 | $redis = new Redis(); 1542 | $redis->connect(REDIS_HOST, REDIS_PORT); 1543 | if(!empty(REDIS_PASS)) { 1544 | $redis->auth(REDIS_PASS); 1545 | } 1546 | $redis->set("syncmusic-list", json_encode($data)); 1547 | } else { 1548 | $this->server->table->set(0, ["music_list" => json_encode($data)]); 1549 | } 1550 | } 1551 | 1552 | /** 1553 | * 1554 | * SetMusicShow 设置用于网页显示的音乐列表 1555 | * 1556 | */ 1557 | private function setMusicShow($data) 1558 | { 1559 | if(USE_REDIS) { 1560 | $redis = new Redis(); 1561 | $redis->connect(REDIS_HOST, REDIS_PORT); 1562 | if(!empty(REDIS_PASS)) { 1563 | $redis->auth(REDIS_PASS); 1564 | } 1565 | $redis->set("syncmusic-show", json_encode($data)); 1566 | } else { 1567 | $this->server->table->set(0, ["music_show" => json_encode($data)]); 1568 | } 1569 | } 1570 | 1571 | /** 1572 | * 1573 | * SetMusicTime 设置音乐播放的结束时间 1574 | * 1575 | */ 1576 | private function setMusicTime($data) 1577 | { 1578 | $this->server->table->set(0, ["music_time" => $data]); 1579 | } 1580 | 1581 | /** 1582 | * 1583 | * SetMusicLong 设置音乐播放的开始时间 1584 | * 1585 | */ 1586 | private function setMusicLong($data) 1587 | { 1588 | $this->server->table->set(0, ["music_long" => $data]); 1589 | } 1590 | 1591 | /** 1592 | * 1593 | * SetMusicPlay 设置音乐已经播放的时间 1594 | * 1595 | */ 1596 | private function setMusicPlay($data) 1597 | { 1598 | $this->server->table->set(0, ["music_play" => $data]); 1599 | } 1600 | 1601 | /** 1602 | * 1603 | * SetSavedMusicList 将等待播放的音乐列表储存到硬盘 1604 | * 1605 | */ 1606 | private function setSavedMusicList() 1607 | { 1608 | @file_put_contents(ROOT . "/musiclist.json", $this->server->table->get(0, "music_list")); 1609 | } 1610 | 1611 | /** 1612 | * 1613 | * SetSavedMusicShow 将用于显示在网页上的音乐列表储存到硬盘 1614 | * 1615 | */ 1616 | private function setSavedMusicShow($data) 1617 | { 1618 | @file_put_contents(ROOT . "/musicshow.json", $this->server->table->get(0, "music_show")); 1619 | } 1620 | 1621 | /** 1622 | * 1623 | * SetAdminIp 设置管理员的 IP 地址 1624 | * 1625 | */ 1626 | private function setAdminIp($ip) 1627 | { 1628 | @file_put_contents(ROOT . "/admin.ip", $ip); 1629 | } 1630 | 1631 | /** 1632 | * 1633 | * SetBannedIp 设置被封禁的 IP 列表 1634 | * 1635 | */ 1636 | private function setBannedIp($ip) 1637 | { 1638 | $this->server->table->set(0, ["banned_ips" => $ip]); 1639 | } 1640 | 1641 | /** 1642 | * 1643 | * SetNeedSwitch 设置需要投票切歌的用户列表 1644 | * 1645 | */ 1646 | private function setNeedSwitch($data) 1647 | { 1648 | $this->server->table->set(0, ["needswitch" => $data]); 1649 | } 1650 | 1651 | /** 1652 | * 1653 | * FetchMusicApi 搜索指定关键字的音乐 1654 | * 1655 | */ 1656 | private function fetchMusicApi($keyWord) 1657 | { 1658 | $keyWord = urlencode($keyWord); 1659 | echo $this->debug ? $this->consoleLog("Http Request >> {$this->musicApi}/api.php?source=netease&types=search&name={$keyWord}&count=1&pages=1", 0) : ""; 1660 | $rawdata = @file_get_contents("{$this->musicApi}/api.php?source=netease&types=search&name={$keyWord}&count=1&pages=1"); 1661 | echo $this->debug ? $this->consoleLog("Http Request << {$rawdata}", 0) : ""; 1662 | return json_decode($rawdata, true); 1663 | } 1664 | 1665 | /** 1666 | * 1667 | * FetchMusic 读取音乐文件内容 1668 | * 1669 | */ 1670 | private function fetchMusic($m, $download = '') 1671 | { 1672 | if(!file_exists(ROOT . "/tmp/{$m['id']}.mp3")) { 1673 | $this->consoleLog("歌曲 {$m['name']} 不存在,下载中...", 1, true); 1674 | $musicFile = @file_get_contents($download); 1675 | $this->consoleLog("歌曲 {$m['name']} 下载完成。", 1, true); 1676 | @file_put_contents(ROOT . "/tmp/{$m['id']}.mp3", $musicFile); 1677 | } else { 1678 | $musicFile = @file_get_contents(ROOT . "/tmp/{$m['id']}.mp3"); 1679 | } 1680 | return $musicFile; 1681 | } 1682 | 1683 | /** 1684 | * 1685 | * ConsoleLog 控制台输出日志 1686 | * 1687 | */ 1688 | private function consoleLog($data, $level = 1, $directOutput = false) 1689 | { 1690 | $msgData = "[" . date("Y-m-d H:i:s") . " " . $this->getLoggerLevel($level) . "] {$data}\n"; 1691 | if($directOutput) { 1692 | echo $msgData; 1693 | } else { 1694 | return $msgData; 1695 | } 1696 | } 1697 | } 1698 | -------------------------------------------------------------------------------- /conf/syncmusic.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | location / { 4 | root /var/www/; 5 | location ~ ^(.+\.php)(.*)$ { 6 | fastcgi_index index.php; 7 | fastcgi_pass 127.0.0.1:9000; 8 | 9 | include fastcgi.conf; 10 | 11 | fastcgi_split_path_info ^(.+\.php)(.*)$; 12 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 13 | fastcgi_param PATH_INFO $fastcgi_path_info; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | syncmusic: 4 | build: . 5 | image: syncmusic 6 | ports: 7 | - "80:80" 8 | - "811:811" 9 | volumes: 10 | - .:/var/www 11 | - ./conf/syncmusic.conf:/etc/nginx/conf.d/default.conf 12 | depends_on: 13 | - redis 14 | redis: 15 | image: redis:alpine 16 | -------------------------------------------------------------------------------- /face.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Image List 8 | 9 | 10 | 11 |
12 | 13 |

表情包

14 |

点击对应表情,然后复制输入框的内容即可

15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |
54 |
55 | 56 | 61 | -------------------------------------------------------------------------------- /face/1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kasuganosoras/SyncMusic/95a25543a9002ed2d72801996258159e1b016026/face/1.gif -------------------------------------------------------------------------------- /face/10.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kasuganosoras/SyncMusic/95a25543a9002ed2d72801996258159e1b016026/face/10.gif -------------------------------------------------------------------------------- /face/11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kasuganosoras/SyncMusic/95a25543a9002ed2d72801996258159e1b016026/face/11.jpg -------------------------------------------------------------------------------- /face/12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kasuganosoras/SyncMusic/95a25543a9002ed2d72801996258159e1b016026/face/12.jpg -------------------------------------------------------------------------------- /face/13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kasuganosoras/SyncMusic/95a25543a9002ed2d72801996258159e1b016026/face/13.jpg -------------------------------------------------------------------------------- /face/14.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kasuganosoras/SyncMusic/95a25543a9002ed2d72801996258159e1b016026/face/14.jpg -------------------------------------------------------------------------------- /face/15.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kasuganosoras/SyncMusic/95a25543a9002ed2d72801996258159e1b016026/face/15.jpg -------------------------------------------------------------------------------- /face/16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kasuganosoras/SyncMusic/95a25543a9002ed2d72801996258159e1b016026/face/16.jpg -------------------------------------------------------------------------------- /face/17.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kasuganosoras/SyncMusic/95a25543a9002ed2d72801996258159e1b016026/face/17.jpg -------------------------------------------------------------------------------- /face/18.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kasuganosoras/SyncMusic/95a25543a9002ed2d72801996258159e1b016026/face/18.jpg -------------------------------------------------------------------------------- /face/19.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kasuganosoras/SyncMusic/95a25543a9002ed2d72801996258159e1b016026/face/19.jpg -------------------------------------------------------------------------------- /face/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kasuganosoras/SyncMusic/95a25543a9002ed2d72801996258159e1b016026/face/2.jpg -------------------------------------------------------------------------------- /face/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kasuganosoras/SyncMusic/95a25543a9002ed2d72801996258159e1b016026/face/20.png -------------------------------------------------------------------------------- /face/21.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kasuganosoras/SyncMusic/95a25543a9002ed2d72801996258159e1b016026/face/21.jpg -------------------------------------------------------------------------------- /face/22.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kasuganosoras/SyncMusic/95a25543a9002ed2d72801996258159e1b016026/face/22.gif -------------------------------------------------------------------------------- /face/23.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kasuganosoras/SyncMusic/95a25543a9002ed2d72801996258159e1b016026/face/23.jpg -------------------------------------------------------------------------------- /face/24.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kasuganosoras/SyncMusic/95a25543a9002ed2d72801996258159e1b016026/face/24.jpg -------------------------------------------------------------------------------- /face/25.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kasuganosoras/SyncMusic/95a25543a9002ed2d72801996258159e1b016026/face/25.jpg -------------------------------------------------------------------------------- /face/26.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kasuganosoras/SyncMusic/95a25543a9002ed2d72801996258159e1b016026/face/26.jpg -------------------------------------------------------------------------------- /face/27.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kasuganosoras/SyncMusic/95a25543a9002ed2d72801996258159e1b016026/face/27.jpg -------------------------------------------------------------------------------- /face/28.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kasuganosoras/SyncMusic/95a25543a9002ed2d72801996258159e1b016026/face/28.jpg -------------------------------------------------------------------------------- /face/29.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kasuganosoras/SyncMusic/95a25543a9002ed2d72801996258159e1b016026/face/29.jpg -------------------------------------------------------------------------------- /face/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kasuganosoras/SyncMusic/95a25543a9002ed2d72801996258159e1b016026/face/3.png -------------------------------------------------------------------------------- /face/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kasuganosoras/SyncMusic/95a25543a9002ed2d72801996258159e1b016026/face/4.jpg -------------------------------------------------------------------------------- /face/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kasuganosoras/SyncMusic/95a25543a9002ed2d72801996258159e1b016026/face/5.png -------------------------------------------------------------------------------- /face/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kasuganosoras/SyncMusic/95a25543a9002ed2d72801996258159e1b016026/face/6.png -------------------------------------------------------------------------------- /face/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kasuganosoras/SyncMusic/95a25543a9002ed2d72801996258159e1b016026/face/7.png -------------------------------------------------------------------------------- /face/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kasuganosoras/SyncMusic/95a25543a9002ed2d72801996258159e1b016026/face/8.jpg -------------------------------------------------------------------------------- /face/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kasuganosoras/SyncMusic/95a25543a9002ed2d72801996258159e1b016026/face/9.jpg -------------------------------------------------------------------------------- /getlength.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import mutagen.mp3 4 | from mutagen.mp3 import MP3 5 | if len(sys.argv) < 2: 6 | print("Music file name cannot be empty!") 7 | exit(1) 8 | if os.path.isfile(sys.argv[1]): 9 | try: 10 | audio = MP3(sys.argv[1]) 11 | print(audio.info.length) 12 | except mutagen.mp3.HeaderNotFoundError: 13 | print(0) 14 | else: 15 | print(0) 16 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | SyncMusic - 在线点歌 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 |
36 |
37 | 38 | 39 | 40 | 41 |
42 | 50 | 73 |
74 |
75 |
76 |
77 | 78 |
79 |
80 |
正在播放
81 |

Loading...

82 |
83 |
歌词
84 |

Loading...

85 |

00:00:0000:00:00

86 |
87 |
88 |
89 |

90 | 音量控制    91 | 95 |

96 |
97 |
98 |
99 |
播放列表
100 |

以下为等待播放的歌曲

101 |
102 |
103 |
104 |
106 |
107 |
108 |
实时聊天
109 |

当前一共在线 0

110 |
111 |
112 | 113 | 114 | 115 | 116 |
117 |

118 | [ 发送表情 ]   119 | [ 投票切歌 ]   120 | [ 搜索音乐 ] 121 |

122 |

提示:输入 “点歌 歌名” 即可点歌。
例如输入:点歌 See You Again
支持输入网易云音乐 ID 来点歌

123 |

遇到不好听的歌可以输入 “投票切歌”,当投票人数超过在线人数的 30% 时就会切歌。

124 |

点播过于难听的歌曲例如 LostRiver 等将会被管理员切歌或从播放列表删除。

125 |

输入 “设置昵称 名字” 即可设置自己的显示昵称,仅限当前客户端的 IP 地址有效。

126 | 127 | 128 |
129 |
130 |
131 | 132 | 133 | 134 | 387 | 388 | -------------------------------------------------------------------------------- /random.txt: -------------------------------------------------------------------------------- 1 | 418602088 2 | 519265246 3 | 536243886 4 | 29459692 5 | 545947135 6 | 36990266 7 | 515452048 8 | 526472185 9 | 29910080 10 | 515540639 11 | 528273377 12 | 1333160781 13 | 478056436 14 | 515453363 15 | 476324207 16 | 472141627 17 | 506092035 18 | 507134193 19 | 1347814906 20 | 1355149745 21 | 1296410418 22 | 529557738 23 | 1312710726 24 | 29732106 25 | 415792222 26 | 26292065 27 | 493394429 28 | 1309975421 29 | 538470558 30 | 540333737 31 | 31877683 32 | 543987451 33 | 31010713 34 | 1310022346 35 | 572763271 36 | 447925342 37 | 1338119851 38 | 437909145 39 | 546722490 40 | 1301572562 41 | 1348792318 42 | 1344353899 43 | 1347684016 44 | 1314777725 45 | 1331192365 46 | 441116579 47 | 543203947 48 | 1320908673 49 | 33522489 50 | 522353191 51 | 1334248838 52 | 487375361 53 | 547976140 54 | 29497338 55 | 347230 56 | 186436 57 | 191232 58 | 212233 59 | 298880 60 | 316938 61 | 66282 62 | 32705017 63 | 110146 64 | 65766 65 | 65800 66 | 513791211 67 | 166432 68 | 409916250 69 | 518781004 70 | 477933952 71 | 571553649 72 | 1355197518 73 | 468513829 74 | 1369798757 75 | 27888500 76 | 479422643 77 | 526412160 78 | 28250504 79 | 420125895 80 | 406318298 81 | 392605 82 | 97357 83 | 167975 84 | 1381049131 85 | 1397674264 86 | 86369 87 | 569213220 88 | 1359595520 89 | 1363948882 90 | 1399533630 91 | 411214279 92 | 347230 93 | 31654343 94 | 1392990601 95 | 529823971 96 | 1305366556 97 | 1334295185 98 | 65766 99 | 1396973729 100 | 426291544 101 | 36270426 102 | 1362247767 103 | 1361348080 104 | 482988834 105 | 25706282 106 | 483671599 107 | 191528 108 | 1330348068 109 | 65800 110 | 522510615 111 | 1313354324 112 | 31445554 113 | 518725853 114 | 442869203 115 | 167876 116 | 29019227 117 | 169185 118 | 1376142151 119 | 202373 120 | 1335350269 121 | 28639182 122 | 63650 123 | 542690276 124 | 1357785909 125 | 1296583188 126 | 502043537 127 | 1365898499 128 | 490407216 129 | 410801653 130 | 25727803 131 | 1365221826 132 | 1308818967 133 | 1391639224 134 | 472045959 135 | 1300994613 136 | 29947420 137 | 1395252835 138 | 536502758 139 | 1313107065 140 | 509313150 141 | 1357999894 142 | 333750 143 | 480353 144 | 1384570191 145 | 1355147933 146 | 501133798 147 | 1383927341 148 | 308353 149 | 1381761209 150 | 1386056380 151 | 1325896149 152 | 1359356908 153 | 224000 154 | 287063 155 | 28907016 156 | 26830207 157 | 27946894 158 | 5308028 159 | 33937527 160 | 4341314 161 | 31654455 162 | 38019092 163 | 29717271 164 | 22212233 165 | 2866921 166 | 4940920 167 | 4017240 168 | 1385367710 169 | 513791211 170 | 1383001696 171 | 5260494 172 | 1373228477 173 | 2740360 174 | 450853439 175 | 1381755293 176 | 424264505 177 | 461083054 178 | 506196018 179 | 557584658 180 | 31654478 181 | 440208476 182 | 452986458 183 | 33211676 184 | 461347998 185 | 26092806 186 | 505449407 187 | 3935139 188 | 26199445 189 | 139774 190 | 37653063 191 | 31356499 192 | 29750825 193 | 19542337 194 | 536622304 195 | 38592976 196 | 33911781 197 | 586299 198 | 496869422 199 | 423228325 200 | 472361096 201 | -------------------------------------------------------------------------------- /search.php: -------------------------------------------------------------------------------- 1 |

无搜索结果

"); 13 | } 14 | } else { 15 | exit("

未输入搜索内容

"); 16 | } 17 | function getArtists($data) { 18 | if(count($data) > 1) { 19 | $artists = ""; 20 | foreach($data as $artist) { 21 | $artists .= $artist . ","; 22 | } 23 | $artists = $artists == "" ? "未知歌手" : mb_substr($artists, 0, mb_strlen($artists) - 1); 24 | } else { 25 | $artists = $data[0]; 26 | } 27 | return $artists; 28 | } 29 | ?> 30 | 31 | 32 | 33 | 34 | 35 | 36 | SyncMusic - 在线点歌 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 52 | 53 | 54 | 55 | "; 56 | } 57 | ?> 58 |
歌名歌手专辑
{$music['name']}" . getArtists($music['artist']) . "{$music['album']}
59 | 60 | 61 | 62 | 76 | 77 | -------------------------------------------------------------------------------- /server.php: -------------------------------------------------------------------------------- 1 | init(); 83 | $syncMusic->run(); 84 | --------------------------------------------------------------------------------