├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── db.sqlite3 ├── db.sqlite3.bak ├── docker-compose.yml ├── manage.py ├── music ├── __init__.py ├── admin.py ├── apps.py ├── decorators.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20200414_1715.py │ ├── 0003_auto_20200416_1901.py │ ├── 0004_auto_20200416_2335.py │ ├── 0005_auto_20200418_2245.py │ └── __init__.py ├── models.py ├── recommend.py ├── subscribe.py ├── tests.py └── views.py ├── project ├── __init__.py ├── asgi.py ├── settings.py ├── temp.py ├── urls.py └── wsgi.py ├── requirements.txt ├── static ├── css │ ├── bootstrap.css │ ├── bootstrap.min.css │ ├── jquery.lighter.css │ └── signin.css ├── fonts │ ├── icomoon.eot │ ├── icomoon.svg │ ├── icomoon.ttf │ └── icomoon.woff ├── js │ ├── bootstrap.min.js │ ├── jquery.lighter.js │ ├── jquery.min.js │ └── popper.min.js └── player │ ├── css │ ├── fonts │ │ ├── icomoon.eot │ │ ├── icomoon.svg │ │ ├── icomoon.ttf │ │ └── icomoon.woff │ ├── main.css │ ├── main2.css │ ├── player.css │ ├── player.less │ ├── player.min.css │ └── player2.css │ ├── images │ ├── bg.jpg │ └── icon.png │ ├── js │ ├── player.js │ ├── player.min.js │ └── player2.js │ └── music │ ├── Kandy.mp3 │ └── PaperGangsta.mp3 ├── temp ├── __init__.py ├── genre_proc.py ├── genres.txt ├── remove_language_empty_char.py ├── replace_genre_lang.py └── temp │ ├── __init__.py │ ├── genre_proc.py │ ├── genres.txt │ ├── remove_language_empty_char.py │ └── replace_genre_lang.py └── templates ├── base.html ├── list.html ├── play.html ├── play2.html ├── play3.html ├── sign_in.html ├── sign_up.html └── user.html /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | data/ 3 | 4 | # Created by .ignore support plugin (hsz.mobi) 5 | ### JupyterNotebooks template 6 | # gitignore template for Jupyter Notebooks 7 | # website: http://jupyter.org/ 8 | 9 | .ipynb_checkpoints 10 | */.ipynb_checkpoints/* 11 | 12 | # IPython 13 | profile_default/ 14 | ipython_config.py 15 | 16 | # Remove previous ipynb_checkpoints 17 | # git rm -r .ipynb_checkpoints/ 18 | 19 | ### Python template 20 | # Byte-compiled / optimized / DLL files 21 | __pycache__/ 22 | *.py[cod] 23 | *$py.class 24 | 25 | # C extensions 26 | *.so 27 | 28 | # Distribution / packaging 29 | .Python 30 | build/ 31 | develop-eggs/ 32 | dist/ 33 | downloads/ 34 | eggs/ 35 | .eggs/ 36 | lib/ 37 | lib64/ 38 | parts/ 39 | sdist/ 40 | var/ 41 | wheels/ 42 | pip-wheel-metadata/ 43 | share/python-wheels/ 44 | *.egg-info/ 45 | .installed.cfg 46 | *.egg 47 | MANIFEST 48 | 49 | # PyInstaller 50 | # Usually these files are written by a python script from a template 51 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 52 | *.manifest 53 | *.spec 54 | 55 | # Installer logs 56 | pip-log.txt 57 | pip-delete-this-directory.txt 58 | 59 | # Unit test / coverage reports 60 | htmlcov/ 61 | .tox/ 62 | .nox/ 63 | .coverage 64 | .coverage.* 65 | .cache 66 | nosetests.xml 67 | coverage.xml 68 | *.cover 69 | *.py,cover 70 | .hypothesis/ 71 | .pytest_cache/ 72 | cover/ 73 | 74 | # Translations 75 | *.mo 76 | *.pot 77 | 78 | # Django stuff: 79 | *.log 80 | local_settings.py 81 | 82 | 83 | # Flask stuff: 84 | instance/ 85 | .webassets-cache 86 | 87 | # Scrapy stuff: 88 | .scrapy 89 | 90 | # Sphinx documentation 91 | docs/_build/ 92 | 93 | # PyBuilder 94 | .pybuilder/ 95 | target/ 96 | 97 | # Jupyter Notebook 98 | .ipynb_checkpoints 99 | 100 | # IPython 101 | profile_default/ 102 | ipython_config.py 103 | 104 | # pyenv 105 | # For a library or package, you might want to ignore these files since the code is 106 | # intended to run in multiple environments; otherwise, check them in: 107 | # .python-version 108 | 109 | # pipenv 110 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 111 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 112 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 113 | # install all needed dependencies. 114 | #Pipfile.lock 115 | 116 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 117 | __pypackages__/ 118 | 119 | # Celery stuff 120 | celerybeat-schedule 121 | celerybeat.pid 122 | 123 | # SageMath parsed files 124 | *.sage.py 125 | 126 | # Environments 127 | .env 128 | .venv 129 | env/ 130 | venv/ 131 | ENV/ 132 | env.bak/ 133 | venv.bak/ 134 | 135 | # Spyder project settings 136 | .spyderproject 137 | .spyproject 138 | 139 | # Rope project settings 140 | .ropeproject 141 | 142 | # mkdocs documentation 143 | /site 144 | 145 | # mypy 146 | .mypy_cache/ 147 | .dmypy.json 148 | dmypy.json 149 | 150 | # Pyre type checker 151 | .pyre/ 152 | 153 | # pytype static type analyzer 154 | .pytype/ 155 | 156 | # Cython debug symbols 157 | cython_debug/ 158 | 159 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7 2 | 3 | # 设置 python 环境变量 4 | ENV PYTHONUNBUFFERED 1 5 | 6 | # 创建 code 文件夹并将其设置为工作目录 7 | RUN mkdir /code 8 | WORKDIR /code 9 | 10 | # 更新 pip 11 | RUN pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pip -U 12 | # 设置清华源 13 | RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple 14 | 15 | # 将 requirements.txt 复制到容器的 code 目录 16 | ADD requirements.txt /code/ 17 | 18 | # 安装库 19 | RUN pip install -r requirements.txt 20 | 21 | # 将当前目录复制到容器的 code 目录 22 | ADD . /code/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UserProfile_MusicRecommend 2 | 基于用户画像以及协同过滤的音乐推荐系统 3 | 4 | 5 | 1.将基于用户的协同过滤算法与用户画像相结合进行推荐,提高推荐列表数据的成熟度。 6 | 7 | 8 | 2.系统在Windows平台上搭建,采用Python3实现各项功能;采取MySQL进行数据的存储,通过Django框架连接系统的前、后端。 9 | 10 | 11 | 3.使用的数据集为kaggle平台上kkbox举办的—KKBox's Music Recommendation Challenge比赛的公开数据集,kkbox是亚洲领先的音乐流媒体服务提供商, 12 | 拥有世界上最全面的亚洲流行音乐库,拥有超过3000万首音乐曲目。 13 | 14 | 15 | 4.针对数据集使用SVD矩阵分解进行相似相关度的计算分析,根据已有的评分情况, 16 | 分析出评分者对各个因子的喜好程度以及歌曲包含各个因子的程度,最后再反过来根据分析结果预测评分,根据评分的结果生成推荐列表。 17 | 18 | -------------------------------------------------------------------------------- /db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mango-wyc/UserProfile_MusicRecommend/fa9f99412927af88401a324fa27de8dd4bb2528e/db.sqlite3 -------------------------------------------------------------------------------- /db.sqlite3.bak: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mango-wyc/UserProfile_MusicRecommend/fa9f99412927af88401a324fa27de8dd4bb2528e/db.sqlite3.bak -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | web: 4 | restart: always # 除正常工作外,容器会在任何时候重启,比如遭遇 bug、进程崩溃、docker 重启等情况。 5 | build: . 6 | environment: 7 | - ENVIRONMENT=docker 8 | command: python3 manage.py runserver 0.0.0.0:8001 9 | volumes: 10 | - .:/code 11 | ports: 12 | - "8001:8001" 13 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /music/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mango-wyc/UserProfile_MusicRecommend/fa9f99412927af88401a324fa27de8dd4bb2528e/music/__init__.py -------------------------------------------------------------------------------- /music/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Music, UserProfile 3 | 4 | 5 | # Register your models here. 6 | @admin.register(Music) 7 | class MusicAdmin(admin.ModelAdmin): 8 | list_display = ['song_name', 'song_length', 'genre_ids', 'artist_name', 'composer', 'lyricist', 'language'] 9 | 10 | 11 | @admin.register(UserProfile) 12 | class UserProfileAdmin(admin.ModelAdmin): 13 | list_display = ['pk', 'user_id', 'user', 'first_run', 'genre_subscribe', 'language_subscribe'] 14 | 15 | def user_id(self, obj: UserProfile): 16 | return obj.user.id 17 | -------------------------------------------------------------------------------- /music/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MusicConfig(AppConfig): 5 | name = 'music' 6 | -------------------------------------------------------------------------------- /music/decorators.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.contrib.auth import logout 3 | from django.http import HttpRequest 4 | from django.http import HttpResponseRedirect 5 | from music.models import UserProfile 6 | 7 | 8 | def cold_boot(function): 9 | def \ 10 | wrapper(request: HttpRequest, *args, **kwargs): 11 | if request.user.is_authenticated: 12 | profile = UserProfile.objects.filter(user=request.user) 13 | if profile.exists(): 14 | profile_obj: UserProfile = profile.first() 15 | if profile_obj.first_run: 16 | messages.warning(request, '首次登录请先订阅喜欢的音乐流派和语言') 17 | return HttpResponseRedirect('/user') 18 | else: 19 | messages.error(request, '找不到用户资料,请重新登录') 20 | logout(request) 21 | return HttpResponseRedirect('/') 22 | 23 | return function(request, *args, **kwargs) 24 | 25 | return wrapper 26 | -------------------------------------------------------------------------------- /music/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.3 on 2020-04-13 22:23 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Music', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('song_name', models.CharField(max_length=200, verbose_name='歌曲名称')), 19 | ('song_length', models.IntegerField(verbose_name='歌曲长度 单位为ms')), 20 | ('genre_ids', models.CharField(max_length=10, verbose_name='歌曲流派')), 21 | ('artist_name', models.CharField(max_length=200, verbose_name='歌手')), 22 | ('composer', models.CharField(max_length=200, verbose_name='作曲')), 23 | ('lyricist', models.CharField(max_length=200, verbose_name='作词')), 24 | ('language', models.CharField(max_length=20, verbose_name='语种')), 25 | ], 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /music/migrations/0002_auto_20200414_1715.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-04-14 17:15 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('music', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterModelOptions( 17 | name='music', 18 | options={'verbose_name': '音乐', 'verbose_name_plural': '音乐'}, 19 | ), 20 | migrations.CreateModel( 21 | name='UserProfile', 22 | fields=[ 23 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 24 | ('dislikes', models.ManyToManyField(blank=True, related_name='dislike_users', to='music.Music')), 25 | ('likes', models.ManyToManyField(blank=True, related_name='like_users', to='music.Music')), 26 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 27 | ], 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /music/migrations/0003_auto_20200416_1901.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-04-16 19:01 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('music', '0002_auto_20200414_1715'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='userprofile', 15 | options={'verbose_name': '用户资料', 'verbose_name_plural': '用户资料'}, 16 | ), 17 | migrations.AddField( 18 | model_name='userprofile', 19 | name='first_run', 20 | field=models.BooleanField(default=True, verbose_name='是否第一次运行 执行冷启动策略'), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /music/migrations/0004_auto_20200416_2335.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-04-16 23:35 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('music', '0003_auto_20200416_1901'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='userprofile', 15 | name='genre_subscribe', 16 | field=models.TextField(blank=True, verbose_name='流派订阅'), 17 | ), 18 | migrations.AddField( 19 | model_name='userprofile', 20 | name='language_subscribe', 21 | field=models.TextField(blank=True, verbose_name='语言订阅'), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /music/migrations/0005_auto_20200418_2245.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-04-18 22:45 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('music', '0004_auto_20200416_2335'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='music', 15 | name='genre_ids', 16 | field=models.CharField(max_length=100, verbose_name='歌曲流派'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /music/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mango-wyc/UserProfile_MusicRecommend/fa9f99412927af88401a324fa27de8dd4bb2528e/music/migrations/__init__.py -------------------------------------------------------------------------------- /music/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import User 3 | 4 | 5 | # Create your models here. 6 | class UserProfile(models.Model): 7 | user = models.OneToOneField(User, on_delete=models.CASCADE) 8 | likes = models.ManyToManyField('Music', blank=True, related_name='like_users') 9 | dislikes = models.ManyToManyField('Music', blank=True, related_name='dislike_users') 10 | first_run = models.BooleanField('是否第一次运行 执行冷启动策略', default=True) 11 | genre_subscribe = models.TextField('流派订阅', blank=True) 12 | language_subscribe = models.TextField('语言订阅', blank=True) 13 | 14 | def __str__(self): 15 | return self.user.username 16 | 17 | class Meta: 18 | verbose_name = '用户资料' 19 | verbose_name_plural = verbose_name 20 | 21 | 22 | class Music(models.Model): 23 | song_name = models.CharField('歌曲名称', max_length=200) 24 | song_length = models.IntegerField('歌曲长度 单位为ms') 25 | genre_ids = models.CharField('歌曲流派', max_length=100) 26 | artist_name = models.CharField('歌手', max_length=200) 27 | composer = models.CharField('作曲', max_length=200) 28 | lyricist = models.CharField('作词', max_length=200) 29 | language = models.CharField('语种', max_length=20) 30 | 31 | def __str__(self): 32 | return self.song_name 33 | 34 | class Meta: 35 | verbose_name = '音乐' 36 | verbose_name_plural = verbose_name 37 | -------------------------------------------------------------------------------- /music/recommend.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from django.contrib import messages 3 | from django.http import HttpRequest 4 | from surprise import SVD, KNNBasic 5 | from surprise import Dataset, Reader, Prediction 6 | 7 | import os 8 | import django 9 | 10 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings") 11 | django.setup() 12 | 13 | from django.contrib.auth.models import User 14 | from music.models import UserProfile, Music 15 | 16 | current_request = None 17 | 18 | 19 | def build_df(): 20 | data = [] 21 | for user_profile in UserProfile.objects.all(): 22 | for like_music in user_profile.likes.all(): 23 | data.append([user_profile.user.id, like_music.pk, 1]) 24 | for dislike_music in user_profile.dislikes.all(): 25 | data.append([user_profile.user.id, dislike_music.pk, 0]) 26 | 27 | return pd.DataFrame(data, columns=['userID', 'itemID', 'rating']) 28 | 29 | 30 | def build_predictions(df: pd.DataFrame, user: User): 31 | userId = user.id 32 | profile = UserProfile.objects.filter(user=user) 33 | if profile.exists(): 34 | profile_obj: UserProfile = profile.first() 35 | else: 36 | return [] 37 | 38 | # 先构建训练集,用SVD训练,再把所有评分过的歌曲放到测试集里, 39 | # 接着把测试集的数据通过训练的算法放到结果集里 40 | reader = Reader(rating_scale=(0, 1)) 41 | data = Dataset.load_from_df(df[['userID', 'itemID', 'rating']], reader) 42 | # 构建训练集 43 | trainset = data.build_full_trainset() 44 | # 构建算法并训练 e.g有限邻算法 45 | algo = SVD() 46 | # 数据拟合 47 | algo.fit(trainset) 48 | 49 | # 取出当前所有有人评分过的歌曲 50 | subsets = df[['itemID']].drop_duplicates() 51 | # 测试集 52 | testset = [] 53 | for row in subsets.iterrows(): 54 | testset.append([userId, row[1].values[0], 0]) 55 | #通过测试集构建预测集 56 | predictions = algo.test(testset, verbose=True) 57 | result_set = [] 58 | 59 | user_like = profile_obj.likes.all() 60 | user_dislike = profile_obj.dislikes.all() 61 | 62 | for item in predictions: 63 | prediction: Prediction = item 64 | if prediction.est > 0.99: 65 | music = Music.objects.get(pk=prediction.iid) 66 | # 去重,不推荐用户已经喜欢的 或不喜欢的音乐 67 | if music in user_like: 68 | continue 69 | if music in user_dislike: 70 | continue 71 | result_set.append(music) 72 | 73 | if len(result_set) == 0: 74 | messages.error(current_request, '你听的歌太少了,多听点歌再来吧~') 75 | 76 | return result_set 77 | 78 | 79 | def build_genre_predictions(user: User): 80 | predictions = [] 81 | profile = UserProfile.objects.filter(user=user) 82 | if profile.exists(): 83 | profile_obj: UserProfile = profile.first() 84 | else: 85 | return predictions 86 | 87 | genre_subscribe = profile_obj.genre_subscribe.split(',') 88 | user_like = profile_obj.likes.all() 89 | user_dislike = profile_obj.dislikes.all() 90 | 91 | for music in Music.objects.filter(genre_ids__in=genre_subscribe): 92 | if music in user_like: 93 | continue 94 | if music in user_dislike: 95 | continue 96 | predictions.append(music) 97 | 98 | return predictions 99 | 100 | 101 | def build_language_predictions(user: User): 102 | predictions = [] 103 | profile = UserProfile.objects.filter(user=user) 104 | if profile.exists(): 105 | profile_obj: UserProfile = profile.first() 106 | else: 107 | return predictions 108 | 109 | language_subscribe = profile_obj.language_subscribe.split(',') 110 | user_like = profile_obj.likes.all() 111 | user_dislike = profile_obj.dislikes.all() 112 | 113 | for music in Music.objects.filter(language__in=language_subscribe): 114 | if music in user_like: 115 | continue 116 | if music in user_dislike: 117 | continue 118 | predictions.append(music) 119 | 120 | return predictions 121 | 122 | 123 | def build_recommend(request: HttpRequest, user: User): 124 | global current_request 125 | current_request = request 126 | predictions = [] 127 | predictions.extend(build_predictions(build_df(), user)) 128 | predictions.extend(build_genre_predictions(user)) 129 | predictions.extend(build_language_predictions(user)) 130 | 131 | return predictions 132 | 133 | 134 | if __name__ == '__main__': 135 | # print(build_df()) 136 | # print(build_predictions(build_df(), User.objects.get(pk=4))) 137 | # print(build_genre_predictions(User.objects.get(pk=2))) 138 | # print(build_language_predictions(User.objects.get(pk=2))) 139 | print(build_recommend(User.objects.get(pk=2))) 140 | -------------------------------------------------------------------------------- /music/subscribe.py: -------------------------------------------------------------------------------- 1 | import os 2 | import django 3 | 4 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings") 5 | django.setup() 6 | 7 | from music.models import UserProfile, Music 8 | 9 | language_labels = { 10 | '3.0': '华语', 11 | '31.0': '韩语', 12 | '52.0': '英语', 13 | '17.0': '日语', 14 | '10.0': '台语', 15 | '38.0': '台语', 16 | '-1.0': '纯音乐', 17 | '59.0': '纯音乐', 18 | '24.0': '粤语', 19 | '45.0': '粤语' 20 | } 21 | 22 | genre_labels = {"465": "朋克", "444": "蓝调", "458": "拉丁", "726": "蓝调", "864": "后摇", "857": "后摇", "850": "世界音乐", "843": "后摇", "1572": "古风", "275": "民族", "2122": "雷鬼", "359": "世界音乐", "338": "乡村", "303": "乡村", "1131": "乡村", 23 | "1089": "乡村", "1266": "乡村", "670": "乡村", "252": "乡村", "2238": "乡村", "1944": "乡村", "2029": "乡村", "1033": "乡村", "979": "乡村", "": "古风", "352": "英伦", "1169": "R&B/Soul", "282": "R&B/Soul", "416": "R&B/Soul", 24 | "2065": "R&B/Soul", "2245": "R&B/Soul", "656": "R&B/Soul", "1201": "R&B/Soul", "1007": "R&B/Soul", "2045": "R&B/Soul", "289": "R&B/Soul", "1995": "民族", "765": "古典", "2052": "古典", "2194": "古典", 25 | "2183": "古典", "2206": "古典", "502": "古典", "1988": "古典", "993": "古典", "986": "古典", "1110": "古典", "1047": "古典", "2157": "后摇", "95": "民族", "1054": "民族", "2248": "民族", "1162": "民族", "965": "民族", "198": "民族", 26 | "531": "民族", "2116": "民族", "751": "民族", "488": "民族", "808": "民族", "2032": "英伦", "1273": "英伦", "1145": "英伦", "212": "英伦", "972": "英伦", "2213": "英伦", "1026": "英伦", "177": "英伦", "2215": "英伦", "205": "英伦", 27 | "2144": "英伦", "1609": "朋克", "663": "金属", "1208": "金属", "2100": "金属", "1103": "金属", "381": "金属", "331": "金属", "1096": "金属", "509": "金属", "1040": "金属", "1981": "金属", "1011": "后摇", "698": "金属", "242": "拉丁", 28 | "139": "朋克", "873": "朋克", "1955": "朋克", "2022": "朋克", "786": "朋克", "947": "朋克", "1259": "朋克", "921": "朋克", "2107": "朋克", "451": "古风", "880": "蓝调", "481": "蓝调", "125": "蓝调", "109": "蓝调", "798": "蓝调", 29 | "1082": "蓝调", "545": "蓝调", "437": "蓝调", "829": "蓝调", "1969": "蓝调", "388": "蓝调", "94": "蓝调", "1152": "拉丁", "430": "古风", "409": "雷鬼", "893": "雷鬼", "1616": "雷鬼", "712": "雷鬼", "958": "拉丁", "940": "世界音乐", 30 | "2130": "雷鬼", "2086": "雷鬼", "1568": "雷鬼", "1138": "雷鬼", "474": "世界音乐", "1180": "世界音乐", "1068": "世界音乐", "423": "世界音乐", "184": "世界音乐", "744": "世界音乐", "691": "世界音乐", "2189": "世界音乐", "2072": "世界音乐", 31 | "1977": "世界音乐", "402": "拉丁", "1287": "拉丁", "2079": "拉丁", "2176": "拉丁", "1605": "拉丁", "1019": "拉丁", "719": "拉丁", "367": "拉丁", "374": "拉丁", "822": "古风", "2058": "古风", "1155": "古风", "1633": "古风", 32 | "310": "古风", "900": "古风", "2109": "古风", "1280": "古风", "2093": "古风", "1630": "古风", "118": "后摇", "516": "后摇", "907": "后摇", "102": "后摇", "1598": "后摇", "191": "后摇", "1124": "R&B/Soul"} 33 | 34 | 35 | def build_genre_ids(): 36 | data = {} 37 | genre_ids = Music.objects.values_list('genre_ids') 38 | 39 | for genre_id in genre_ids: 40 | if '|' not in genre_id[0]: 41 | genre = genre_id[0] 42 | data[genre] = True 43 | else: 44 | for genre in genre_id[0].split('|'): 45 | data[genre] = True 46 | 47 | if '' in data: 48 | del data[''] 49 | print(data.keys()) 50 | return list(data.keys()) 51 | 52 | 53 | def build_languages(): 54 | data = {} 55 | languages = Music.objects.values_list('language') 56 | 57 | for language in languages: 58 | if '|' not in language[0]: 59 | lang = language[0].strip() 60 | data[lang] = True 61 | else: 62 | for lang in language[0].split('|'): 63 | lang = lang.strip() 64 | data[lang] = True 65 | 66 | if '' in data: 67 | del data[''] 68 | return list(data.keys()) 69 | 70 | 71 | if __name__ == '__main__': 72 | # print(build_genre_ids()) 73 | 74 | print(build_languages()) 75 | -------------------------------------------------------------------------------- /music/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /music/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.contrib.auth import authenticate, login, logout 3 | from django.contrib.auth.decorators import login_required 4 | from django.contrib.auth.models import User 5 | from django.http import HttpResponseRedirect 6 | from django.core.paginator import Paginator 7 | from django.shortcuts import render, get_object_or_404 8 | from .models import Music, UserProfile 9 | from .recommend import build_df, build_recommend 10 | from .subscribe import build_genre_ids, build_languages 11 | from .decorators import cold_boot 12 | 13 | current_play = None 14 | 15 | current_recommend = [] 16 | 17 | 18 | def home(request): 19 | return all(request) 20 | 21 | 22 | def sign_up(request): 23 | if request.method == 'POST': 24 | username = request.POST.get('username') 25 | password = request.POST.get('password') 26 | if User.objects.filter(username=username).exists(): 27 | messages.add_message(request, messages.ERROR, '用户名已存在!') 28 | else: 29 | user_obj = User.objects.create_user(username=username, password=password) 30 | UserProfile.objects.create(user=user_obj) 31 | messages.add_message(request, messages.SUCCESS, '注册成功!') 32 | return HttpResponseRedirect('/') 33 | return render(request, 'sign_up.html') 34 | 35 | 36 | def sign_in(request): 37 | if request.method == 'POST': 38 | username = request.POST.get('username') 39 | password = request.POST.get('password') 40 | user = authenticate(request, username=username, password=password) 41 | if user is not None: 42 | login(request, user=user) 43 | messages.success(request, '登录成功') 44 | return HttpResponseRedirect('/') 45 | else: 46 | messages.add_message(request, messages.ERROR, '用户名或密码错误!') 47 | return render(request, 'sign_in.html') 48 | 49 | 50 | @login_required(login_url='/sign_in') 51 | def user_logout(request): 52 | logout(request) 53 | messages.info(request, '退出登录') 54 | return HttpResponseRedirect('/') 55 | 56 | 57 | @cold_boot 58 | def all(request): 59 | page_number = request.GET.get('page', 1) 60 | queryset = Music.objects.all() 61 | paginator = Paginator(queryset, 20) 62 | musics = paginator.page(page_number) 63 | context = { 64 | 'musics': musics, 65 | 'user_likes': [], 66 | 'user_dislikes': [] 67 | } 68 | if request.user.is_authenticated: 69 | user_profile = UserProfile.objects.filter(user=request.user) 70 | if user_profile.exists(): 71 | user_profile = user_profile.first() 72 | context['user_likes'] = user_profile.likes.all() 73 | context['user_dislikes'] = user_profile.dislikes.all() 74 | return render(request, 'list.html', context) 75 | 76 | 77 | @login_required(login_url='/sign_in') 78 | @cold_boot 79 | def recommend(request): 80 | page_number = request.GET.get('page', 1) 81 | recommend_set = build_recommend(request, request.user) 82 | paginator = Paginator(recommend_set, 20) 83 | musics = paginator.page(page_number) 84 | context = { 85 | 'musics': musics, 86 | 'user_likes': [], 87 | 'user_dislikes': [] 88 | } 89 | user_profile = UserProfile.objects.filter(user=request.user) 90 | if user_profile.exists(): 91 | user_profile = user_profile.first() 92 | context['user_likes'] = user_profile.likes.all() 93 | context['user_dislikes'] = user_profile.dislikes.all() 94 | return render(request, 'list.html', context) 95 | 96 | 97 | @login_required(login_url='/sign_in') 98 | def like(request, pk: int): 99 | user_obj = UserProfile.objects.get(user=request.user) 100 | music_obj = get_object_or_404(Music.objects.all(), pk=pk) 101 | user_obj.likes.add(music_obj) 102 | user_obj.dislikes.remove(music_obj) 103 | messages.add_message(request, messages.INFO, '已经添加到我喜欢') 104 | redirect_url = request.GET.get('from', '/') 105 | if 'action' in request.GET: 106 | redirect_url += f'&action={request.GET["action"]}' 107 | return HttpResponseRedirect(redirect_url) 108 | 109 | 110 | @login_required(login_url='/sign_in') 111 | def dislike(request, pk: int): 112 | user_obj = UserProfile.objects.get(user=request.user) 113 | music_obj = get_object_or_404(Music.objects.all(), pk=pk) 114 | user_obj.dislikes.add(music_obj) 115 | user_obj.likes.remove(music_obj) 116 | messages.add_message(request, messages.INFO, '已经添加到我不喜欢') 117 | redirect_url = request.GET.get('from', '/') 118 | if 'action' in request.GET: 119 | redirect_url += f'&action={request.GET["action"]}' 120 | return HttpResponseRedirect(redirect_url) 121 | 122 | 123 | def play(request, pk: int = 0): 124 | global current_play 125 | if pk > 0: 126 | music_obj = Music.objects.filter(pk=pk) 127 | if music_obj.exists(): 128 | current_play = music_obj.first() 129 | if current_play is None: 130 | messages.error(request, '当前没有正在播放的音乐') 131 | return HttpResponseRedirect('/') 132 | 133 | return render(request, 'play.html', context={ 134 | 'music': current_play 135 | }) 136 | 137 | 138 | @login_required(login_url='/sign_in') 139 | def user_center(request): 140 | user_profile = UserProfile.objects.filter(user=request.user) 141 | if user_profile.exists(): 142 | profile_obj: UserProfile = user_profile.first() 143 | else: 144 | messages.error(request, '找不到用户资料,请重新登录') 145 | logout(request) 146 | return HttpResponseRedirect('/') 147 | 148 | if request.method == 'POST': 149 | genres = request.POST.getlist('genres', '') 150 | languages = request.POST.getlist('languages', '') 151 | profile_obj.first_run = False 152 | 153 | if len(genres) > 0: 154 | profile_obj.genre_subscribe = ','.join(genres) 155 | profile_obj.save() 156 | messages.success(request, '修改流派订阅成功!') 157 | elif not profile_obj.first_run: 158 | profile_obj.genre_subscribe = '' 159 | profile_obj.save() 160 | messages.success(request, '修改流派订阅成功!') 161 | 162 | if len(languages) > 0: 163 | profile_obj.language_subscribe = ','.join(languages) 164 | profile_obj.save() 165 | messages.success(request, '修改语言订阅成功!') 166 | elif not profile_obj.first_run: 167 | profile_obj.language_subscribe = '' 168 | profile_obj.save() 169 | messages.success(request, '修改语言订阅成功!') 170 | 171 | context = { 172 | 'user_likes': profile_obj.likes.all(), 173 | 'user_dislikes': profile_obj.dislikes.all(), 174 | 'genres': build_genre_ids(), 175 | 'languages': build_languages(), 176 | 'genre_subscribe': profile_obj.genre_subscribe.split(','), 177 | 'language_subscribe': [] 178 | } 179 | 180 | # 去除空字符 181 | for lang in profile_obj.language_subscribe.split(','): 182 | lang = lang.strip() 183 | context['language_subscribe'].append(lang) 184 | 185 | return render(request, 'user.html', context=context) 186 | 187 | 188 | def search(request): 189 | if 'keyword' not in request.GET: 190 | messages.error(request, '请输入搜索关键词') 191 | return HttpResponseRedirect('/') 192 | 193 | keyword = request.GET.get('keyword') 194 | action = request.GET.get('action') 195 | 196 | musics = [] 197 | 198 | if action == 'song_name': 199 | musics = Music.objects.filter(song_name__contains=keyword) 200 | if action == 'artist_name': 201 | musics = Music.objects.filter(artist_name__contains=keyword) 202 | 203 | messages.info(request, f'搜索关键词:{keyword},找到 {len(musics)} 首音乐') 204 | context = { 205 | 'musics': musics, 206 | 'user_likes': [], 207 | 'user_dislikes': [] 208 | } 209 | if request.user.is_authenticated: 210 | user_profile = UserProfile.objects.filter(user=request.user) 211 | if user_profile.exists(): 212 | user_profile = user_profile.first() 213 | context['user_likes'] = user_profile.likes.all() 214 | context['user_dislikes'] = user_profile.dislikes.all() 215 | 216 | return render(request, 'list.html', context) 217 | -------------------------------------------------------------------------------- /project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mango-wyc/UserProfile_MusicRecommend/fa9f99412927af88401a324fa27de8dd4bb2528e/project/__init__.py -------------------------------------------------------------------------------- /project/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for music_recommend project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /project/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for music_recommend project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.0.3. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | from django.contrib import messages 17 | 18 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = '3pu%^(q6_w9s)@37ciy$lg8d6uwp@qdyw1meyuk^##_@7#+ry9' 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = True 28 | 29 | ALLOWED_HOSTS = ['*'] 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'grappelli', 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | 'music' 42 | ] 43 | 44 | MIDDLEWARE = [ 45 | 'django.middleware.security.SecurityMiddleware', 46 | 'django.contrib.sessions.middleware.SessionMiddleware', 47 | 'django.middleware.common.CommonMiddleware', 48 | 'django.middleware.csrf.CsrfViewMiddleware', 49 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 50 | 'django.contrib.messages.middleware.MessageMiddleware', 51 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 52 | ] 53 | 54 | ROOT_URLCONF = 'project.urls' 55 | 56 | TEMPLATES = [ 57 | { 58 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 59 | 'DIRS': [os.path.join(BASE_DIR, 'templates')] 60 | , 61 | 'APP_DIRS': True, 62 | 'OPTIONS': { 63 | 'context_processors': [ 64 | 'django.template.context_processors.debug', 65 | 'django.template.context_processors.request', 66 | 'django.contrib.auth.context_processors.auth', 67 | 'django.contrib.messages.context_processors.messages', 68 | ], 69 | }, 70 | }, 71 | ] 72 | 73 | WSGI_APPLICATION = 'project.wsgi.application' 74 | 75 | # Database 76 | # https://docs.djangoproject.com/en/3.0/ref/settings/#databases 77 | 78 | DATABASES = { 79 | 'default': { 80 | 'ENGINE': 'django.db.backends.sqlite3', 81 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 82 | }, 83 | 'mysql': { 84 | 'ENGINE': 'django.db.backends.mysql', # 数据库引擎 85 | 'NAME': 'music_recommdation', # 数据库名,先前创建的 86 | 'USER': 'root', # 用户名,可以自己创建用户 87 | 'PASSWORD': '123456', # 密码 88 | 'HOST': 'localhost', # mysql服务所在的主机ip 89 | 'PORT': '3306', # mysql服务端口 90 | } 91 | } 92 | 93 | # Password validation 94 | # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators 95 | 96 | AUTH_PASSWORD_VALIDATORS = [ 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 99 | }, 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 102 | }, 103 | { 104 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 105 | }, 106 | { 107 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 108 | }, 109 | ] 110 | 111 | # Internationalization 112 | # https://docs.djangoproject.com/en/3.0/topics/i18n/ 113 | 114 | LANGUAGE_CODE = 'zh-hans' 115 | 116 | TIME_ZONE = 'Asia/Shanghai' 117 | 118 | USE_I18N = True 119 | 120 | USE_L10N = True 121 | 122 | USE_TZ = False 123 | 124 | # Static files (CSS, JavaScript, Images) 125 | # https://docs.djangoproject.com/en/3.0/howto/static-files/ 126 | 127 | STATICFILES_DIRS = [ 128 | os.path.join(BASE_DIR, 'static') 129 | ] 130 | 131 | STATIC_ROOT = '' 132 | STATIC_URL = '/static/' 133 | 134 | MEDIA_ROOT = os.path.join(BASE_DIR, 'static', 'media') 135 | MEDIA_URL = '/media/' 136 | 137 | # messages 138 | 139 | MESSAGE_TAGS = { 140 | 0: 'primary', 141 | 1: 'light', 142 | 2: 'dark', 143 | 3: 'console', 144 | messages.DEBUG: 'secondary', 145 | messages.INFO: 'info', 146 | messages.SUCCESS: 'success', 147 | messages.WARNING: 'warning', 148 | messages.ERROR: 'danger', 149 | } 150 | -------------------------------------------------------------------------------- /project/temp.py: -------------------------------------------------------------------------------- 1 | # 去除language空字符 2 | 3 | import os 4 | import django 5 | 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings") 7 | django.setup() 8 | 9 | from music.models import UserProfile, Music 10 | 11 | if __name__ == '__main__': 12 | music_set = Music.objects.all() 13 | total = len(music_set) 14 | for index, music in enumerate(music_set): 15 | music.language = music.language.replace('\n', '') 16 | music.save() 17 | 18 | print(f'{index + 1}/{total}: {music.song_name}') 19 | -------------------------------------------------------------------------------- /project/urls.py: -------------------------------------------------------------------------------- 1 | """music_recommend URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.conf import settings 17 | from django.contrib import admin 18 | from django.urls import path, include 19 | from django.conf.urls.static import static 20 | from music import views 21 | 22 | urlpatterns = [ 23 | path('grappelli/', include('grappelli.urls')), # grappelli URLS 24 | path('admin/', admin.site.urls), 25 | path('', views.home), 26 | path('recommend', views.recommend), 27 | path('sign_in', views.sign_in), 28 | path('sign_up', views.sign_up), 29 | path('logout', views.user_logout), 30 | path('like/', views.like), 31 | path('dislike/', views.dislike), 32 | path('play', views.play), 33 | path('play/', views.play), 34 | path('user', views.user_center), 35 | path('search',views.search), 36 | ] 37 | 38 | urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 39 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 40 | -------------------------------------------------------------------------------- /project/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for music_recommend project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.2.7 2 | certifi==2020.4.5.1 3 | Django==3.0.5 4 | django-grappelli==2.14.1 5 | joblib==0.14.1 6 | numpy==1.18.1 7 | pandas==1.0.3 8 | python-dateutil==2.8.1 9 | pytz==2019.3 10 | scikit-surprise==1.1.0 11 | scipy==1.4.1 12 | simpleui==3.9 13 | six==1.14.0 14 | sqlparse==0.3.1 15 | ujson==2.0.3 16 | wincertstore==0.2 17 | -------------------------------------------------------------------------------- /static/css/jquery.lighter.css: -------------------------------------------------------------------------------- 1 | /* jQuery Lighter 2 | * Copyright 2015 Kevin Sylvestre 3 | * 1.3.4 4 | */ 5 | .lighter { 6 | position: fixed; 7 | top: 0; 8 | bottom: 0; 9 | left: 0; 10 | right: 0; 11 | z-index: 4000; 12 | opacity: 1; } 13 | .lighter img { 14 | display: block; 15 | width: 100%; 16 | height: 100%; } 17 | .lighter-fade .lighter-overlay { 18 | opacity: 0; } 19 | .lighter-loading .lighter-spinner { 20 | opacity: 1; 21 | -webkit-transform: scale(1); 22 | -moz-transform: scale(1); 23 | -ms-transform: scale(1); 24 | -o-transform: scale(1); 25 | transform: scale(1); } 26 | .lighter-loading .lighter-container { 27 | opacity: 0; 28 | -webkit-transform: scale(0.4); 29 | -moz-transform: scale(0.4); 30 | -ms-transform: scale(0.4); 31 | -o-transform: scale(0.4); 32 | transform: scale(0.4); } 33 | .lighter-fetched .lighter-spinner { 34 | opacity: 0; 35 | -webkit-transform: scale(0.4); 36 | -moz-transform: scale(0.4); 37 | -ms-transform: scale(0.4); 38 | -o-transform: scale(0.4); 39 | transform: scale(0.4); } 40 | .lighter-fetched .lighter-container { 41 | opacity: 1; 42 | -webkit-transform: scale(1); 43 | -moz-transform: scale(1); 44 | -ms-transform: scale(1); 45 | -o-transform: scale(1); 46 | transform: scale(1); } 47 | .lighter-fade .lighter-container { 48 | opacity: 0; 49 | -webkit-transform: scale(0.4); 50 | -moz-transform: scale(0.4); 51 | -ms-transform: scale(0.4); 52 | -o-transform: scale(0.4); 53 | transform: scale(0.4); } 54 | .lighter-fade .lighter-spinner { 55 | opacity: 0; 56 | -webkit-transform: scale(0.4); 57 | -moz-transform: scale(0.4); 58 | -ms-transform: scale(0.4); 59 | -o-transform: scale(0.4); 60 | transform: scale(0.4); } 61 | .lighter-overlay { 62 | -webkit-transition: all 0.4s ease-in-out; 63 | -moz-transition: all 0.4s ease-in-out; 64 | transition: all 0.4s ease-in-out; 65 | -webkit-transition-property: opacity, -webkit-transform; 66 | -moz-transition-property: opacity, -moz-transform; 67 | transition-property: opacity, transform; 68 | background: rgba(255, 255, 255, 0.8); 69 | height: 100%; 70 | width: 100%; } 71 | .lighter-container { 72 | -webkit-transition: all 0.4s ease-in-out; 73 | -moz-transition: all 0.4s ease-in-out; 74 | transition: all 0.4s ease-in-out; 75 | -webkit-transition-property: opacity, -webkit-transform; 76 | -moz-transition-property: opacity, -moz-transform; 77 | transition-property: opacity, transform; 78 | background: #FFF; 79 | position: absolute; 80 | z-index: 4000; 81 | top: 50%; 82 | left: 50%; 83 | right: 50%; 84 | bottom: 50%; 85 | box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.4); } 86 | .lighter-close { 87 | top: 0; 88 | left: 0; 89 | width: 30px; 90 | height: 30px; 91 | margin: -15px; 92 | line-height: 28px; 93 | font-size: 16pt; 94 | font-family: Helvetica, Arial, serif; } 95 | .lighter-next, .lighter-prev { 96 | display: none; 97 | top: 50%; 98 | width: 40px; 99 | height: 40px; 100 | margin: -20px 0; 101 | line-height: 34px; 102 | font-size: 32pt; 103 | font-family: Times, serif; } 104 | .lighter-next { 105 | right: 40px; } 106 | .lighter-prev { 107 | left: 40px; } 108 | .lighter-next, .lighter-prev, .lighter-close { 109 | cursor: pointer; 110 | position: absolute; 111 | z-index: 8000; 112 | text-align: center; 113 | border-radius: 50%; 114 | color: rgba(255, 255, 255, 0.8); 115 | background: rgba(0, 0, 0, 0.2); 116 | -webkit-user-select: none; 117 | -moz-user-select: none; 118 | -ms-user-select: none; 119 | user-select: none; 120 | -webkit-transition: all 0.4s ease-in-out; 121 | -moz-transition: all 0.4s ease-in-out; 122 | transition: all 0.4s ease-in-out; 123 | -webkit-transition-property: color, background; 124 | -moz-transition-property: color, background; 125 | transition-property: color, background; } 126 | .lighter-next:hover, .lighter-prev:hover, .lighter-close:hover { 127 | color: white; 128 | background: rgba(0, 0, 0, 0.4); } 129 | .lighter-next:active, .lighter-prev:active, .lighter-close:active { 130 | color: white; 131 | background: rgba(0, 0, 0, 0.4); } 132 | .lighter-spinner { 133 | -webkit-transition: opacity 0.4s ease-in-out; 134 | -moz-transition: opacity 0.4s ease-in-out; 135 | transition: opacity 0.4s ease-in-out; 136 | position: absolute; 137 | width: 60px; 138 | height: 20px; 139 | margin: -10px -30px; 140 | z-index: 4000; 141 | top: 50%; 142 | left: 50%; 143 | right: 50%; 144 | bottom: 50%; } 145 | .lighter-spinner::after { 146 | clear: both; 147 | content: ""; 148 | display: table; } 149 | .lighter-dot { 150 | display: block; 151 | float: left; 152 | background: rgba(0, 0, 0, 0.2); 153 | width: 16px; 154 | height: 16px; 155 | margin: 2px; 156 | padding: 0; 157 | border-radius: 50%; 158 | -webkit-animation: lighter-bounce 1.6s infinite ease-in-out; 159 | -moz-animation: lighter-bounce 1.6s infinite ease-in-out; 160 | animation: lighter-bounce 1.6s infinite ease-in-out; } 161 | .lighter-dot:nth-child(1) { 162 | -webkit-animation-delay: -0.4s; 163 | -moz-animation-delay: -0.4s; 164 | animation-delay: -0.4s; } 165 | .lighter-dot:nth-child(2) { 166 | -webkit-animation-delay: -0.26667s; 167 | -moz-animation-delay: -0.26667s; 168 | animation-delay: -0.26667s; } 169 | .lighter-dot:nth-child(3) { 170 | -webkit-animation-delay: -0.13333s; 171 | -moz-animation-delay: -0.13333s; 172 | animation-delay: -0.13333s; } 173 | 174 | @-webkit-keyframes lighter-bounce { 175 | from,to { 176 | -webkit-transform: scale(0); } 177 | 20% { 178 | -webkit-transform: scale(0); } 179 | 50% { 180 | -webkit-transform: scale(1); } 181 | 80% { 182 | -webkit-transform: scale(0); } } 183 | @-moz-keyframes lighter-bounce { 184 | from,to { 185 | -moz-transform: scale(0); } 186 | 20% { 187 | -moz-transform: scale(0); } 188 | 50% { 189 | -moz-transform: scale(1); } 190 | 80% { 191 | -moz-transform: scale(0); } } 192 | @keyframes lighter-bounce { 193 | from,to { 194 | -webkit-transform: scale(0); 195 | -moz-transform: scale(0); 196 | -ms-transform: scale(0); 197 | -o-transform: scale(0); 198 | transform: scale(0); } 199 | 20% { 200 | -webkit-transform: scale(0); 201 | -moz-transform: scale(0); 202 | -ms-transform: scale(0); 203 | -o-transform: scale(0); 204 | transform: scale(0); } 205 | 50% { 206 | -webkit-transform: scale(1); 207 | -moz-transform: scale(1); 208 | -ms-transform: scale(1); 209 | -o-transform: scale(1); 210 | transform: scale(1); } 211 | 80% { 212 | -webkit-transform: scale(0); 213 | -moz-transform: scale(0); 214 | -ms-transform: scale(0); 215 | -o-transform: scale(0); 216 | transform: scale(0); } } 217 | -------------------------------------------------------------------------------- /static/css/signin.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | } 5 | 6 | body { 7 | display: -ms-flexbox; 8 | display: flex; 9 | -ms-flex-align: center; 10 | align-items: center; 11 | padding-top: 40px; 12 | padding-bottom: 40px; 13 | background-color: #f5f5f5; 14 | } 15 | 16 | .form-signin { 17 | width: 100%; 18 | max-width: 330px; 19 | padding: 15px; 20 | margin: auto; 21 | } 22 | .form-signin .checkbox { 23 | font-weight: 400; 24 | } 25 | .form-signin .form-control { 26 | position: relative; 27 | box-sizing: border-box; 28 | height: auto; 29 | padding: 10px; 30 | font-size: 16px; 31 | } 32 | .form-signin .form-control:focus { 33 | z-index: 2; 34 | } 35 | .form-signin input[type="email"] { 36 | margin-bottom: -1px; 37 | border-bottom-right-radius: 0; 38 | border-bottom-left-radius: 0; 39 | } 40 | .form-signin input[type="password"] { 41 | margin-bottom: 10px; 42 | border-top-left-radius: 0; 43 | border-top-right-radius: 0; 44 | } 45 | -------------------------------------------------------------------------------- /static/fonts/icomoon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mango-wyc/UserProfile_MusicRecommend/fa9f99412927af88401a324fa27de8dd4bb2528e/static/fonts/icomoon.eot -------------------------------------------------------------------------------- /static/fonts/icomoon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /static/fonts/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mango-wyc/UserProfile_MusicRecommend/fa9f99412927af88401a324fa27de8dd4bb2528e/static/fonts/icomoon.ttf -------------------------------------------------------------------------------- /static/fonts/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mango-wyc/UserProfile_MusicRecommend/fa9f99412927af88401a324fa27de8dd4bb2528e/static/fonts/icomoon.woff -------------------------------------------------------------------------------- /static/js/jquery.lighter.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.9.3 2 | 3 | /* 4 | jQuery Lighter 5 | Copyright 2015 Kevin Sylvestre 6 | 1.3.4 7 | */ 8 | 9 | (function() { 10 | "use strict"; 11 | var $, Animation, Lighter, Slide, 12 | bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; 13 | 14 | $ = jQuery; 15 | 16 | Animation = (function() { 17 | function Animation() {} 18 | 19 | Animation.transitions = { 20 | "webkitTransition": "webkitTransitionEnd", 21 | "mozTransition": "mozTransitionEnd", 22 | "oTransition": "oTransitionEnd", 23 | "transition": "transitionend" 24 | }; 25 | 26 | Animation.transition = function($el) { 27 | var el, i, len, ref, result, type; 28 | for (i = 0, len = $el.length; i < len; i++) { 29 | el = $el[i]; 30 | ref = this.transitions; 31 | for (type in ref) { 32 | result = ref[type]; 33 | if (el.style[type] != null) { 34 | return result; 35 | } 36 | } 37 | } 38 | }; 39 | 40 | Animation.execute = function($el, callback) { 41 | var transition; 42 | transition = this.transition($el); 43 | if (transition != null) { 44 | return $el.one(transition, callback); 45 | } else { 46 | return callback(); 47 | } 48 | }; 49 | 50 | return Animation; 51 | 52 | })(); 53 | 54 | Slide = (function() { 55 | function Slide(url) { 56 | this.url = url; 57 | } 58 | 59 | Slide.prototype.type = function() { 60 | switch (false) { 61 | case !this.url.match(/\.(webp|jpeg|jpg|jpe|gif|png|bmp)$/i): 62 | return 'image'; 63 | default: 64 | return 'unknown'; 65 | } 66 | }; 67 | 68 | Slide.prototype.preload = function(callback) { 69 | var image; 70 | image = new Image(); 71 | image.src = this.url; 72 | return image.onload = (function(_this) { 73 | return function() { 74 | _this.dimensions = { 75 | width: image.width, 76 | height: image.height 77 | }; 78 | return callback(_this); 79 | }; 80 | })(this); 81 | }; 82 | 83 | Slide.prototype.$content = function() { 84 | return $("").attr({ 85 | src: this.url 86 | }); 87 | }; 88 | 89 | return Slide; 90 | 91 | })(); 92 | 93 | Lighter = (function() { 94 | Lighter.namespace = "lighter"; 95 | 96 | Lighter.prototype.defaults = { 97 | loading: '#{Lighter.namespace}-loading', 98 | fetched: '#{Lighter.namespace}-fetched', 99 | padding: 40, 100 | dimensions: { 101 | width: 480, 102 | height: 480 103 | }, 104 | template: "
\n
\n \n ×\n \n \n
\n
\n
\n
\n
\n
\n
\n
" 105 | }; 106 | 107 | Lighter.lighter = function($target, options) { 108 | var data; 109 | if (options == null) { 110 | options = {}; 111 | } 112 | data = $target.data('_lighter'); 113 | if (!data) { 114 | $target.data('_lighter', data = new Lighter($target, options)); 115 | } 116 | return data; 117 | }; 118 | 119 | Lighter.prototype.$ = function(selector) { 120 | return this.$el.find(selector); 121 | }; 122 | 123 | function Lighter($target, settings) { 124 | if (settings == null) { 125 | settings = {}; 126 | } 127 | this.show = bind(this.show, this); 128 | this.hide = bind(this.hide, this); 129 | this.observe = bind(this.observe, this); 130 | this.keyup = bind(this.keyup, this); 131 | this.size = bind(this.size, this); 132 | this.align = bind(this.align, this); 133 | this.process = bind(this.process, this); 134 | this.resize = bind(this.resize, this); 135 | this.type = bind(this.type, this); 136 | this.prev = bind(this.prev, this); 137 | this.next = bind(this.next, this); 138 | this.close = bind(this.close, this); 139 | this.$ = bind(this.$, this); 140 | this.$target = $target; 141 | this.settings = $.extend({}, this.defaults, settings); 142 | this.$el = $(this.settings.template); 143 | this.$overlay = this.$("." + Lighter.namespace + "-overlay"); 144 | this.$content = this.$("." + Lighter.namespace + "-content"); 145 | this.$container = this.$("." + Lighter.namespace + "-container"); 146 | this.$close = this.$("." + Lighter.namespace + "-close"); 147 | this.$prev = this.$("." + Lighter.namespace + "-prev"); 148 | this.$next = this.$("." + Lighter.namespace + "-next"); 149 | this.$body = this.$("." + Lighter.namespace + "-body"); 150 | this.dimensions = this.settings.dimensions; 151 | this.process(); 152 | } 153 | 154 | Lighter.prototype.close = function(event) { 155 | if (event != null) { 156 | event.preventDefault(); 157 | } 158 | if (event != null) { 159 | event.stopPropagation(); 160 | } 161 | return this.hide(); 162 | }; 163 | 164 | Lighter.prototype.next = function(event) { 165 | if (event != null) { 166 | event.preventDefault(); 167 | } 168 | return event != null ? event.stopPropagation() : void 0; 169 | }; 170 | 171 | Lighter.prototype.prev = function() { 172 | if (typeof event !== "undefined" && event !== null) { 173 | event.preventDefault(); 174 | } 175 | return typeof event !== "undefined" && event !== null ? event.stopPropagation() : void 0; 176 | }; 177 | 178 | Lighter.prototype.type = function(href) { 179 | if (href == null) { 180 | href = this.href(); 181 | } 182 | return this.settings.type || (this.href().match(/\.(webp|jpeg|jpg|jpe|gif|png|bmp)$/i) ? "image" : void 0); 183 | }; 184 | 185 | Lighter.prototype.resize = function(dimensions) { 186 | this.dimensions = dimensions; 187 | return this.align(); 188 | }; 189 | 190 | Lighter.prototype.process = function() { 191 | var fetched, loading; 192 | fetched = (function(_this) { 193 | return function() { 194 | return _this.$el.removeClass(Lighter.namespace + "-loading").addClass(Lighter.namespace + "-fetched"); 195 | }; 196 | })(this); 197 | loading = (function(_this) { 198 | return function() { 199 | return _this.$el.removeClass(Lighter.namespace + "-fetched").addClass(Lighter.namespace + "-loading"); 200 | }; 201 | })(this); 202 | this.slide = new Slide(this.$target.attr("href")); 203 | loading(); 204 | return this.slide.preload((function(_this) { 205 | return function(slide) { 206 | _this.resize(slide.dimensions); 207 | _this.$content.html(_this.slide.$content()); 208 | return fetched(); 209 | }; 210 | })(this)); 211 | }; 212 | 213 | Lighter.prototype.align = function() { 214 | var size; 215 | size = this.size(); 216 | return this.$container.css({ 217 | width: size.width, 218 | height: size.height, 219 | margin: "-" + (size.height / 2) + "px -" + (size.width / 2) + "px" 220 | }); 221 | }; 222 | 223 | Lighter.prototype.size = function() { 224 | var ratio; 225 | ratio = Math.max(this.dimensions.height / ($(window).height() - this.settings.padding), this.dimensions.width / ($(window).width() - this.settings.padding)); 226 | return { 227 | width: ratio > 1.0 ? Math.round(this.dimensions.width / ratio) : this.dimensions.width, 228 | height: ratio > 1.0 ? Math.round(this.dimensions.height / ratio) : this.dimensions.height 229 | }; 230 | }; 231 | 232 | Lighter.prototype.keyup = function(event) { 233 | if (event.target.form != null) { 234 | return; 235 | } 236 | if (event.which === 27) { 237 | this.close(); 238 | } 239 | if (event.which === 37) { 240 | this.prev(); 241 | } 242 | if (event.which === 39) { 243 | return this.next(); 244 | } 245 | }; 246 | 247 | Lighter.prototype.observe = function(method) { 248 | if (method == null) { 249 | method = 'on'; 250 | } 251 | $(window)[method]("resize", this.align); 252 | $(document)[method]("keyup", this.keyup); 253 | this.$overlay[method]("click", this.close); 254 | this.$close[method]("click", this.close); 255 | this.$next[method]("click", this.next); 256 | return this.$prev[method]("click", this.prev); 257 | }; 258 | 259 | Lighter.prototype.hide = function() { 260 | var alpha, omega; 261 | alpha = (function(_this) { 262 | return function() { 263 | return _this.observe('off'); 264 | }; 265 | })(this); 266 | omega = (function(_this) { 267 | return function() { 268 | return _this.$el.remove(); 269 | }; 270 | })(this); 271 | alpha(); 272 | this.$el.position(); 273 | this.$el.addClass(Lighter.namespace + "-fade"); 274 | return Animation.execute(this.$el, omega); 275 | }; 276 | 277 | Lighter.prototype.show = function() { 278 | var alpha, omega; 279 | omega = (function(_this) { 280 | return function() { 281 | return _this.observe('on'); 282 | }; 283 | })(this); 284 | alpha = (function(_this) { 285 | return function() { 286 | return $(document.body).append(_this.$el); 287 | }; 288 | })(this); 289 | alpha(); 290 | this.$el.position(); 291 | this.$el.removeClass(Lighter.namespace + "-fade"); 292 | return Animation.execute(this.$el, omega); 293 | }; 294 | 295 | return Lighter; 296 | 297 | })(); 298 | 299 | $.fn.extend({ 300 | lighter: function(option) { 301 | if (option == null) { 302 | option = {}; 303 | } 304 | return this.each(function() { 305 | var $this, action, options; 306 | $this = $(this); 307 | options = $.extend({}, $.fn.lighter.defaults, typeof option === "object" && option); 308 | action = typeof option === "string" ? option : option.action; 309 | if (action == null) { 310 | action = "show"; 311 | } 312 | return Lighter.lighter($this, options)[action](); 313 | }); 314 | } 315 | }); 316 | 317 | $(document).on("click", "[data-lighter]", function(event) { 318 | event.preventDefault(); 319 | event.stopPropagation(); 320 | return $(this).lighter(); 321 | }); 322 | 323 | }).call(this); 324 | -------------------------------------------------------------------------------- /static/js/popper.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) Federico Zivolo 2019 3 | Distributed under the MIT License (license terms are at http://opensource.org/licenses/MIT). 4 | */var e='undefined'!=typeof window&&'undefined'!=typeof document&&'undefined'!=typeof navigator;const t=function(){const t=['Edge','Trident','Firefox'];for(let o=0;o{t||(t=!0,window.Promise.resolve().then(()=>{t=!1,e()}))}}function n(e){let o=!1;return()=>{o||(o=!0,setTimeout(()=>{o=!1,e()},t))}}const i=e&&window.Promise;var r=i?o:n;function p(e){return e&&'[object Function]'==={}.toString.call(e)}function d(e,t){if(1!==e.nodeType)return[];const o=e.ownerDocument.defaultView,n=o.getComputedStyle(e,null);return t?n[t]:n}function s(e){return'HTML'===e.nodeName?e:e.parentNode||e.host}function f(e){if(!e)return document.body;switch(e.nodeName){case'HTML':case'BODY':return e.ownerDocument.body;case'#document':return e.body;}const{overflow:t,overflowX:o,overflowY:n}=d(e);return /(auto|scroll|overlay)/.test(t+n+o)?e:f(s(e))}function a(e){return e&&e.referenceNode?e.referenceNode:e}const l=e&&!!(window.MSInputMethodContext&&document.documentMode),m=e&&/MSIE 10/.test(navigator.userAgent);function h(e){return 11===e?l:10===e?m:l||m}function c(e){if(!e)return document.documentElement;const t=h(10)?document.body:null;let o=e.offsetParent||null;for(;o===t&&e.nextElementSibling;)o=(e=e.nextElementSibling).offsetParent;const n=o&&o.nodeName;return n&&'BODY'!==n&&'HTML'!==n?-1!==['TH','TD','TABLE'].indexOf(o.nodeName)&&'static'===d(o,'position')?c(o):o:e?e.ownerDocument.documentElement:document.documentElement}function u(e){const{nodeName:t}=e;return'BODY'!==t&&('HTML'===t||c(e.firstElementChild)===e)}function g(e){return null===e.parentNode?e:g(e.parentNode)}function b(e,t){if(!e||!e.nodeType||!t||!t.nodeType)return document.documentElement;const o=e.compareDocumentPosition(t)&Node.DOCUMENT_POSITION_FOLLOWING,n=o?e:t,i=o?t:e,r=document.createRange();r.setStart(n,0),r.setEnd(i,0);const{commonAncestorContainer:p}=r;if(e!==p&&t!==p||n.contains(i))return u(p)?p:c(p);const d=g(e);return d.host?b(d.host,t):b(e,g(t).host)}function w(e,t='top'){const o='top'===t?'scrollTop':'scrollLeft',n=e.nodeName;if('BODY'===n||'HTML'===n){const t=e.ownerDocument.documentElement,n=e.ownerDocument.scrollingElement||t;return n[o]}return e[o]}function y(e,t,o=!1){const n=w(t,'top'),i=w(t,'left'),r=o?-1:1;return e.top+=n*r,e.bottom+=n*r,e.left+=i*r,e.right+=i*r,e}function E(e,t){const o='x'===t?'Left':'Top',n='Left'==o?'Right':'Bottom';return parseFloat(e[`border${o}Width`],10)+parseFloat(e[`border${n}Width`],10)}function x(e,t,o,n){return Math.max(t[`offset${e}`],t[`scroll${e}`],o[`client${e}`],o[`offset${e}`],o[`scroll${e}`],h(10)?parseInt(o[`offset${e}`])+parseInt(n[`margin${'Height'===e?'Top':'Left'}`])+parseInt(n[`margin${'Height'===e?'Bottom':'Right'}`]):0)}function v(e){const t=e.body,o=e.documentElement,n=h(10)&&getComputedStyle(o);return{height:x('Height',t,o,n),width:x('Width',t,o,n)}}var O=Object.assign||function(e){for(var t,o=1;oO({key:e},d[e],{area:B(d[e])})).sort((e,t)=>t.area-e.area),f=s.filter(({width:e,height:t})=>e>=o.clientWidth&&t>=o.clientHeight),a=0t[e])}function M(e,t,o){o=o.split('-')[0];const n=k(e),i={width:n.width,height:n.height},r=-1!==['right','left'].indexOf(o),p=r?'top':'left',d=r?'left':'top',s=r?'height':'width',f=r?'width':'height';return i[p]=t[p]+t[s]/2-n[s]/2,i[d]=o===d?t[d]-n[f]:t[A(d)],i}function F(e,t){return Array.prototype.find?e.find(t):e.filter(t)[0]}function I(e,t,o){if(Array.prototype.findIndex)return e.findIndex((e)=>e[t]===o);const n=F(e,(e)=>e[t]===o);return e.indexOf(n)}function R(e,t,o){const n=void 0===o?e:e.slice(0,I(e,'name',o));return n.forEach((e)=>{e['function']&&console.warn('`modifier.function` is deprecated, use `modifier.fn`!');const o=e['function']||e.fn;e.enabled&&p(o)&&(t.offsets.popper=L(t.offsets.popper),t.offsets.reference=L(t.offsets.reference),t=o(t,e))}),t}function U(){if(this.state.isDestroyed)return;let e={instance:this,styles:{},arrowStyles:{},attributes:{},flipped:!1,offsets:{}};e.offsets.reference=W(this.state,this.popper,this.reference,this.options.positionFixed),e.placement=H(this.options.placement,e.offsets.reference,this.popper,this.reference,this.options.modifiers.flip.boundariesElement,this.options.modifiers.flip.padding),e.originalPlacement=e.placement,e.positionFixed=this.options.positionFixed,e.offsets.popper=M(this.popper,e.offsets.reference,e.placement),e.offsets.popper.position=this.options.positionFixed?'fixed':'absolute',e=R(this.modifiers,e),this.state.isCreated?this.options.onUpdate(e):(this.state.isCreated=!0,this.options.onCreate(e))}function Y(e,t){return e.some(({name:e,enabled:o})=>o&&e===t)}function V(e){const t=[!1,'ms','Webkit','Moz','O'],o=e.charAt(0).toUpperCase()+e.slice(1);for(let n=0;n{e.removeEventListener('scroll',t.updateBound)}),t.updateBound=null,t.scrollParents=[],t.scrollElement=null,t.eventsEnabled=!1,t}function X(){this.state.eventsEnabled&&(cancelAnimationFrame(this.scheduleUpdate),this.state=_(this.reference,this.state))}function J(e){return''!==e&&!isNaN(parseFloat(e))&&isFinite(e)}function Q(e,t){Object.keys(t).forEach((o)=>{let n='';-1!==['width','height','top','right','bottom','left'].indexOf(o)&&J(t[o])&&(n='px'),e.style[o]=t[o]+n})}function Z(e,t){Object.keys(t).forEach(function(o){const n=t[o];!1===n?e.removeAttribute(o):e.setAttribute(o,t[o])})}function $(e){return Q(e.instance.popper,e.styles),Z(e.instance.popper,e.attributes),e.arrowElement&&Object.keys(e.arrowStyles).length&&Q(e.arrowElement,e.arrowStyles),e}function ee(e,t,o,n,i){const r=W(i,t,e,o.positionFixed),p=H(o.placement,r,t,e,o.modifiers.flip.boundariesElement,o.modifiers.flip.padding);return t.setAttribute('x-placement',p),Q(t,{position:o.positionFixed?'fixed':'absolute'}),o}function te(e,t){const{popper:o,reference:n}=e.offsets,{round:i,floor:r}=Math,p=(e)=>e,d=i(n.width),s=i(o.width),f=-1!==['left','right'].indexOf(e.placement),a=-1!==e.placement.indexOf('-'),l=t?f||a||d%2==s%2?i:r:p,m=t?i:p;return{left:l(1==d%2&&1==s%2&&!a&&t?o.left-1:o.left),top:m(o.top),bottom:m(o.bottom),right:l(o.right)}}const oe=e&&/Firefox/i.test(navigator.userAgent);function ne(e,t){const{x:o,y:n}=t,{popper:i}=e.offsets,r=F(e.instance.modifiers,(e)=>'applyStyle'===e.name).gpuAcceleration;void 0!==r&&console.warn('WARNING: `gpuAcceleration` option moved to `computeStyle` modifier and will not be supported in future versions of Popper.js!');const p=void 0===r?t.gpuAcceleration:r,d=c(e.instance.popper),s=S(d),f={position:i.position},a=te(e,2>window.devicePixelRatio||!oe),l='bottom'===o?'top':'bottom',m='right'===n?'left':'right',h=V('transform');let u,g;if(g='bottom'==l?'HTML'===d.nodeName?-d.clientHeight+a.bottom:-s.height+a.bottom:a.top,u='right'==m?'HTML'===d.nodeName?-d.clientWidth+a.right:-s.width+a.right:a.left,p&&h)f[h]=`translate3d(${u}px, ${g}px, 0)`,f[l]=0,f[m]=0,f.willChange='transform';else{const e='bottom'==l?-1:1,t='right'==m?-1:1;f[l]=g*e,f[m]=u*t,f.willChange=`${l}, ${m}`}const b={"x-placement":e.placement};return e.attributes=O({},b,e.attributes),e.styles=O({},f,e.styles),e.arrowStyles=O({},e.offsets.arrow,e.arrowStyles),e}function ie(e,t,o){const n=F(e,({name:e})=>e===t),i=!!n&&e.some((e)=>e.name===o&&e.enabled&&e.orderi[m]&&(e.offsets.popper[a]+=r[a]+h-i[m]),e.offsets.popper=L(e.offsets.popper);const c=r[a]+r[s]/2-h/2,u=d(e.instance.popper),g=parseFloat(u[`margin${f}`],10),b=parseFloat(u[`border${f}Width`],10);let w=c-e.offsets.popper[a]-g-b;return w=Math.max(Math.min(i[s]-h,w),0),e.arrowElement=o,e.offsets.arrow={[a]:Math.round(w),[l]:''},e}function pe(e){if('end'===e)return'start';return'start'===e?'end':e}var de=['auto-start','auto','auto-end','top-start','top','top-end','right-start','right','right-end','bottom-end','bottom','bottom-start','left-end','left','left-start'];const se=de.slice(3);function fe(e,t=!1){const o=se.indexOf(e),n=se.slice(o+1).concat(se.slice(0,o));return t?n.reverse():n}const ae={FLIP:'flip',CLOCKWISE:'clockwise',COUNTERCLOCKWISE:'counterclockwise'};function le(e,t){if(Y(e.instance.modifiers,'inner'))return e;if(e.flipped&&e.placement===e.originalPlacement)return e;const o=P(e.instance.popper,e.instance.reference,t.padding,t.boundariesElement,e.positionFixed);let n=e.placement.split('-')[0],i=A(n),r=e.placement.split('-')[1]||'',p=[];switch(t.behavior){case ae.FLIP:p=[n,i];break;case ae.CLOCKWISE:p=fe(n);break;case ae.COUNTERCLOCKWISE:p=fe(n,!0);break;default:p=t.behavior;}return p.forEach((d,s)=>{if(n!==d||p.length===s+1)return e;n=e.placement.split('-')[0],i=A(n);const f=e.offsets.popper,a=e.offsets.reference,l=Math.floor,m='left'===n&&l(f.right)>l(a.left)||'right'===n&&l(f.left)l(a.top)||'bottom'===n&&l(f.top)l(o.right),u=l(f.top)l(o.bottom),b='left'===n&&h||'right'===n&&c||'top'===n&&u||'bottom'===n&&g,w=-1!==['top','bottom'].indexOf(n),y=!!t.flipVariations&&(w&&'start'===r&&h||w&&'end'===r&&c||!w&&'start'===r&&u||!w&&'end'===r&&g),E=!!t.flipVariationsByContent&&(w&&'start'===r&&c||w&&'end'===r&&h||!w&&'start'===r&&g||!w&&'end'===r&&u),x=y||E;(m||b||x)&&(e.flipped=!0,(m||b)&&(n=p[s+1]),x&&(r=pe(r)),e.placement=n+(r?'-'+r:''),e.offsets.popper=O({},e.offsets.popper,M(e.instance.popper,e.offsets.reference,e.placement)),e=R(e.instance.modifiers,e,'flip'))}),e}function me(e){const{popper:t,reference:o}=e.offsets,n=e.placement.split('-')[0],i=Math.floor,r=-1!==['top','bottom'].indexOf(n),p=r?'right':'bottom',d=r?'left':'top',s=r?'width':'height';return t[p]i(o[p])&&(e.offsets.popper[d]=i(o[p])),e}function he(e,t,o,n){var i=Math.max;const r=e.match(/((?:\-|\+)?\d*\.?\d*)(.*)/),p=+r[1],d=r[2];if(!p)return e;if(0===d.indexOf('%')){let e;switch(d){case'%p':e=o;break;case'%':case'%r':default:e=n;}const i=L(e);return i[t]/100*p}if('vh'===d||'vw'===d){let e;return e='vh'===d?i(document.documentElement.clientHeight,window.innerHeight||0):i(document.documentElement.clientWidth,window.innerWidth||0),e/100*p}return p}function ce(e,t,o,n){const i=[0,0],r=-1!==['right','left'].indexOf(n),p=e.split(/(\+|\-)/).map((e)=>e.trim()),d=p.indexOf(F(p,(e)=>-1!==e.search(/,|\s/)));p[d]&&-1===p[d].indexOf(',')&&console.warn('Offsets separated by white space(s) are deprecated, use a comma (,) instead.');const s=/\s*,\s*|\s+/;let f=-1===d?[p]:[p.slice(0,d).concat([p[d].split(s)[0]]),[p[d].split(s)[1]].concat(p.slice(d+1))];return f=f.map((e,n)=>{const i=(1===n?!r:r)?'height':'width';let p=!1;return e.reduce((e,t)=>''===e[e.length-1]&&-1!==['+','-'].indexOf(t)?(e[e.length-1]=t,p=!0,e):p?(e[e.length-1]+=t,p=!1,e):e.concat(t),[]).map((e)=>he(e,i,t,o))}),f.forEach((e,t)=>{e.forEach((o,n)=>{J(o)&&(i[t]+=o*('-'===e[n-1]?-1:1))})}),i}function ue(e,{offset:t}){const{placement:o,offsets:{popper:n,reference:i}}=e,r=o.split('-')[0];let p;return p=J(+t)?[+t,0]:ce(t,n,i,r),'left'===r?(n.top+=p[0],n.left-=p[1]):'right'===r?(n.top+=p[0],n.left+=p[1]):'top'===r?(n.left+=p[0],n.top-=p[1]):'bottom'===r&&(n.left+=p[0],n.top+=p[1]),e.popper=n,e}function ge(e,t){let o=t.boundariesElement||c(e.instance.popper);e.instance.reference===o&&(o=c(o));const n=V('transform'),i=e.instance.popper.style,{top:r,left:p,[n]:d}=i;i.top='',i.left='',i[n]='';const s=P(e.instance.popper,e.instance.reference,t.padding,o,e.positionFixed);i.top=r,i.left=p,i[n]=d,t.boundaries=s;const f=t.priority;let a=e.offsets.popper;const l={primary(e){let o=a[e];return a[e]s[e]&&!t.escapeWithReference&&(n=Math.min(a[o],s[e]-('right'===e?a.width:a.height))),{[o]:n}}};return f.forEach((e)=>{const t=-1===['left','top'].indexOf(e)?'secondary':'primary';a=O({},a,l[t](e))}),e.offsets.popper=a,e}function be(e){const t=e.placement,o=t.split('-')[0],n=t.split('-')[1];if(n){const{reference:t,popper:i}=e.offsets,r=-1!==['bottom','top'].indexOf(o),p=r?'left':'top',d=r?'width':'height',s={start:{[p]:t[p]},end:{[p]:t[p]+t[d]-i[d]}};e.offsets.popper=O({},i,s[n])}return e}function we(e){if(!ie(e.instance.modifiers,'hide','preventOverflow'))return e;const t=e.offsets.reference,o=F(e.instance.modifiers,(e)=>'preventOverflow'===e.name).boundaries;if(t.bottomo.right||t.top>o.bottom||t.right{},onUpdate:()=>{},modifiers:Ee};class ve{constructor(e,t,o={}){this.scheduleUpdate=()=>requestAnimationFrame(this.update),this.update=r(this.update.bind(this)),this.options=O({},ve.Defaults,o),this.state={isDestroyed:!1,isCreated:!1,scrollParents:[]},this.reference=e&&e.jquery?e[0]:e,this.popper=t&&t.jquery?t[0]:t,this.options.modifiers={},Object.keys(O({},ve.Defaults.modifiers,o.modifiers)).forEach((e)=>{this.options.modifiers[e]=O({},ve.Defaults.modifiers[e]||{},o.modifiers?o.modifiers[e]:{})}),this.modifiers=Object.keys(this.options.modifiers).map((e)=>O({name:e},this.options.modifiers[e])).sort((e,t)=>e.order-t.order),this.modifiers.forEach((e)=>{e.enabled&&p(e.onLoad)&&e.onLoad(this.reference,this.popper,this.options,e,this.state)}),this.update();const n=this.options.eventsEnabled;n&&this.enableEventListeners(),this.state.eventsEnabled=n}update(){return U.call(this)}destroy(){return j.call(this)}enableEventListeners(){return G.call(this)}disableEventListeners(){return X.call(this)}}ve.Utils=('undefined'==typeof window?global:window).PopperUtils,ve.placements=de,ve.Defaults=xe;export default ve; 5 | //# sourceMappingURL=popper.min.js.map 6 | -------------------------------------------------------------------------------- /static/player/css/fonts/icomoon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mango-wyc/UserProfile_MusicRecommend/fa9f99412927af88401a324fa27de8dd4bb2528e/static/player/css/fonts/icomoon.eot -------------------------------------------------------------------------------- /static/player/css/fonts/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mango-wyc/UserProfile_MusicRecommend/fa9f99412927af88401a324fa27de8dd4bb2528e/static/player/css/fonts/icomoon.ttf -------------------------------------------------------------------------------- /static/player/css/fonts/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mango-wyc/UserProfile_MusicRecommend/fa9f99412927af88401a324fa27de8dd4bb2528e/static/player/css/fonts/icomoon.woff -------------------------------------------------------------------------------- /static/player/css/main.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | margin: 0; 5 | font-family: arial, "Microsoft YaHei"; 6 | color: #FEFEFE; 7 | } 8 | 9 | body { 10 | background-size: 100% 100%; 11 | background-position: center center; 12 | background-repeat: no-repeat; 13 | /* background-image: url("../images/bg.jpg");*/ 14 | -webkit-transition: all .3s ease-out; 15 | -moz-transition: all .3s ease-out; 16 | -ms-transition: all .3s ease-out; 17 | -o-transition: all .3s ease-out; 18 | transition: all .3s ease-out; 19 | } 20 | 21 | #fileWrapper { 22 | transition: all 0.5s ease; 23 | } 24 | 25 | #fileWrapper:hover { 26 | opacity: 1 !important; 27 | } 28 | 29 | #visualizer_wrapper { 30 | text-align: center; 31 | } 32 | 33 | footer { 34 | position: fixed; 35 | bottom: 2px; 36 | color: #aaa; 37 | } 38 | 39 | .play-box { 40 | position: fixed; 41 | width: 1000px; 42 | height: 100px; 43 | margin: 0 auto; 44 | left: 0; 45 | right: 0; 46 | bottom: 0; 47 | } -------------------------------------------------------------------------------- /static/player/css/main2.css: -------------------------------------------------------------------------------- 1 | html,body{height:100%;margin:0;font-family:arial,microsoft yahei;color:#fefefe}body{background-size:100% 100%;background-position:center center;background-repeat:no-repeat;-webkit-transition:all .3s ease-out;-moz-transition:all .3s ease-out;-ms-transition:all .3s ease-out;-o-transition:all .3s ease-out;transition:all .3s ease-out;background-color:#002878}#fileWrapper{transition:all .5s ease}#fileWrapper:hover{opacity:1!important}#visualizer_wrapper{text-align:center}footer{position:fixed;bottom:2px;color:#aaa}.play-box{position:fixed;width:1000px;height:100px;margin:0 auto;left:0;right:0;bottom:0}.drive,.shadows{position:fixed;top:0;right:0;bottom:0;left:0}.drive li{position:absolute;left:50%;top:50%;border:1px solid rgba(255,255,255,.15);box-shadow:0 1vw 3vw rgba(0,0,0,.5) inset;width:100vw;height:100vh;animation:drive 10s linear infinite;animation-direction:reverse}.shadows li{position:absolute;left:50%;top:50%;background:#000;width:100vw;height:100vh;animation:shadows 10s linear infinite;animation-direction:reverse;list-style-type:none;list-style-image:none}.drive li:nth-child(1){animation-delay:0}.drive li:nth-child(2){animation-delay:-1s}.drive li:nth-child(3){animation-delay:-2s}.drive li:nth-child(4){animation-delay:-3s}.drive li:nth-child(5){animation-delay:-4s}.drive li:nth-child(6){animation-delay:-5s}.drive li:nth-child(7){animation-delay:-6s}.drive li:nth-child(8){animation-delay:-7s}.drive li:nth-child(9){animation-delay:-8s}.drive li:nth-child(10){animation-delay:-9s}.shadows li:nth-child(1){animation-delay:0}.shadows li:nth-child(2){animation-delay:-1s}.shadows li:nth-child(3){animation-delay:-2s}.shadows li:nth-child(4){animation-delay:-3s}.shadows li:nth-child(5){animation-delay:-4s}.shadows li:nth-child(6){animation-delay:-5s}.shadows li:nth-child(7){animation-delay:-6s}.shadows li:nth-child(8){animation-delay:-7s}.shadows li:nth-child(9){animation-delay:-8s}.shadows li:nth-child(10){animation-delay:-9s}@keyframes drive{0%{transform:translate(-50%,-50%) scale(1.05) rotate(0deg)}100%{transform:translate(-50%,-50%) scale(0) rotate(-45deg)}}@keyframes shadows{0%{transform:translate(-50%,-50%) scale(1.05) rotate(0deg);opacity:0}100%{transform:translate(-50%,-50%) scale(0) rotate(-45deg);opacity:.25}} -------------------------------------------------------------------------------- /static/player/css/player.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | .visualizer-player { 3 | display: block; 4 | width: 100%; 5 | height: 100%; 6 | background: #272822; 7 | font-family: arial, "Microsoft YaHei"; 8 | border-radius: 3px; 9 | } 10 | @font-face { 11 | /*自定义字体-用于绘制字体图标*/ 12 | font-family: 'icomoon'; 13 | src: url('fonts/icomoon.eot?lawp0h'); 14 | src: url('fonts/icomoon.eot?lawp0h#iefix') format('embedded-opentype'), url('fonts/icomoon.ttf?lawp0h') format('truetype'), url('fonts/icomoon.woff?lawp0h') format('woff'), url('fonts/icomoon.svg?lawp0h#icomoon') format('svg'); 15 | font-weight: normal; 16 | font-style: normal; 17 | } 18 | .visualizer-player [class^="icon-"], 19 | .visualizer-player [class*=" icon-"] { 20 | /*统一设置字体图标*/ 21 | /* 使用 !important 是要防止与浏览器扩展的问题,改变字体 */ 22 | font-family: 'icomoon' !important; 23 | speak: none; 24 | font-style: normal; 25 | font-weight: normal; 26 | font-variant: normal; 27 | text-transform: none; 28 | line-height: 1; 29 | /* 更好的字体渲染 */ 30 | -webkit-font-smoothing: antialiased; 31 | -moz-osx-font-smoothing: grayscale; 32 | } 33 | .visualizer-player canvas { 34 | position: absolute; 35 | } 36 | .visualizer-player .visualizer-player-tips { 37 | position: fixed; 38 | top: calc(50vh - 40px); 39 | text-align: center; 40 | line-height: 30px; 41 | left: 0; 42 | right: 0; 43 | width: 300px; 44 | height: 30px; 45 | margin: 0 auto; 46 | background-color: #272822; 47 | border-radius: 3px; 48 | cursor: pointer; 49 | padding: 5px; 50 | font-size: 14px; 51 | } 52 | .visualizer-player .player-control { 53 | position: absolute; 54 | height: 32px; 55 | width: 20%; 56 | top: 40%; 57 | margin: 0 0.5%; 58 | } 59 | .visualizer-player .player-control i { 60 | margin: 0 5px; 61 | font-size: 32px; 62 | cursor: pointer; 63 | color: #A7A7A7; 64 | -moz-user-select: none; 65 | -webkit-user-select: none; 66 | -ms-user-select: none; 67 | user-select: none; 68 | } 69 | .visualizer-player .player-control i:hover { 70 | color: #FFFFFF; 71 | } 72 | .visualizer-player .player-control #volumeBox { 73 | padding-top: 5px; 74 | display: inline-block; 75 | width: 35px; 76 | } 77 | .visualizer-player .player-control #volumeBox #volumeBar { 78 | position: absolute; 79 | margin: -20px 0 0 40px; 80 | border-radius: 4px; 81 | width: 100px; 82 | height: 5px; 83 | background: #5a5a5a; 84 | cursor: pointer; 85 | } 86 | .visualizer-player .player-control #volumeBox #volumeBar #volumeSize { 87 | position: absolute; 88 | left: 0; 89 | bottom: 0; 90 | height: 100%; 91 | border-radius: 4px; 92 | background: #258fb8; 93 | } 94 | .visualizer-player .player-control #volumeBox:hover #volumeBar { 95 | background: #FFFFFF; 96 | } 97 | .visualizer-player .song-info { 98 | position: absolute; 99 | border-bottom: 1px solid #575656; 100 | height: 25px; 101 | width: 100%; 102 | left: 0; 103 | top: 0; 104 | } 105 | .visualizer-player .song-info i { 106 | position: relative; 107 | height: 25px; 108 | width: 25px; 109 | top: 2px; 110 | font-size: 18px; 111 | margin: 0 5px 0 0; 112 | animation: musicIcon 2.5s infinite; 113 | -webkit-animation: musicIcon 2.5s infinite; 114 | /*Safari and Chrome*/ 115 | } 116 | .visualizer-player .song-info span { 117 | text-align: center; 118 | font-weight: bold; 119 | margin: 0 5px; 120 | } 121 | .visualizer-player .song-info .player-state { 122 | position: absolute; 123 | z-index: 100; 124 | width: 100%; 125 | height: 100%; 126 | line-height: 25px; 127 | text-align: center; 128 | background: #272822; 129 | border-radius: 4px; 130 | } 131 | .visualizer-player .song-info #songTitle, 132 | .visualizer-player .song-info #artist, 133 | .visualizer-player .song-info #album, 134 | .visualizer-player .song-info span { 135 | margin: 0 5px; 136 | cursor: default; 137 | overflow: hidden; 138 | text-overflow: ellipsis; 139 | white-space: nowrap; 140 | height: 100%; 141 | font-size: 14px; 142 | line-height: 25px; 143 | float: left; 144 | } 145 | .visualizer-player .song-info #album, 146 | .visualizer-player .song-info #artist { 147 | font-size: 12px; 148 | } 149 | .visualizer-player .player-show { 150 | position: absolute; 151 | font-size: 14px; 152 | left: 30%; 153 | height: 50px; 154 | width: 70%; 155 | top: 30%; 156 | cursor: default; 157 | } 158 | .visualizer-player .player-show .player-time { 159 | margin: 1% 0 0 0; 160 | -moz-user-select: none; 161 | -webkit-user-select: none; 162 | -ms-user-select: none; 163 | user-select: none; 164 | } 165 | .visualizer-player .player-show .progress { 166 | position: absolute; 167 | bottom: 15px; 168 | border-radius: 4px; 169 | left: 0; 170 | width: 95%; 171 | height: 10px; 172 | background: #5a5a5a; 173 | } 174 | .visualizer-player .player-show .progress .player-progress-bar { 175 | position: absolute; 176 | left: 0; 177 | bottom: 0; 178 | border-radius: 4px; 179 | height: 100%; 180 | background: #258fb8; 181 | } 182 | @keyframes musicIcon { 183 | from { 184 | font-size: 12px; 185 | } 186 | to { 187 | font-size: 18px; 188 | } 189 | } 190 | @-webkit-keyframes musicIcon { 191 | /*Safari and Chrome*/ 192 | from { 193 | font-size: 12px; 194 | } 195 | to { 196 | font-size: 18px; 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /static/player/css/player.less: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | .visualizer-player { 4 | @font-face { 5 | /*自定义字体-用于绘制字体图标*/ 6 | font-family: 'icomoon'; 7 | src: url('fonts/icomoon.eot?lawp0h'); 8 | src: url('fonts/icomoon.eot?lawp0h#iefix') format('embedded-opentype'), 9 | url('fonts/icomoon.ttf?lawp0h') format('truetype'), 10 | url('fonts/icomoon.woff?lawp0h') format('woff'), 11 | url('fonts/icomoon.svg?lawp0h#icomoon') format('svg'); 12 | font-weight: normal; 13 | font-style: normal; 14 | } 15 | 16 | [class^="icon-"], 17 | [class*=" icon-"] { 18 | /*统一设置字体图标*/ 19 | /* 使用 !important 是要防止与浏览器扩展的问题,改变字体 */ 20 | font-family: 'icomoon' !important; 21 | speak: none; 22 | font-style: normal; 23 | font-weight: normal; 24 | font-variant: normal; 25 | text-transform: none; 26 | line-height: 1; 27 | /* 更好的字体渲染 */ 28 | -webkit-font-smoothing: antialiased; 29 | -moz-osx-font-smoothing: grayscale; 30 | } 31 | 32 | display: block; 33 | width: 100%; 34 | height: 100%; 35 | background: #272822; 36 | font-family: arial, 37 | "Microsoft YaHei"; 38 | border-radius: 3px; 39 | 40 | canvas { 41 | position: absolute; 42 | } 43 | 44 | .visualizer-player-tips { 45 | position: fixed; 46 | top: calc(50vh - 40px); 47 | text-align: center; 48 | line-height: 30px; 49 | left: 0; 50 | right: 0; 51 | width: 300px; 52 | height: 30px; 53 | margin: 0 auto; 54 | background-color: #272822; 55 | border-radius: 3px; 56 | cursor: pointer; 57 | padding: 5px; 58 | font-size: 14px; 59 | } 60 | 61 | .player-control { 62 | position: absolute; 63 | height: 32px; 64 | width: 20%; 65 | top: 40%; 66 | margin: 0 0.5%; 67 | 68 | i { 69 | margin: 0 5px; 70 | font-size: 32px; 71 | cursor: pointer; 72 | color: #A7A7A7; 73 | -moz-user-select: none; 74 | -webkit-user-select: none; 75 | -ms-user-select: none; 76 | user-select: none; 77 | } 78 | 79 | i:hover { 80 | color: #FFFFFF; 81 | } 82 | 83 | #volumeBox { 84 | padding-top: 5px; 85 | display: inline-block; 86 | width: 35px; 87 | 88 | #volumeBar { 89 | position: absolute; 90 | margin: -20px 0 0 40px; 91 | border-radius: 4px; 92 | width: 100px; 93 | height: 5px; 94 | background: #5a5a5a; 95 | cursor: pointer; 96 | 97 | #volumeSize { 98 | position: absolute; 99 | left: 0; 100 | bottom: 0; 101 | height: 100%; 102 | border-radius: 4px; 103 | background: #258fb8; 104 | } 105 | } 106 | } 107 | 108 | #volumeBox:hover { 109 | #volumeBar { 110 | background: #FFFFFF; 111 | } 112 | } 113 | } 114 | 115 | 116 | .song-info { 117 | position: absolute; 118 | border-bottom: 1px solid #575656; 119 | height: 25px; 120 | width: 100%; 121 | left: 0; 122 | top: 0; 123 | 124 | i { 125 | position: relative; 126 | height: 25px; 127 | width: 25px; 128 | top: 2px; 129 | font-size: 18px; 130 | margin: 0 5px 0 0; 131 | animation: musicIcon 2.5s infinite; 132 | -webkit-animation: musicIcon 2.5s infinite; 133 | /*Safari and Chrome*/ 134 | } 135 | 136 | span { 137 | text-align: center; 138 | font-weight: bold; 139 | margin: 0 5px; 140 | } 141 | 142 | .player-state { 143 | position: absolute; 144 | z-index: 100; 145 | width: 100%; 146 | height: 100%; 147 | line-height: 25px; 148 | text-align: center; 149 | background: #272822; 150 | border-radius: 4px; 151 | } 152 | 153 | #songTitle, 154 | #artist, 155 | #album, 156 | span { 157 | margin: 0 5px; 158 | cursor: default; 159 | overflow: hidden; 160 | text-overflow: ellipsis; 161 | white-space: nowrap; 162 | height: 100%; 163 | font-size: 14px; 164 | line-height: 25px; 165 | float: left; 166 | } 167 | 168 | #album, 169 | #artist { 170 | font-size: 12px; 171 | } 172 | } 173 | 174 | .player-show { 175 | position: absolute; 176 | font-size: 14px; 177 | left: 30%; 178 | height: 50px; 179 | width: 70%; 180 | top: 30%; 181 | cursor: default; 182 | 183 | .player-time { 184 | margin: 1% 0 0 0; 185 | -moz-user-select: none; 186 | -webkit-user-select: none; 187 | -ms-user-select: none; 188 | user-select: none; 189 | } 190 | 191 | .progress { 192 | position: absolute; 193 | bottom: 15px; 194 | border-radius: 4px; 195 | left: 0; 196 | width: 95%; 197 | height: 10px; 198 | background: #5a5a5a; 199 | 200 | .player-progress-bar { 201 | position: absolute; 202 | left: 0; 203 | bottom: 0; 204 | border-radius: 4px; 205 | height: 100%; 206 | background: #258fb8; 207 | } 208 | } 209 | } 210 | 211 | @keyframes musicIcon { 212 | from { 213 | font-size: 12px; 214 | } 215 | 216 | to { 217 | font-size: 18px; 218 | } 219 | } 220 | 221 | @-webkit-keyframes musicIcon 222 | 223 | /*Safari and Chrome*/ 224 | { 225 | from { 226 | font-size: 12px; 227 | } 228 | 229 | to { 230 | font-size: 18px; 231 | } 232 | } 233 | } -------------------------------------------------------------------------------- /static/player/css/player.min.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8";.visualizer-player{display:block;width:100%;height:100%;background:#272822;font-family:arial,"Microsoft YaHei";border-radius:3px}@font-face{font-family:icomoon;src:url(fonts/icomoon.eot?lawp0h);src:url(fonts/icomoon.eot?lawp0h#iefix) format('embedded-opentype'),url(fonts/icomoon.ttf?lawp0h) format('truetype'),url(fonts/icomoon.woff?lawp0h) format('woff'),url(fonts/icomoon.svg?lawp0h#icomoon) format('svg');font-weight:400;font-style:normal}.visualizer-player [class*=" icon-"],.visualizer-player [class^=icon-]{font-family:icomoon!important;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.visualizer-player canvas{position:absolute}.visualizer-player .visualizer-player-tips{position:fixed;top:calc(50vh - 40px);text-align:center;line-height:30px;left:0;right:0;width:300px;height:30px;margin:0 auto;background-color:#272822;border-radius:3px;cursor:pointer;padding:5px;font-size:14px}.visualizer-player .player-control{position:absolute;height:32px;width:20%;top:40%;margin:0 .5%}.visualizer-player .player-control i{margin:0 5px;font-size:32px;cursor:pointer;color:#a7a7a7;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none}.visualizer-player .player-control i:hover{color:#fff}.visualizer-player .player-control #volumeBox{padding-top:5px;display:inline-block;width:35px}.visualizer-player .player-control #volumeBox #volumeBar{position:absolute;margin:-20px 0 0 40px;border-radius:4px;width:100px;height:5px;background:#5a5a5a;cursor:pointer}.visualizer-player .player-control #volumeBox #volumeBar #volumeSize{position:absolute;left:0;bottom:0;height:100%;border-radius:4px;background:#258fb8}.visualizer-player .player-control #volumeBox:hover #volumeBar{background:#fff}.visualizer-player .song-info{position:absolute;border-bottom:1px solid #575656;height:25px;width:100%;left:0;top:0}.visualizer-player .song-info i{position:relative;height:25px;width:25px;top:2px;font-size:18px;margin:0 5px 0 0;animation:musicIcon 2.5s infinite;-webkit-animation:musicIcon 2.5s infinite}.visualizer-player .song-info span{text-align:center;font-weight:700;margin:0 5px}.visualizer-player .song-info .player-state{position:absolute;z-index:100;width:100%;height:100%;line-height:25px;text-align:center;background:#272822;border-radius:4px}.visualizer-player .song-info #album,.visualizer-player .song-info #artist,.visualizer-player .song-info #songTitle,.visualizer-player .song-info span{margin:0 5px;cursor:default;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;height:100%;font-size:14px;line-height:25px;float:left}.visualizer-player .song-info #album,.visualizer-player .song-info #artist{font-size:12px}.visualizer-player .player-show{position:absolute;font-size:14px;left:30%;height:50px;width:70%;top:30%;cursor:default}.visualizer-player .player-show .player-time{margin:1% 0 0 0;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none}.visualizer-player .player-show .progress{position:absolute;bottom:15px;border-radius:4px;left:0;width:95%;height:10px;background:#5a5a5a}.visualizer-player .player-show .progress .player-progress-bar{position:absolute;left:0;bottom:0;border-radius:4px;height:100%;background:#258fb8}@keyframes musicIcon{from{font-size:12px}to{font-size:18px}}@-webkit-keyframes musicIcon{from{font-size:12px}to{font-size:18px}} -------------------------------------------------------------------------------- /static/player/css/player2.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8";.visualizer-player{display:block;width:100%;height:100%;background:#272822;font-family:arial,microsoft yahei;border-radius:3px}@font-face{font-family:icomoon;src:url(fonts/icomoon.eot?lawp0h);src:url(fonts/icomoon.eot?lawp0h#iefix) format('embedded-opentype'),url(fonts/icomoon.ttf?lawp0h) format('truetype'),url(fonts/icomoon.woff?lawp0h) format('woff'),url(fonts/icomoon.svg?lawp0h#icomoon) format('svg');font-weight:400;font-style:normal}.visualizer-player [class^=icon-],.visualizer-player [class*=" icon-"]{font-family:icomoon!important;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.visualizer-player canvas{position:absolute}.visualizer-player .player-control{position:absolute;height:32px;width:20%;top:40%;margin:0 .5%}.visualizer-player .player-control i{margin:0 5px;font-size:32px;cursor:pointer;color:#a7a7a7;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none}.visualizer-player .player-control i:hover{color:#fff}.visualizer-player .player-control #volumeBox{padding-top:5px;display:inline-block;width:35px}.visualizer-player .player-control #volumeBox #volumeBar{position:absolute;margin:-20px 0 0 40px;border-radius:4px;width:100px;height:5px;background:#5a5a5a;cursor:pointer}.visualizer-player .player-control #volumeBox #volumeBar #volumeSize{position:absolute;left:0;bottom:0;height:100%;border-radius:4px;background:#258fb8}.visualizer-player .player-control #volumeBox:hover #volumeBar{background:#fff}.visualizer-player .song-info{position:absolute;border-bottom:1px solid #575656;height:25px;width:100%;left:0;top:0}.visualizer-player .song-info i{position:relative;height:25px;width:25px;top:2px;font-size:18px;margin:0 5px 0 0;animation:musicIcon 2.5s infinite;-webkit-animation:musicIcon 2.5s infinite}.visualizer-player .song-info span{text-align:center;font-weight:700;margin:0 5px}.visualizer-player .song-info .player-state{position:absolute;z-index:100;width:100%;height:100%;line-height:25px;text-align:center;background:#272822;border-radius:4px}.visualizer-player .song-info #songTitle,.visualizer-player .song-info #artist,.visualizer-player .song-info #album,.visualizer-player .song-info span{margin:0 5px;cursor:default;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;height:100%;font-size:14px;line-height:25px;float:left}.visualizer-player .song-info #album,.visualizer-player .song-info #artist{font-size:12px}.visualizer-player .player-show{position:absolute;font-size:14px;left:30%;height:50px;width:70%;top:30%;cursor:default}.visualizer-player .player-show .player-time{margin:1% 0 0;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none}.visualizer-player .player-show .progress{position:absolute;bottom:15px;border-radius:4px;left:0;width:95%;height:10px;background:#5a5a5a}.visualizer-player .player-show .progress .player-progress-bar{position:absolute;left:0;bottom:0;border-radius:4px;height:100%;background:#258fb8}@keyframes musicIcon{from{font-size:12px}to{font-size:18px}}@-webkit-keyframes musicIcon{from{font-size:12px}to{font-size:18px}} -------------------------------------------------------------------------------- /static/player/images/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mango-wyc/UserProfile_MusicRecommend/fa9f99412927af88401a324fa27de8dd4bb2528e/static/player/images/bg.jpg -------------------------------------------------------------------------------- /static/player/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mango-wyc/UserProfile_MusicRecommend/fa9f99412927af88401a324fa27de8dd4bb2528e/static/player/images/icon.png -------------------------------------------------------------------------------- /static/player/js/player.js: -------------------------------------------------------------------------------- 1 | /** 2 | * HTML5 Audio Visualizer Player 3 | * HTML5音乐可视化播放器 4 | * 版本号:1.4.0 5 | * Author:PoppinRubo 6 | * License: MIT 7 | */ 8 | 9 | //创建一个对象方法 10 | function Player() { 11 | //播放获取进度信息时间计时器 12 | var timer; 13 | //加载超时计时 14 | var overtime; 15 | //先把自己用变量储存起来,后面要用 16 | var myself = this; 17 | //默认设置 18 | myself.button = { //设置生成的控制按钮,默认开启 19 | prev: true, //上一首 20 | play: true, //播放,暂停 21 | next: true, //下一首 22 | volume: true, //音量 23 | progressControl: true //进度控制 24 | } 25 | 26 | myself.analyser = null; 27 | 28 | //定义事件默认空方法 29 | myself.event = function (e) { 30 | //未设置事件方法就默认执行空方法 31 | } 32 | //能量传出方法默认空方法 33 | myself.energy = function (v) { 34 | //未设置能量传出就默认执行空方法 35 | } 36 | 37 | //频谱配置初始化,外部调用就开始进行处理 38 | this.init = function (object) { 39 | myself.playList = object.playList || [{ 40 | title: '播放列表为空', 41 | album: '', 42 | artist: '', 43 | mp3: '' 44 | }]; 45 | myself.autoPlay = object.autoPlay || false; 46 | myself.event = object.event || myself.event; 47 | myself.energy = object.energy || myself.energy; 48 | myself.button = object.button || myself.button; 49 | myself.color = object.color || null; 50 | myself.effect = object.effect || 0; //默认随机,效果为0表示随机切换效果 51 | //记录是否处理过音频,保证createMediaElementSource只创建一次,多次创建会出现错误 52 | myself.handle = 0; 53 | createParts(); 54 | } 55 | 56 | //改变效果 57 | this.change = function (object) { 58 | myself.effect = object.effect || 0; 59 | myself.color = object.color || null; 60 | drawSpectrum(myself.analyser); 61 | } 62 | 63 | //实例化一个音频类型window.AudioContext 64 | function windowAudioContext() { 65 | //下面这些是为了统一Chrome和Firefox的AudioContext 66 | window.AudioContext = window.AudioContext || window.webkitAudioContext || window.mozAudioContext || window.msAudioContext; 67 | window.requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame; 68 | window.cancelAnimationFrame = window.cancelAnimationFrame || window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame || window.msCancelAnimationFrame; 69 | try { 70 | myself.audioContext = new AudioContext(); 71 | } catch (e) { 72 | console.log('您的浏览器不支持 AudioContext,信息:' + e); 73 | } 74 | } 75 | 76 | //创建播放部件 77 | function createParts() { 78 | //创建audio 79 | var audio = document.createElement("AUDIO"); 80 | audio.crossOrigin = 'anonymous'; 81 | var player = document.getElementsByTagName("player")[0]; 82 | player.className = 'visualizer-player'; 83 | player.appendChild(audio); 84 | myself.player = player; 85 | myself.audio = audio; 86 | 87 | //音乐部件 88 | var songInfo = document.createElement("div"); 89 | var control = document.createElement("div"); 90 | var playerShow = document.createElement("div"); 91 | var playerTime = document.createElement("div"); 92 | var progress = document.createElement("div"); 93 | var playerProgressBar = document.createElement("div"); 94 | songInfo.className = 'song-info'; 95 | control.className = 'player-control'; 96 | playerShow.className = 'player-show'; 97 | playerTime.className = 'player-time'; 98 | progress.className = 'progress'; 99 | playerProgressBar.className = 'player-progress-bar'; 100 | player.appendChild(songInfo); 101 | player.appendChild(control); 102 | player.appendChild(playerShow); 103 | playerShow.appendChild(playerTime); 104 | playerShow.appendChild(progress); 105 | progress.appendChild(playerProgressBar); 106 | 107 | myself.songInfo = songInfo; 108 | myself.progress = progress; 109 | myself.playerProgressBar = playerProgressBar; 110 | 111 | //创建控制按钮 112 | var button = myself.button; 113 | //创建画布 114 | var canvas = document.createElement("canvas"); 115 | canvas.height = 450; 116 | canvas.width = player.clientWidth; 117 | canvas.style.bottom = player.clientHeight + 'px'; 118 | player.appendChild(canvas); 119 | myself.canvas = canvas; 120 | 121 | if (button.prev) { 122 | //上一首,按钮创建 123 | var prevBtn = document.createElement('i'); 124 | prevBtn.className = "icon-previous"; 125 | prevBtn.id = "playPrev"; 126 | prevBtn.title = "上一首"; 127 | prevBtn.innerHTML = ""; 128 | control.appendChild(prevBtn); 129 | //上一首,控制 130 | var playPrev = document.getElementById("playPrev"); 131 | playPrev.onclick = function () { 132 | prev(); 133 | } 134 | } 135 | if (button.play) { 136 | //播放,暂停,按钮创建 137 | var playBtn = document.createElement('i'); 138 | playBtn.className = "icon-play"; 139 | playBtn.id = "playControl"; 140 | playBtn.title = "播放"; 141 | playBtn.innerHTML = ""; 142 | playBtn.setAttribute('data', 'pause'); 143 | control.appendChild(playBtn); 144 | //播放,暂停,控制 145 | var playControl = document.getElementById("playControl"); 146 | playControl.onclick = function () { 147 | play(); 148 | } 149 | } 150 | if (button.next) { 151 | //下一首,按钮创建 152 | var nextBtn = document.createElement('i'); 153 | nextBtn.className = "icon-next"; 154 | nextBtn.id = "playNext"; 155 | nextBtn.title = "下一首"; 156 | nextBtn.innerHTML = ""; 157 | control.appendChild(nextBtn); 158 | //下一首,控制 159 | var playNext = document.getElementById("playNext"); 160 | playNext.onclick = function () { 161 | next(); 162 | } 163 | } 164 | if (button.volume) { 165 | //按钮与音量控制条容器 166 | var volumeBox = document.createElement('div'); 167 | volumeBox.id = "volumeBox"; 168 | control.appendChild(volumeBox); 169 | //音量,按钮创建 170 | var volumeBtn = document.createElement('i'); 171 | volumeBtn.className = "icon-volume"; 172 | volumeBtn.id = "playVolume"; 173 | volumeBtn.title = "音量"; 174 | if (playBtn) { 175 | playBtn.setAttribute('data', 'normal'); 176 | } 177 | volumeBtn.innerHTML = ""; 178 | volumeBox.appendChild(volumeBtn); 179 | //音量控制条 180 | var volumeBar = document.createElement('div'); 181 | volumeBar.id = "volumeBar"; 182 | volumeBox.appendChild(volumeBar); 183 | 184 | var volumeSize = document.createElement('div'); 185 | volumeSize.id = "volumeSize"; 186 | volumeBar.appendChild(volumeSize); 187 | 188 | volumeBar.onclick = function (event) { 189 | volumeChange(event, volumeBar, volumeSize); 190 | } 191 | 192 | //音量,点击控制,静音-恢复 193 | var volumeBtn = document.getElementById("playVolume"); 194 | volumeBtn.onclick = function () { 195 | volume(); 196 | } 197 | } 198 | 199 | //显示时间容器 200 | playerTime.innerHTML = "- 00:00 / 00:00    0%"; 201 | myself.playerTime = playerTime; 202 | 203 | //进度条 204 | if (myself.button.progressControl) { 205 | var progress = myself.progress; 206 | progress.style.cursor = "pointer"; 207 | progress.onclick = function (event) { 208 | progressControl(event, progress); 209 | } 210 | } 211 | 212 | //调用实例化AudioContext 213 | windowAudioContext(); 214 | 215 | //加载地址方法,audio加入一个初始地址 216 | var playList = myself.playList; 217 | //把列表第一个mp3地址设置到audio上 218 | myself.audio.src = playList[0].mp3; 219 | 220 | //歌曲信息,创建 221 | var songTitle = document.createElement('div'); 222 | songTitle.id = "songTitle"; 223 | songInfo.appendChild(songTitle); 224 | 225 | var album = document.createElement('div'); 226 | album.id = "album"; 227 | songInfo.appendChild(album); 228 | 229 | var span = document.createElement('span'); 230 | span.innerHTML = '-'; 231 | songInfo.appendChild(span); 232 | myself.division = span; 233 | 234 | var artist = document.createElement('div'); 235 | artist.id = "artist"; 236 | songInfo.appendChild(artist); 237 | 238 | //记录当前播放在数组里的位置 239 | myself.nowPlay = 0; 240 | //信息设置 241 | updates(); 242 | //获取存储音量 243 | myself.audio.volume = volumeGetCookie(); 244 | //设置自动播放,开始播放 245 | if (myself.autoPlay) { 246 | play(); 247 | } 248 | } 249 | 250 | //播放,暂停 控制 251 | function play() { 252 | var playBtn = document.getElementById("playControl"); 253 | //播放控制 254 | if (myself.audio.paused) { 255 | var playing = myself.audio.play(); 256 | playing.then(function () { 257 | //字符图标变化 258 | if (playBtn) { 259 | playBtn.setAttribute("data", "play"); 260 | playBtn.title = "暂停"; 261 | playBtn.innerHTML = ""; 262 | } 263 | timer = setInterval(function () { 264 | //显示时长 265 | showTime(); 266 | //获取就绪状态并处理相应 267 | playerState(); 268 | }, 1000); 269 | //播放媒体信息更新 270 | updates(); 271 | //处理播放数据,处理过就不再处理 272 | if (myself.handle == 0) { 273 | playHandle(); 274 | } 275 | //如果存在提示则移除 276 | var tips = document.getElementsByClassName('visualizer-player-tips'); 277 | if (tips.length > 0) { 278 | myself.player.removeChild(tips[0]); 279 | } 280 | }).catch(function () { 281 | //处理浏览器不支持自动播放情况 282 | var tips = document.createElement("div"); 283 | tips.className = 'visualizer-player-tips'; 284 | tips.innerHTML = '浏览器不支持自动播放,点我开始播放'; 285 | tips.onclick = function () { 286 | myself.player.removeChild(tips); 287 | play(); 288 | }; 289 | myself.player.appendChild(tips); 290 | return false; 291 | }) 292 | } else { 293 | myself.audio.pause(); 294 | //字符图标变化 295 | if (playBtn) { 296 | playBtn.setAttribute("data", "pause"); 297 | playBtn.title = "播放"; 298 | playBtn.innerHTML = ""; 299 | } 300 | window.clearInterval(timer); 301 | } 302 | //事件传出 303 | myself.event({ 304 | eventType: "play", 305 | describe: "播放/暂停", 306 | playing: !myself.audio.paused 307 | }); 308 | } 309 | 310 | //播放媒体信息更新 311 | function updates() { 312 | var list = myself.playList; 313 | var nowPlay = myself.nowPlay; 314 | var songTitle = document.getElementById("songTitle"); 315 | songTitle.innerHTML = list[nowPlay].title; 316 | songTitle.title = "歌曲:" + list[nowPlay].title; 317 | var songAlbum = document.getElementById("album"); 318 | var albumTitle = list[nowPlay].album || ''; 319 | songAlbum.innerHTML = "(" + (albumTitle) + ")"; 320 | songAlbum.style.display = albumTitle ? 'block' : 'none'; 321 | songAlbum.title = "所属专辑:" + albumTitle; 322 | var songArtist = document.getElementById("artist"); 323 | var artistName = list[nowPlay].artist; 324 | myself.division.style.display = artistName ? 'block' : 'none'; 325 | songArtist.innerHTML = artistName; 326 | songArtist.title = "艺术家:" + artistName; 327 | } 328 | 329 | //音频播放状态,做消息处理 330 | function playerState() { 331 | //音频当前的就绪状态, 0 未连接 1 打开连接 2 发送请求 3 交互 4 完成交互,接手响应 332 | var state = myself.audio.readyState; 333 | var playerState = document.getElementById("player-state"); 334 | var songInfo = myself.songInfo; 335 | if (state == 4) { 336 | if (playerState != null) { 337 | songInfo.removeChild(playerState); 338 | //清除超时计时 339 | window.clearTimeout(overtime); 340 | } 341 | } else { 342 | if (playerState == null) { 343 | playerState = document.createElement("div"); 344 | playerState.className = "player-state"; 345 | playerState.id = "player-state"; 346 | playerState.innerHTML = "加载中……"; 347 | songInfo.appendChild(playerState); 348 | //加载超时处理 349 | overtime = setTimeout(function () { //2分钟后超时处理 350 | if (myself.audio.readyState == 0) { 351 | playerState.innerHTML = "加载失败!" 352 | } 353 | }, 120000) 354 | } 355 | } 356 | } 357 | 358 | //显示时长,进度 359 | function showTime() { 360 | if (myself.audio.readyState == 4) { 361 | //时长总量 362 | var duration = myself.audio.duration; 363 | //时长进度 364 | var currentTime = myself.audio.currentTime; 365 | //剩余量 366 | var surplusTime = duration - currentTime; 367 | var ratio = ((currentTime / duration) * 100).toFixed(1); 368 | //将100.00%变为100% 369 | ratio = ratio == 100.0 ? 100 : ratio; 370 | 371 | function timeFormat(t) { 372 | return Math.floor(t / 60) + ":" + (t % 60 / 100).toFixed(2).slice(-2); 373 | } 374 | 375 | myself.playerTime.innerHTML = "- " + timeFormat(surplusTime) + " / " + timeFormat(duration) + "    " + ratio + "%"; 376 | myself.playerProgressBar.style.width = ratio + "%"; 377 | if (ratio == 100) { //播放结束就播放就调用下一首 378 | next(); 379 | } 380 | 381 | } else { //状态不为4说明未就绪显示00:00 382 | myself.playerTime.innerHTML = "- 00:00 / 00:00    0%"; 383 | } 384 | 385 | 386 | } 387 | 388 | //播放上一首 389 | function prev() { 390 | //数组播放最前移动到最后 391 | if (myself.nowPlay == 0) { 392 | myself.nowPlay = myself.playList.length; 393 | } 394 | //记录当前播放在数组里的位置位置移动,减小 395 | myself.nowPlay = myself.nowPlay - 1; 396 | //媒体url信息更新 397 | myself.audio.src = myself.playList[myself.nowPlay].mp3; 398 | //先清除计时避免越点计时越快 399 | window.clearInterval(timer); 400 | //重绘,变换效果 401 | if (myself.analyser != null) { 402 | drawSpectrum(myself.analyser); 403 | } 404 | //事件传出 405 | myself.event({ 406 | eventType: "prev", 407 | describe: "播放上一首" 408 | }); 409 | play(); 410 | } 411 | 412 | //播放下一首 413 | function next() { 414 | //数组播放最后移动到最前 415 | if (myself.nowPlay == myself.playList.length - 1) { 416 | myself.nowPlay = -1; 417 | } 418 | //记录当前播放在数组里的位置位置移动,增加 419 | myself.nowPlay = myself.nowPlay + 1; 420 | //媒体url信息更新 421 | myself.audio.src = myself.playList[myself.nowPlay].mp3; 422 | //先清除计时避免越点计时越快 423 | window.clearInterval(timer); 424 | //重绘,变换效果 425 | if (myself.analyser != null) { 426 | drawSpectrum(myself.analyser); 427 | } 428 | //事件传出 429 | myself.event({ 430 | eventType: "next", 431 | describe: "播放上一首" 432 | }); 433 | play(); 434 | } 435 | 436 | //音量点击控制,静音-恢复 437 | function volume() { 438 | if (myself.button.volume) { //判断是否设置音量按钮 439 | var volumeBtn = document.getElementById("playVolume"); 440 | var data = volumeBtn.getAttribute("data"); 441 | //字符图标变化 442 | if (data == "normal") { 443 | volumeBtn.setAttribute("data", "mute"); 444 | volumeBtn.innerHTML = ""; 445 | } else { 446 | volumeBtn.setAttribute("data", "normal"); 447 | volumeBtn.innerHTML = "" 448 | } 449 | } 450 | //点击音量控制 451 | if (myself.audio.muted) { 452 | myself.audio.muted = false; 453 | } else { 454 | myself.audio.muted = true; 455 | } 456 | } 457 | 458 | //音量控制条点击设置音量大小 459 | function volumeChange(e, volumeBar, volumeSize) { 460 | //点击的位置 461 | var offsetX = e.offsetX; 462 | //获取音量条总高度 463 | var width = volumeBar.offsetWidth; 464 | //算出占比 465 | var proportion = offsetX / width; 466 | volumeSize.style.width = (proportion * 100) + "%"; 467 | var size = proportion; 468 | //音量设置 469 | myself.audio.volume = size; 470 | //音量cookie存储 471 | volumeSetCookie(size); 472 | } 473 | 474 | //音量cookie设置 475 | function volumeSetCookie(size) { 476 | var d = new Date(); 477 | d.setHours(d.getHours() + (24 * 30)); //保存一个月 478 | document.cookie = "playerVolume=" + size + ";expires=" + d.toGMTString(); 479 | } 480 | 481 | //音量cookie获取 482 | function volumeGetCookie() { 483 | var volumeSize = document.getElementById("volumeSize"); 484 | var arr, reg = new RegExp("(^| )playerVolume=([^;]*)(;|$)"); 485 | var volume = 1; 486 | if (arr = document.cookie.match(reg)) { 487 | volume = unescape(arr[2]); 488 | } else { 489 | volume = 0.5; 490 | } 491 | volumeSize.style.width = volume * 100 + "%"; 492 | return volume; 493 | } 494 | 495 | //进度点击控制 496 | function progressControl(e, progress) { 497 | //点击的位置 498 | var offsetX = e.offsetX; 499 | //获取进度条总长度 500 | var width = progress.offsetWidth; 501 | //算出占比 502 | var proportion = offsetX / width; 503 | //把宽的比例换为播放比例,再计算audio播放位置 504 | var duration = myself.audio.duration; 505 | var playTime = duration * proportion; 506 | //从此处播放 507 | myself.audio.currentTime = playTime; 508 | 509 | } 510 | 511 | //播放处理,提取数据 512 | function playHandle() { 513 | //IE浏览器不绘制频谱 514 | if (!!window.ActiveXObject || "ActiveXObject" in window) { 515 | return false; 516 | } 517 | windowAudioContext(); 518 | var audioContext = myself.audioContext; 519 | var analyser = audioContext.createAnalyser(); 520 | var playData = audioContext.createMediaElementSource(myself.audio); 521 | // 将播放数据与分析器连接 522 | playData.connect(analyser); 523 | analyser.connect(audioContext.destination); 524 | //接下来把分析器传出去创建频谱 525 | drawSpectrum(analyser); 526 | //记录一下,还会用到analyser 527 | myself.analyser = analyser; 528 | myself.handle = 1; 529 | } 530 | 531 | //频谱效果处理 532 | function drawSpectrum(analyser) { 533 | //颜色数组 534 | var colorArray = ['#f82466', '#00FFFF', '#AFFF7C', '#FFAA6A', '#6AD5FF', '#D26AFF', '#FF6AE6', '#FF6AB8', '#FF6A6A', "#7091FF"]; 535 | //颜色随机数 536 | var colorRandom = Math.floor(Math.random() * colorArray.length); 537 | //未设置将随机选取颜色 538 | myself.colour = myself.color || colorArray[colorRandom]; 539 | //图形数组 540 | var effectArray = [1, 2, 3]; 541 | //效果随机数 542 | var effectRandom = Math.floor(Math.random() * effectArray.length); 543 | var effect = myself.effect || effectArray[effectRandom]; 544 | //随机选取效果 545 | switch (effect) { 546 | case 1: 547 | //条形 548 | bar(analyser); 549 | break; 550 | case 2: 551 | //环形声波 552 | circular(analyser); 553 | break; 554 | case 3: 555 | //心电图效果 556 | line(analyser); 557 | break; 558 | default: 559 | //条形 560 | bar(analyser); 561 | } 562 | 563 | } 564 | 565 | //16进制颜色转为RGB格式,传入16进制颜色代码与透明度 566 | function colorRgb(color, opacity) { 567 | var reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/; //十六进制颜色值的正则表达式 568 | opacity = opacity < 0 ? 0 : opacity; //颜色范围控制 569 | opacity = opacity > 1 ? 1 : opacity; 570 | if (color && reg.test(color)) { 571 | if (color.length === 4) { 572 | var sColorNew = "#"; 573 | for (var i = 1; i < 4; i += 1) { 574 | sColorNew += color.slice(i, i + 1).concat(color.slice(i, i + 1)); 575 | } 576 | color = sColorNew; 577 | } 578 | //处理六位的颜色值 579 | var sColorChange = []; 580 | for (var i = 1; i < 7; i += 2) { 581 | sColorChange.push(parseInt("0x" + color.slice(i, i + 2))); 582 | } 583 | return "rgba(" + sColorChange.join(",") + "," + opacity + ")"; 584 | } else { 585 | return color; 586 | } 587 | } 588 | 589 | //条状效果 590 | function bar(analyser) { 591 | var canvas = myself.canvas, 592 | cwidth = canvas.width, 593 | cheight = canvas.height - 2, 594 | meterWidth = 10, //频谱条宽度 595 | capHeight = 2, 596 | capStyle = '#FFFFFF', 597 | meterNum = 1000 / (10 + 2), //频谱条数量 598 | ctx = canvas.getContext('2d'), 599 | capYPositionArray = [], //将上一画面各帽头的位置保存到这个数组 600 | gradient = ctx.createLinearGradient(0, 0, 0, 300); 601 | gradient.addColorStop(1, myself.colour); 602 | var drawMeter = function () { 603 | var array = new Uint8Array(analyser.frequencyBinCount); 604 | analyser.getByteFrequencyData(array); 605 | var step = Math.round(array.length / meterNum); //计算采样步长 606 | ctx.clearRect(0, 0, cwidth, cheight); 607 | for (var i = 0; i < meterNum; i++) { 608 | var value = array[i * step]; //获取当前能量值 609 | if (capYPositionArray.length < Math.round(meterNum)) { 610 | capYPositionArray.push(value); //初始化保存帽头位置的数组,将第一个画面的数据压入其中 611 | }; 612 | ctx.fillStyle = capStyle; 613 | //开始绘制帽头 614 | if (value < capYPositionArray[i]) { //如果当前值小于之前值 615 | ctx.fillRect(i * 12, cheight - (--capYPositionArray[i]), meterWidth, capHeight); //则使用前一次保存的值来绘制帽头 616 | } else { 617 | ctx.fillRect(i * 12, cheight - value, meterWidth, capHeight); //否则使用当前值直接绘制 618 | capYPositionArray[i] = value; 619 | }; 620 | //开始绘制频谱条 621 | ctx.fillStyle = gradient; 622 | ctx.fillRect(i * 12, cheight - value + capHeight, meterWidth, cheight); 623 | //把能量传出 624 | myself.energy(value); 625 | } 626 | requestAnimationFrame(drawMeter); 627 | } 628 | requestAnimationFrame(drawMeter); 629 | } 630 | 631 | //环形声波 632 | function circular(analyser) { 633 | var canvas = myself.canvas, 634 | width = canvas.width, 635 | height = canvas.height, 636 | ctx = canvas.getContext('2d'); 637 | var drawCircular = function () { 638 | var array = new Uint8Array(128); //长度为128无符号数组用于保存getByteFrequencyData返回的频域数据 639 | analyser.getByteFrequencyData(array); //以下是根据频率数据画图 640 | ctx.clearRect(0, 0, width, height); //清除画布 641 | for (var i = 0; i < (array.length); i++) { 642 | var value = array[i]; 643 | ctx.beginPath(); 644 | ctx.arc(width / 2, height / 2, value * 0.8, 0, 400, false); 645 | ctx.lineWidth = 2; //线圈粗细 646 | ctx.strokeStyle = (1, colorRgb(myself.colour, value / 1000)); //颜色透明度随值变化 647 | ctx.stroke(); //画空心圆 648 | ctx.closePath(); 649 | //把能量传出 650 | myself.energy(value); 651 | } 652 | requestAnimationFrame(drawCircular); 653 | }; 654 | requestAnimationFrame(drawCircular); 655 | } 656 | 657 | //心电图效果 658 | function line(analyser) { 659 | var canvas = myself.canvas, 660 | width = canvas.width, 661 | height = canvas.height, 662 | ctx = canvas.getContext('2d'); 663 | var drawLine = function () { 664 | var array = new Uint8Array(128); //长度为128无符号数组用于保存getByteFrequencyData返回的频域数据 665 | analyser.getByteFrequencyData(array); //以下是根据频率数据画图 666 | ctx.clearRect(0, 0, width, height); //清除画布 667 | ctx.strokeStyle = myself.colour; 668 | ctx.lineWidth = 2; //线粗细 669 | ctx.beginPath(); 670 | for (var i = 0; i < (array.length); i++) { 671 | var value = array[i]; 672 | //绘制线根据能量值变化 673 | ctx.lineTo(i * 9, value + 150); 674 | //把能量传出 675 | myself.energy(value); 676 | }; 677 | ctx.stroke(); 678 | ctx.closePath(); 679 | requestAnimationFrame(drawLine); 680 | }; 681 | requestAnimationFrame(drawLine); 682 | } 683 | } -------------------------------------------------------------------------------- /static/player/js/player.min.js: -------------------------------------------------------------------------------- 1 | function Player(){var timer,overtime,myself=this;function windowAudioContext(){window.AudioContext=window.AudioContext||window.webkitAudioContext||window.mozAudioContext||window.msAudioContext,window.requestAnimationFrame=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.msRequestAnimationFrame,window.cancelAnimationFrame=window.cancelAnimationFrame||window.webkitCancelAnimationFrame||window.mozCancelAnimationFrame||window.msCancelAnimationFrame;try{myself.audioContext=new AudioContext}catch(e){console.log("您的浏览器不支持 AudioContext,信息:"+e)}}function createParts(){var audio=document.createElement("AUDIO");audio.crossOrigin="anonymous";var player=document.getElementsByTagName("player")[0];player.className="visualizer-player",player.appendChild(audio),myself.player=player,myself.audio=audio;var songInfo=document.createElement("div"),control=document.createElement("div"),playerShow=document.createElement("div"),playerTime=document.createElement("div"),progress=document.createElement("div"),playerProgressBar=document.createElement("div");songInfo.className="song-info",control.className="player-control",playerShow.className="player-show",playerTime.className="player-time",progress.className="progress",playerProgressBar.className="player-progress-bar",player.appendChild(songInfo),player.appendChild(control),player.appendChild(playerShow),playerShow.appendChild(playerTime),playerShow.appendChild(progress),progress.appendChild(playerProgressBar),myself.songInfo=songInfo,myself.progress=progress,myself.playerProgressBar=playerProgressBar;var button=myself.button,canvas=document.createElement("canvas"),progress;if(canvas.height=450,canvas.width=player.clientWidth,canvas.style.bottom=player.clientHeight+"px",player.appendChild(canvas),myself.canvas=canvas,button.prev){var prevBtn=document.createElement("i"),playPrev;prevBtn.className="icon-previous",prevBtn.id="playPrev",prevBtn.title="上一首",prevBtn.innerHTML="",control.appendChild(prevBtn),document.getElementById("playPrev").onclick=function(){prev()}}if(button.play){var playBtn=document.createElement("i"),playControl;playBtn.className="icon-play",playBtn.id="playControl",playBtn.title="播放",playBtn.innerHTML="",playBtn.setAttribute("data","pause"),control.appendChild(playBtn),document.getElementById("playControl").onclick=function(){play()}}if(button.next){var nextBtn=document.createElement("i"),playNext;nextBtn.className="icon-next",nextBtn.id="playNext",nextBtn.title="下一首",nextBtn.innerHTML="",control.appendChild(nextBtn),document.getElementById("playNext").onclick=function(){next()}}if(button.volume){var volumeBox=document.createElement("div"),volumeBtn;volumeBox.id="volumeBox",control.appendChild(volumeBox),(volumeBtn=document.createElement("i")).className="icon-volume",volumeBtn.id="playVolume",volumeBtn.title="音量",playBtn&&playBtn.setAttribute("data","normal"),volumeBtn.innerHTML="",volumeBox.appendChild(volumeBtn);var volumeBar=document.createElement("div");volumeBar.id="volumeBar",volumeBox.appendChild(volumeBar);var volumeSize=document.createElement("div"),volumeBtn;volumeSize.id="volumeSize",volumeBar.appendChild(volumeSize),volumeBar.onclick=function(event){volumeChange(event,volumeBar,volumeSize)},(volumeBtn=document.getElementById("playVolume")).onclick=function(){volume()}}(playerTime.innerHTML="- 00:00 / 00:00    0%",myself.playerTime=playerTime,myself.button.progressControl)&&((progress=myself.progress).style.cursor="pointer",progress.onclick=function(event){progressControl(event,progress)});windowAudioContext();var playList=myself.playList;myself.audio.src=playList[0].mp3;var songTitle=document.createElement("div");songTitle.id="songTitle",songInfo.appendChild(songTitle);var album=document.createElement("div");album.id="album",songInfo.appendChild(album);var span=document.createElement("span");span.innerHTML="-",songInfo.appendChild(span),myself.division=span;var artist=document.createElement("div");artist.id="artist",songInfo.appendChild(artist),myself.nowPlay=0,updates(),myself.audio.volume=volumeGetCookie(),myself.autoPlay&&play()}function play(){var playBtn=document.getElementById("playControl"),playing;myself.audio.paused?myself.audio.play().then((function(){playBtn&&(playBtn.setAttribute("data","play"),playBtn.title="暂停",playBtn.innerHTML=""),timer=setInterval((function(){showTime(),playerState()}),1e3),updates(),0==myself.handle&&playHandle();var tips=document.getElementsByClassName("visualizer-player-tips");tips.length>0&&myself.player.removeChild(tips[0])})).catch((function(){var tips=document.createElement("div");return tips.className="visualizer-player-tips",tips.innerHTML="浏览器不支持自动播放,点我开始播放",tips.onclick=function(){myself.player.removeChild(tips),play()},myself.player.appendChild(tips),!1})):(myself.audio.pause(),playBtn&&(playBtn.setAttribute("data","pause"),playBtn.title="播放",playBtn.innerHTML=""),window.clearInterval(timer));myself.event({eventType:"play",describe:"播放/暂停",playing:!myself.audio.paused})}function updates(){var list=myself.playList,nowPlay=myself.nowPlay,songTitle=document.getElementById("songTitle");songTitle.innerHTML=list[nowPlay].title,songTitle.title="歌曲:"+list[nowPlay].title;var songAlbum=document.getElementById("album"),albumTitle=list[nowPlay].album||"";songAlbum.innerHTML="("+albumTitle+")",songAlbum.style.display=albumTitle?"block":"none",songAlbum.title="所属专辑:"+albumTitle;var songArtist=document.getElementById("artist"),artistName=list[nowPlay].artist;myself.division.style.display=artistName?"block":"none",songArtist.innerHTML=artistName,songArtist.title="艺术家:"+artistName}function playerState(){var state=myself.audio.readyState,playerState=document.getElementById("player-state"),songInfo=myself.songInfo;4==state?null!=playerState&&(songInfo.removeChild(playerState),window.clearTimeout(overtime)):null==playerState&&((playerState=document.createElement("div")).className="player-state",playerState.id="player-state",playerState.innerHTML="加载中……",songInfo.appendChild(playerState),overtime=setTimeout((function(){0==myself.audio.readyState&&(playerState.innerHTML="加载失败!")}),12e4))}function showTime(){if(4==myself.audio.readyState){var duration=myself.audio.duration,currentTime=myself.audio.currentTime,surplusTime=duration-currentTime,ratio=(currentTime/duration*100).toFixed(1);function timeFormat(t){return Math.floor(t/60)+":"+(t%60/100).toFixed(2).slice(-2)}ratio=100==ratio?100:ratio,myself.playerTime.innerHTML="- "+timeFormat(surplusTime)+" / "+timeFormat(duration)+"    "+ratio+"%",myself.playerProgressBar.style.width=ratio+"%",100==ratio&&next()}else myself.playerTime.innerHTML="- 00:00 / 00:00    0%"}function prev(){0==myself.nowPlay&&(myself.nowPlay=myself.playList.length),myself.nowPlay=myself.nowPlay-1,myself.audio.src=myself.playList[myself.nowPlay].mp3,window.clearInterval(timer),null!=myself.analyser&&drawSpectrum(myself.analyser),myself.event({eventType:"prev",describe:"播放上一首"}),play()}function next(){myself.nowPlay==myself.playList.length-1&&(myself.nowPlay=-1),myself.nowPlay=myself.nowPlay+1,myself.audio.src=myself.playList[myself.nowPlay].mp3,window.clearInterval(timer),null!=myself.analyser&&drawSpectrum(myself.analyser),myself.event({eventType:"next",describe:"播放上一首"}),play()}function volume(){if(myself.button.volume){var volumeBtn=document.getElementById("playVolume"),data;"normal"==volumeBtn.getAttribute("data")?(volumeBtn.setAttribute("data","mute"),volumeBtn.innerHTML=""):(volumeBtn.setAttribute("data","normal"),volumeBtn.innerHTML="")}myself.audio.muted?myself.audio.muted=!1:myself.audio.muted=!0}function volumeChange(e,volumeBar,volumeSize){var offsetX,width,proportion=e.offsetX/volumeBar.offsetWidth;volumeSize.style.width=100*proportion+"%";var size=proportion;myself.audio.volume=size,volumeSetCookie(size)}function volumeSetCookie(size){var d=new Date;d.setHours(d.getHours()+720),document.cookie="playerVolume="+size+";expires="+d.toGMTString()}function volumeGetCookie(){var volumeSize=document.getElementById("volumeSize"),arr,reg=new RegExp("(^| )playerVolume=([^;]*)(;|$)"),volume=1;return volume=(arr=document.cookie.match(reg))?unescape(arr[2]):.5,volumeSize.style.width=100*volume+"%",volume}function progressControl(e,progress){var offsetX,width,proportion=e.offsetX/progress.offsetWidth,duration,playTime=myself.audio.duration*proportion;myself.audio.currentTime=playTime}function playHandle(){if(window.ActiveXObject||"ActiveXObject"in window)return!1;windowAudioContext();var audioContext=myself.audioContext,analyser=audioContext.createAnalyser(),playData;audioContext.createMediaElementSource(myself.audio).connect(analyser),analyser.connect(audioContext.destination),drawSpectrum(analyser),myself.analyser=analyser,myself.handle=1}function drawSpectrum(analyser){var colorArray=["#f82466","#00FFFF","#AFFF7C","#FFAA6A","#6AD5FF","#D26AFF","#FF6AE6","#FF6AB8","#FF6A6A","#7091FF"],colorRandom=Math.floor(Math.random()*colorArray.length);myself.colour=myself.color||colorArray[colorRandom];var effectArray=[1,2,3],effectRandom=Math.floor(Math.random()*effectArray.length),effect;switch(myself.effect||effectArray[effectRandom]){case 1:bar(analyser);break;case 2:circular(analyser);break;case 3:line(analyser);break;default:bar(analyser)}}function colorRgb(color,opacity){var reg;if(opacity=(opacity=opacity<0?0:opacity)>1?1:opacity,color&&/^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/.test(color)){if(4===color.length){for(var sColorNew="#",i=1;i<4;i+=1)sColorNew+=color.slice(i,i+1).concat(color.slice(i,i+1));color=sColorNew}for(var sColorChange=[],i=1;i<7;i+=2)sColorChange.push(parseInt("0x"+color.slice(i,i+2)));return"rgba("+sColorChange.join(",")+","+opacity+")"}return color}function bar(analyser){var canvas=myself.canvas,cwidth=canvas.width,cheight=canvas.height-2,meterWidth=10,capHeight=2,capStyle="#FFFFFF",meterNum=1e3/12,ctx=canvas.getContext("2d"),capYPositionArray=[],gradient=ctx.createLinearGradient(0,0,0,300);gradient.addColorStop(1,myself.colour);var drawMeter=function(){var array=new Uint8Array(analyser.frequencyBinCount);analyser.getByteFrequencyData(array);var step=Math.round(array.length/(1e3/12));ctx.clearRect(0,0,cwidth,cheight);for(var i=0;i<1e3/12;i++){var value=array[i*step];capYPositionArray.length加载中……";songInfo.appendChild(playerState);overtime=setTimeout(function(){if(myself.audio.readyState==0){playerState.innerHTML="加载失败!"}},120000)}}} 21 | function showTime(){if(myself.audio.readyState==4){var duration=myself.audio.duration;var currentTime=myself.audio.currentTime;var surplusTime=duration-currentTime;var ratio=((currentTime/duration)*100).toFixed(1);ratio=ratio==100.0?100:ratio;function timeFormat(t){return Math.floor(t/60)+":"+(t%60/100).toFixed(2).slice(-2);} 22 | myself.playerTime.innerHTML="- "+timeFormat(surplusTime)+" / "+timeFormat(duration)+"    "+ratio+"%";myself.playerProgressBar.style.width=ratio+"%";if(ratio==100){next();}}else{myself.playerTime.innerHTML="- 00:00 / 00:00    0%";}} 23 | function prev(){if(myself.nowPlay==0){myself.nowPlay=myself.playList.length;} 24 | myself.nowPlay=myself.nowPlay-1;myself.audio.src=myself.playList[myself.nowPlay].mp3;window.clearInterval(timer);if(myself.analyser!=null){drawSpectrum(myself.analyser);} 25 | myself.event({eventType:"prev",describe:"播放上一首"});play();} 26 | function next(){if(myself.nowPlay==myself.playList.length-1){myself.nowPlay=-1;} 27 | myself.nowPlay=myself.nowPlay+1;myself.audio.src=myself.playList[myself.nowPlay].mp3;window.clearInterval(timer);if(myself.analyser!=null){drawSpectrum(myself.analyser);} 28 | myself.event({eventType:"next",describe:"播放上一首"});play();} 29 | function volume(){if(myself.button.volume){var volumeBtn=document.getElementById("playVolume");var data=volumeBtn.getAttribute("data");if(data=="normal"){volumeBtn.setAttribute("data","mute");volumeBtn.innerHTML="";}else{volumeBtn.setAttribute("data","normal");volumeBtn.innerHTML=""}} 30 | if(myself.audio.muted){myself.audio.muted=false;}else{myself.audio.muted=true;}} 31 | function volumeChange(e,volumeBar,volumeSize){var offsetX=e.offsetX;var width=volumeBar.offsetWidth;var proportion=offsetX/width;volumeSize.style.width=(proportion*100)+"%";var size=proportion;myself.audio.volume=size;volumeSetCookie(size);} 32 | function volumeSetCookie(size){var d=new Date();d.setHours(d.getHours()+(24*30));document.cookie="playerVolume="+size+";expires="+d.toGMTString();} 33 | function volumeGetCookie(){var volumeSize=document.getElementById("volumeSize");var arr,reg=new RegExp("(^| )playerVolume=([^;]*)(;|$)");var volume=1;if(arr=document.cookie.match(reg)){volume=unescape(arr[2]);}else{volume=0.5;} 34 | volumeSize.style.width=volume*100+"%";return volume;} 35 | function progressControl(e,progress){var offsetX=e.offsetX;var width=progress.offsetWidth;var proportion=offsetX/width;var duration=myself.audio.duration;var playTime=duration*proportion;myself.audio.currentTime=playTime;} 36 | function playHandle(){windowAudioContext();var audioContext=myself.audioContext;var analyser=audioContext.createAnalyser();var playData=audioContext.createMediaElementSource(myself.audio);playData.connect(analyser);analyser.connect(audioContext.destination);drawSpectrum(analyser);myself.analyser=analyser;myself.handle=1;} 37 | function drawSpectrum(analyser){var colorArray=['#f82466','#00FFFF','#AFFF7C','#FFAA6A','#6AD5FF','#D26AFF','#FF6AE6','#FF6AB8','#FF6A6A',"#7091FF"];var colorRandom=Math.floor(Math.random()*colorArray.length);myself.colour=myself.color||colorArray[colorRandom];var effectArray=[1,2,3];var effectRandom=Math.floor(Math.random()*effectArray.length);var effect=myself.effect||effectArray[effectRandom];switch(effect){case 1:bar(analyser);break;case 2:circular(analyser);break;case 3:line(analyser);break;default:bar(analyser);}} 38 | function colorRgb(color,opacity){var reg=/^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/;opacity=opacity<0?0:opacity;opacity=opacity>1?1:opacity;if(color&®.test(color)){if(color.length===4){var sColorNew="#";for(var i=1;i<4;i+=1){sColorNew+=color.slice(i,i+1).concat(color.slice(i,i+1));} 39 | color=sColorNew;} 40 | var sColorChange=[];for(var i=1;i<7;i+=2){sColorChange.push(parseInt("0x"+color.slice(i,i+2)));} 41 | return "rgba("+sColorChange.join(",")+","+opacity+")";}else{return color;}} 42 | function bar(analyser){var canvas=myself.canvas,cwidth=canvas.width,cheight=canvas.height-2,meterWidth=10,capHeight=2,capStyle='#FFFFFF',meterNum=1000/(10+2),ctx=canvas.getContext('2d'),capYPositionArray=[],gradient=ctx.createLinearGradient(0,0,0,300);gradient.addColorStop(1,myself.colour);var drawMeter=function(){var array=new Uint8Array(analyser.frequencyBinCount);analyser.getByteFrequencyData(array);var step=Math.round(array.length/meterNum);ctx.clearRect(0,0,cwidth,cheight);for(var i=0;i 2 | 3 | 4 | 5 | {% block title %}{% endblock %} 6 | 7 | 8 | {% block head %}{% endblock %} 9 | 10 | 11 |
12 | 56 | 57 | {% block alert %} 58 | {% if messages %} 59 | {% for message in messages %} 60 | {% if message.tags != 'console' %} 61 | 64 | {% endif %} 65 | {% endfor %} 66 | {% endif %} 67 | {% endblock %} 68 | 69 | {% block body %}{% endblock %} 70 | 71 |
72 |
73 |

Copyright @ MusicRecommend

74 |
75 | 76 |
77 | 78 | {% block footer %}{% endblock %} 79 |
80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /templates/list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %} 4 | {% if title %} 5 | {{ title }} 6 | {% else %} 7 | 音乐推荐系统 8 | {% endif %} 9 | {% endblock %} 10 | 11 | {% block body %} 12 |
13 | {% for music in musics %} 14 |
15 |
16 |
{{ music.song_name }}
17 | {{ music.song_length }} ms 18 |
19 |

20 | 歌手:{{ music.artist_name }} 作曲:{{ music.composer }} 作词:{{ music.lyricist }} 21 |

22 | 流派:{{ music.genre_ids }} 23 | 语种:{{ music.language }} 24 | {% if music in user_likes %} 25 |

已添加到用户喜欢

26 | {% endif %} 27 | {% if music in user_dislikes %} 28 |

已添加到用户不喜欢

29 | {% endif %} 30 | 31 |
32 | 播放 33 | 喜欢 34 | 不喜欢 35 |
36 |
37 | {% endfor %} 38 |
39 | 40 | 45 | 46 | 90 | {% endblock %} -------------------------------------------------------------------------------- /templates/play.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 正在播放 - {{ music.song_name }} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 |
20 |
21 | 22 | 62 | 63 | -------------------------------------------------------------------------------- /templates/play2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 正在播放 - {{ music.song_name }} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 54 | -------------------------------------------------------------------------------- /templates/play3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | HTML5 可视化音乐播放器 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 | 76 | -------------------------------------------------------------------------------- /templates/sign_in.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 用户登录 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /templates/sign_up.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 用户注册 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /templates/user.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}用户中心{% endblock %} 4 | 5 | {% block body %} 6 | 7 |
8 |
用户信息
9 |
10 |
用户名:{{ request.user.username }}
11 |
12 |
13 | 14 |
15 | {% csrf_token %} 16 | 17 |
18 |
流派订阅
19 |
20 | 21 | {% for genre in genres %} 22 |
23 | {% if genre in genre_subscribe %} 24 | 25 | {% else %} 26 | 27 | {% endif %} 28 | 29 |
30 | {% endfor %} 31 | 32 |
33 | 36 |
37 |
38 | 39 |
40 | {% csrf_token %} 41 | 42 |
43 |
语种订阅
44 |
45 | 46 | {% for language in languages %} 47 |
48 | {% if language in language_subscribe %} 49 | 50 | {% else %} 51 | 52 | {% endif %} 53 | 54 |
55 | {% endfor %} 56 | 57 |
58 | 61 |
62 |
63 | 64 |
65 |
喜欢的音乐
66 |
67 | {% for music in user_likes %} 68 |
69 |
70 |
{{ music.song_name }}
71 | {{ music.song_length }} ms 72 |
73 |

74 | 歌手:{{ music.artist_name }} 作曲:{{ music.composer }} 作词:{{ music.lyricist }} 75 |

76 | 流派:{{ music.genre_ids }} 77 | 语种:{{ music.language }} 78 | 79 |
80 | 播放 81 | 不喜欢 82 |
83 |
84 | {% endfor %} 85 |
86 | 87 |
88 | 89 |
90 |
不喜欢的音乐
91 |
92 | {% for music in user_dislikes %} 93 |
94 |
95 |
{{ music.song_name }}
96 | {{ music.song_length }} ms 97 |
98 |

99 | 歌手:{{ music.artist_name }} 作曲:{{ music.composer }} 作词:{{ music.lyricist }} 100 |

101 | 流派:{{ music.genre_ids }} 102 | 语种:{{ music.language }} 103 | 104 |
105 | 播放 106 | 喜欢 107 |
108 |
109 | {% endfor %} 110 |
111 |
112 | 113 | {% endblock %} --------------------------------------------------------------------------------