音乐标签web

This commit is contained in:
charlesxie
2023-03-11 15:13:24 +08:00
parent d939133f44
commit ad8f232753
26 changed files with 772 additions and 235 deletions

3
.gitignore vendored
View File

@@ -16,4 +16,5 @@ yarn-error.log*
*.ntvs*
*.njsproj
*.sln
local_settings.py
local_settings.py
db.sqlite3

View File

@@ -0,0 +1,35 @@
from rest_framework import serializers
class FileListSerializer(serializers.Serializer):
file_path = serializers.CharField(required=True)
class Id3Serializer(serializers.Serializer):
file_path = serializers.CharField(required=True)
file_name = serializers.CharField(required=True)
class MusicId3Serializer(serializers.Serializer):
title = serializers.CharField(required=True, allow_null=True, allow_blank=True)
artist = serializers.CharField(required=True, allow_null=True, allow_blank=True)
album = serializers.CharField(required=True, allow_null=True, allow_blank=True)
genre = serializers.CharField(required=True, allow_null=True, allow_blank=True)
year = serializers.CharField(required=True, allow_null=True, allow_blank=True)
lyrics = serializers.CharField(required=True, allow_null=True, allow_blank=True)
comment = serializers.CharField(required=True, allow_null=True, allow_blank=True)
album_img = serializers.CharField(required=False, allow_null=True, allow_blank=True)
file_full_path = serializers.CharField(required=True)
class UpdateId3Serializer(serializers.Serializer):
music_id3_info = MusicId3Serializer(many=True)
class FetchId3ByTitleSerializer(serializers.Serializer):
title = serializers.CharField(required=True)
class FetchLlyricSerializer(serializers.Serializer):
song_id = serializers.CharField(required=True)

View File

@@ -0,0 +1,10 @@
# coding:UTF-8
import time
def timestamp_to_dt(timestamp, format_type="%Y-%m-%d %H:%M:%S"):
# 转换成localtime
time_local = time.localtime(timestamp)
# 转换成新的时间格式(2016-05-05 20:28:54)
dt = time.strftime(format_type, time_local)
return dt

View File

@@ -1,9 +1,142 @@
from rest_framework import mixins
from rest_framework.response import Response
import base64
import os
import music_tag
from rest_framework.decorators import action
from applications.task.serialziers import FileListSerializer, Id3Serializer, UpdateId3Serializer, \
FetchId3ByTitleSerializer, FetchLlyricSerializer
from applications.task.utils import timestamp_to_dt
from applications.utils.send import send
from component.drf.viewsets import GenericViewSet
from django_vue_cli.settings import BASE_URL
class TaskViewSets(mixins.ListModelMixin, GenericViewSet):
def list(self, request, *args, **kwargs):
return Response({"name": ""})
class TaskViewSets(GenericViewSet):
def get_serializer_class(self):
if self.action == "file_list":
return FileListSerializer
elif self.action == "music_id3":
return Id3Serializer
elif self.action == "update_id3":
return UpdateId3Serializer
elif self.action == "fetch_id3_by_title":
return FetchId3ByTitleSerializer
elif self.action == "fetch_lyric":
return FetchLlyricSerializer
return FileListSerializer
@action(methods=['POST'], detail=False)
def file_list(self, request, *args, **kwargs):
validate_data = self.is_validated_data(request.data)
file_path = validate_data['file_path']
file_path_list = file_path.split('/')
data = os.listdir(file_path)
children_data = []
allow_type = ["flac", "mp3"]
for each in data:
file_type = each.split(".")[-1]
if file_type not in allow_type:
continue
children_data.append({
"name": each,
"title": each
})
res_data = [
{
"name": file_path_list[-1],
"title": file_path_list[-1],
"expanded": True,
"id": 1,
"children": children_data
}
]
return self.success_response(data=res_data)
@action(methods=['POST'], detail=False)
def music_id3(self, request, *args, **kwargs):
validate_data = self.is_validated_data(request.data)
file_path = validate_data['file_path']
file_name = validate_data['file_name']
file_title = file_name.split('.')[0]
f = music_tag.load_file(f"{file_path}/{file_name}")
artwork = f["artwork"].values
bs64_img = ""
if artwork:
bs64_img = base64.b64encode(artwork[0].raw).decode()
res_data = {
"title": f["title"].value or file_title,
"artist": f["artist"].value,
"album": f["album"].value,
"genre": f["genre"].value,
"year": f["year"].value,
"lyrics": f["lyrics"].value,
"comment": f["comment"].value,
"artwork": "data:image/jpeg;base64," + bs64_img,
}
return self.success_response(data=res_data)
@action(methods=['POST'], detail=False)
def update_id3(self, request, *args, **kwargs):
validate_data = self.is_validated_data(request.data)
music_id3_info = validate_data['music_id3_info']
for each in music_id3_info:
f = music_tag.load_file(each["file_full_path"])
f["title"] = each["title"]
f["artist"] = each["artist"]
f["album"] = each["album"]
f["genre"] = each["genre"]
f["year"] = each["year"]
f["lyrics"] = each["lyrics"]
f["comment"] = each["comment"]
if each.get("album_img", None):
img_data = send().GET(each["album_img"])
if img_data.status_code == 200:
f['artwork'] = img_data.content
f['artwork'].first.raw_thumbnail([64, 64])
f.save()
return self.success_response()
@action(methods=['POST'], detail=False)
def fetch_lyric(self, request, *args, **kwargs):
validate_data = self.is_validated_data(request.data)
song_id = validate_data["song_id"]
try:
data = send({"url": BASE_URL + "api/song/lyric?lv=-1&kv=-1&tv=-1",
"params": {"id": song_id}}, "linuxapi").POST("")
lyric = data.json().get("lrc", {}).get("lyric")
except Exception as e:
lyric = f"未找到歌词 {e}"
return self.success_response(data=lyric)
@action(methods=['POST'], detail=False)
def fetch_id3_by_title(self, request, *args, **kwargs):
validate_data = self.is_validated_data(request.data)
title = validate_data["title"]
data = send({'s': title, 'type': '1', 'limit': '10', 'offset': '0'}).POST("weapi/cloudsearch/get/web")
songs = data.json().get("result", {}).get("songs", [])
formated_songs = []
for song in songs:
artists = song.get("ar", [])
album = song.get("al", {})
if artists:
artist = artists[0].get("name", "")
artist_id = artists[0].get("id", "")
else:
artist = ""
artist_id = ""
year = song.get("publishTime", 0)
if year:
year = timestamp_to_dt(year / 1000, "%Y")
formated_songs.append({
"id": song["id"],
"name": song["name"],
"artist": artist,
"artist_id": artist_id,
"album": album.get("name", ""),
"album_id": album.get("id", ""),
"album_img": album.get("picUrl", {}),
"year": year
})
return self.success_response(data=formated_songs)

View File

View File

@@ -0,0 +1,79 @@
from json import dumps
from os import urandom
from base64 import b64encode
from binascii import hexlify
from hashlib import md5
from Cryptodome.Cipher import AES
__all__ = ["weEncrypt", "linuxEncrypt", "eEncrypt", "MD5"]
MODULUS = (
"00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7"
"b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280"
"104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932"
"575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b"
"3ece0462db0a22b8e7"
)
PUBKEY = "010001"
NONCE = b"0CoJUm6Qyw8W8jud"
LINUXKEY = b"rFgB&h#%2?^eDg:Q"
EAPIKEY = b'e82ckenh8dichen8'
def MD5(value):
m = md5()
m.update(value.encode())
return m.hexdigest()
def weEncrypt(text):
"""
引用自 https://github.com/darknessomi/musicbox/blob/master/NEMbox/encrypt.py#L40
"""
data = dumps(text).encode("utf-8")
secret = create_key(16)
method = {"iv": True, "base64": True}
params = aes(aes(data, NONCE, method), secret, method)
encseckey = rsa(secret, PUBKEY, MODULUS)
return {"params": params, "encSecKey": encseckey}
def linuxEncrypt(text):
"""
参考自 https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/util/crypto.js#L28
"""
text = str(text).encode()
data = aes(text, LINUXKEY)
return {"eparams": data.decode()}
def eEncrypt(url, text):
text = str(text)
digest = MD5("nobody{}use{}md5forencrypt".format(url, text))
data = "{}-36cd479b6b5-{}-36cd479b6b5-{}".format(url, text, digest)
return {"params": aes(data.encode(), EAPIKEY)}
def aes(text, key, method={}):
pad = 16 - len(text) % 16
text = text + bytearray([pad] * pad)
if "iv" in method:
encryptor = AES.new(key, AES.MODE_CBC, b"0102030405060708")
else:
encryptor = AES.new(key, AES.MODE_ECB)
ciphertext = encryptor.encrypt(text)
if "base64" in method:
return b64encode(ciphertext)
return hexlify(ciphertext).upper()
def rsa(text, pubkey, modulus):
text = text[::-1]
rs = pow(int(hexlify(text), 16),
int(pubkey, 16), int(modulus, 16))
return format(rs, "x").zfill(256)
def create_key(size):
return hexlify(urandom(size))[:16]

View File

@@ -0,0 +1,74 @@
from json import loads
from django.http import HttpResponse
BASE_URL = "https://music.163.com/"
def readFile(method, path, mode="r"):
with open(path, mode) as f:
if method == "read":
return f.read()
elif method == "readlines":
return f.readlines()
def saveFile(path, content, mode="w"):
with open(path, mode) as f:
f.write(str(content))
def getCookie():
return loads(
readFile("read", "cookies").replace("'", '"').encode()
)
def request_query(r, *args):
# ["id",{"ids":800435}] ["id","ids"] "id"
def check(txt):
if type(txt) == int:
return str(txt)
return txt
dic = {}
try:
info = loads(r.body)
except:
pass
for i in args:
if type(i) == list:
j = i[1]
i = i[0]
else:
j = i
if r.method == "POST":
try:
query = info[i] if r.body[0] == 123 else r.POST.get(i)
except:
query = None
elif r.method == "GET":
query = r.GET.get(i)
try:
if type(j) == dict:
key = list(j.keys())[0]
dic[key] = check(query if query else j[key])
else:
dic[j] = check(query)
except:
dic[i] = check(query)
return dic
def Http_Response(r, text, type="application/json,charset=UTF-8"):
try:
query = request_query(r, "var", "cb")
except:
query = {"var": None, "cb": None}
if query["var"] or query["cb"]:
if query["var"]:
text = "{}={}".format(query["var"], text)
elif query["cb"]:
text = "{}({})".format(query["cb"], text)
type = "application/javascript; charset=UTF-8"
if type == "":
return HttpResponse(text)
return HttpResponse(text, content_type=type)

101
applications/utils/send.py Normal file
View File

@@ -0,0 +1,101 @@
import requests
from time import time
from json import loads
from random import randint
from .public import readFile, getCookie
from .encrypt import weEncrypt, linuxEncrypt, eEncrypt
userAgents = [
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',
'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1',
'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1',
'Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Mobile Safari/537.36',
'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Mobile Safari/537.36',
'Mozilla/5.0 (Linux; Android 5.1.1; Nexus 6 Build/LYZ28E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Mobile Safari/537.36',
'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_2 like Mac OS X) AppleWebKit/603.2.4 (KHTML, like Gecko) Mobile/14F89;GameHelper',
'Mozilla/5.0 (iPhone; CPU iPhone OS 10_0 like Mac OS X) AppleWebKit/602.1.38 (KHTML, like Gecko) Version/10.0 Mobile/14A300 Safari/602.1',
'Mozilla/5.0 (iPad; CPU OS 10_0 like Mac OS X) AppleWebKit/602.1.38 (KHTML, like Gecko) Version/10.0 Mobile/14A300 Safari/602.1',
'Mozilla/5.0 (Linux; U; Android 8.1.0; zh-cn; BKK-AL10 Build/HONORBKK-AL10) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/66.0.3359.126 MQQBrowser/10.6 Mobile Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:46.0) Gecko/20100101 Firefox/46.0',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/603.2.4 (KHTML, like Gecko) Version/10.1.1 Safari/603.2.4',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:46.0) Gecko/20100101 Firefox/46.0',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/13.10586'
]
class send:
def __init__(self, data={}, encrypt_method="weapi", timeout=10, url=""):
self.BASE_URL = "https://music.163.com/"
self.headers = {
"User-Agent": userAgents[randint(0, len(userAgents)) - 1],
"Content-Type": "application/x-www-form-urlencoded",
"Referer": self.BASE_URL
}
self.session = requests.session()
self.encrypt_method = encrypt_method
self.data = data
self.timeout = timeout
self.url = url
def __url(self, url):
if url == "":
return self.BASE_URL+"api/linux/forward"
if url[:4] == "http":
return url
return self.BASE_URL + url
def __cookies(self, data={}):
try:
cookies = getCookie()
except:
return data
return {**data, **cookies}
def encrypt(self, data):
cookie = self.__cookies()
if self.encrypt_method == "linuxapi":
self.headers["User-Agent"] = userAgents[0]
data["method"] = "POST"
return linuxEncrypt(data)
elif self.encrypt_method == "weapi":
data["csrf_token"] = cookie["__csrf"] if "__csrf" in cookie else ""
return weEncrypt(data)
elif self.encrypt_method == "eapi":
data["header"] = {
'osver': "",
"appver": "8.0.0",
"channel": "",
"deviceId": "",
"mobilename": "",
"os": "android",
"resolution": "1920x1080",
"versioncode": "140",
"buildver": str(int(time())),
"requestId": str(int(time()*100))+"_0"+str(randint(100, 999)),
"__csrf": cookie["__csrf"] if "__csrf" in cookie else ""
}
if "MUSIC_U" in cookie: data["header"]["MUSIC_U"] = cookie["MUSIC_U"]
if "MUSIC_A" in cookie: data["header"]["MUSIC_A"] = cookie["MUSIC_A"]
self.headers["Cookie"] = ''.join(map(lambda key: f"{key}={data['header'][key]};",data["header"]))
return eEncrypt(self.url, data)
return data
def POST(self, url, cookie={}):
response = self.session.post(self.__url(url),
data=self.encrypt(self.data),
headers=self.headers,
cookies=self.__cookies(cookie),
timeout=self.timeout)
return response
def GET(self, url, cookie={}):
if self.encrypt_method == "eapi":
self.headers["User-Agent"] = userAgents[-1]
response = self.session.get(self.__url(url),
headers=self.headers,
cookies=self.__cookies(cookie),
timeout=self.timeout)
return response

View File

@@ -10,42 +10,37 @@ class ApiGenericMixin(object):
"""API视图类通用函数"""
# TODO 权限部分加载基类中
# permission_classes = ()
permission_classes = ()
def finalize_response(self, request, response, *args, **kwargs):
"""统一数据返回格式"""
# 文件导出时response {HttpResponse}
if not isinstance(response, Response):
return response
"""统一返回数据格式"""
if response.status_code == 403:
pass
if response.data is None:
response.data = {"result": True, "code": ResponseCodeStatus.OK, "message": "success", "data": []}
response.data = {
'result': True,
'message': 'success',
'data': None
}
elif isinstance(response.data, (list, tuple)):
response.data = {
"result": True,
"code": ResponseCodeStatus.OK,
"message": "success",
"data": response.data,
'result': True,
'message': 'success',
'data': response.data
}
elif isinstance(response.data, dict):
if not ("result" in response.data):
response.data = {
"result": True,
"code": ResponseCodeStatus.OK,
"message": "success",
"data": response.data,
}
else:
response.data = {
"result": response.data["result"],
"code": ResponseCodeStatus.OK,
"message": response.data.get("message"),
"data": response.data,
}
if response.status_code == status.HTTP_204_NO_CONTENT and request.method == "DELETE":
elif isinstance(response.data, dict) and ('code' not in response.data and 'result' not in response.data):
response.data = {
'result': True,
'message': 'success',
'data': response.data
}
if response.status_code == status.HTTP_204_NO_CONTENT and request.method == 'DELETE':
response.status_code = status.HTTP_200_OK
return super(ApiGenericMixin, self).finalize_response(request, response, *args, **kwargs)
return super(ApiGenericMixin, self).finalize_response(
request, response, *args, **kwargs
)
class ApiGatewayMixin(object):
"""对外开放API返回格式统一

View File

@@ -84,7 +84,12 @@ class GenericViewSet(ApiGenericMixin, viewsets.GenericViewSet):
data.update(request.data)
data._mutable = _mutable
def failure_response(self, msg="failed"):
return Response({"result": False, "code": "400", "data": [], "message": msg})
def success_response(self, msg="success", data=None):
data = data or []
return Response({"result": True, "code": "200", "data": data, "message": msg})
class CreateModelAndLogMixin(mixins.CreateModelMixin):
"""
Create a model instance and log.

View File

@@ -1,88 +0,0 @@
# -*- coding: utf-8 -*-
"""
猴子补丁实现django中mysql线程池
"""
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from sqlalchemy import event, exc
from sqlalchemy.pool import Pool, manage
# POOL_PESSIMISTIC_MODE为True表示每次复用连接池都检查一下连接状态
POOL_PESSIMISTIC_MODE = getattr(settings, "DJORM_POOL_PESSIMISTIC", False)
POOL_SETTINGS = getattr(settings, "DJORM_POOL_OPTIONS", {})
POOL_SETTINGS.setdefault("recycle", 3600)
POOL_SETTINGS.setdefault("pool_size", 500)
print(POOL_SETTINGS)
@event.listens_for(Pool, "checkout")
def _on_checkout(dbapi_connection, connection_record, connection_proxy):
if POOL_PESSIMISTIC_MODE:
cursor = dbapi_connection.cursor()
try:
cursor.execute("SELECT 1")
except Exception:
# raise DisconnectionError - pool will try
# connecting again up to three times before raising.
raise exc.DisconnectionError()
finally:
cursor.close()
@event.listens_for(Pool, "checkin")
def _on_checkin(*args, **kwargs):
pass
@event.listens_for(Pool, "connect")
def _on_connect(*args, **kwargs):
pass
def patch_mysql():
class HashableDict(dict):
def __hash__(self):
return hash(frozenset(self))
class HashableList(list):
def __hash__(self):
return hash(tuple(sorted(self)))
class ManagerProxy(object):
def __init__(self, manager):
self.manager = manager
def __getattr__(self, key):
return getattr(self.manager, key)
def connect(self, *args, **kwargs):
if "conv" in kwargs:
conv = kwargs["conv"]
if isinstance(conv, dict):
items = []
for k, v in conv.items():
if isinstance(v, list):
v = HashableList(v)
items.append((k, v))
kwargs["conv"] = HashableDict(items)
if "ssl" in kwargs:
ssl = kwargs["ssl"]
if isinstance(ssl, dict):
items = []
for k, v in ssl.items():
if isinstance(v, list):
v = HashableList(v)
items.append((k, v))
kwargs["ssl"] = HashableDict(items)
return self.manager.connect(*args, **kwargs)
try:
from django.db.backends.mysql import base as mysql_base
except (ImproperlyConfigured, ImportError) as e:
return
if not hasattr(mysql_base, "_Database"):
mysql_base._Database = mysql_base.Database
mysql_base.Database = ManagerProxy(manage(mysql_base._Database, **POOL_SETTINGS))

View File

@@ -1,7 +1,5 @@
from __future__ import absolute_import
from component.mysql_pool import patch_mysql
from .celery_app import app as current_app
__all__ = ('current_app',)
patch_mysql()
__all__ = ('current_app',)

View File

@@ -7,9 +7,6 @@ import datetime
BASE_DIR = Path(__file__).resolve().parent.parent
sys.path.insert(1, os.path.join(os.getcwd(), 'lib'))
# docker 中redis
BROKER_URL = "redis://redis:6379/3"
SECRET_KEY = 'django-insecure-u5_r=pekio0@zt!y(kgbufuosb9mddu8*qeejkzj@=7uyvb392'
DEBUG = False
@@ -72,18 +69,11 @@ TIME_ZONE = "Asia/Shanghai"
LANGUAGE_CODE = "zh-hans"
# Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.mysql",
"NAME": "dj-vue", # noqa bomboo
"USER": "root",
"PASSWORD": "123456", # xhongc
"HOST": "127.0.0.1", # todo docker config mysql
"PORT": "3306",
# 单元测试 DB 配置,建议不改动
"TEST": {"NAME": "test_db", "CHARSET": "utf8", "COLLATION": "utf8_general_ci"},
},
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Password validation
@@ -142,16 +132,7 @@ REST_FRAMEWORK = {
"DATETIME_FORMAT": "%Y-%m-%d %H:%M:%S",
"NON_FIELD_ERRORS_KEY": "params_error",
}
DJORM_POOL_PESSIMISTIC = False
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/8",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
}
}
JWT_AUTH = {
# 过期时间生成的took七天之后不能使用
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
@@ -161,6 +142,8 @@ JWT_AUTH = {
# 请求头携带的参数
'JWT_AUTH_HEADER_PREFIX': 'JWT',
}
BASE_URL = "https://music.163.com/"
try:
from local_settings import * # noqa
except ImportError:

View File

@@ -1,7 +0,0 @@
FROM nginx
EXPOSE 80
RUN mkdir -p /home/ubuntu/django_vue_log
RUN rm /etc/nginx/conf.d/default.conf
ADD ./docker_file/nginx/nginx-dv.conf /etc/nginx/conf.d/
ADD ./templates/index.html /home/ubuntu/

View File

@@ -1,14 +0,0 @@
server {
listen 80;
server_name 127.0.0.1;
location /static {
alias /home/ubuntu/static;
}
location / {
proxy_pass http://django-vue-cli:8000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
root /home/ubuntu;
index index.html;
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,18 +1,10 @@
celery==4.4.7
Django==2.2.6
django-celery-beat==2.2.0
django-celery-results==1.2.1
django-cors-headers==3.2.1
django-filter==2.0.0
django-mysql==3.8.1
djangorestframework==3.8.1
mysqlclient==1.4.4
python-dateutil==2.8.2
redis==3.5.3
requests==2.27.1
gunicorn==20.1.0
gevent==21.12.0
sqlalchemy==1.4.42
django-redis==5.2.0
django-redis-cache==3.0.1
djangorestframework-jwt==1.11.0
djangorestframework-jwt==1.11.0
music-tag==0.4.3
Pillow==9.4.0

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -3,7 +3,7 @@
<head>
<meta charset=utf-8>
<meta name=viewport content="width=device-width,initial-scale=1">
<title>Home</title>
<title>音乐标签Web版Music Tag Web</title>
</head>
<body>
<script>

View File

@@ -1,3 +1,3 @@
<!DOCTYPE html><html><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Home</title><link href=./static/dist/css/app.css rel=stylesheet></head><body><script>window.siteUrl = "http://127.0.0.1:8080/"
<!DOCTYPE html><html><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>音乐标签Web版Music Tag Web</title><link href=./static/dist/css/app.css rel=stylesheet></head><body><script>window.siteUrl = "http://127.0.0.1:8080/"
window.APP_CODE = 'dj-flow';
window.CSRF_COOKIE_NAME = 'django_vue_cli_csrftoken'</script><div id=app></div><script type=text/javascript src=./static/dist/js/manifest.js></script><script type=text/javascript src=./static/dist/js/vendor.js></script><script type=text/javascript src=./static/dist/js/app.js></script></body></html>
window.CSRF_COOKIE_NAME = 'django_vue_cli_csrftoken'</script><div id=app></div><script type=text/javascript src=./static/dist/js/manifest.js></script><script type=text/javascript src=./static/dist/js/vendor.js></script><script type=text/javascript src=./static/dist/js/app.js></script></body></html>

View File

@@ -9,5 +9,20 @@ export default {
},
logout: function(params) {
return POST(reUrl + '/logout/', params)
},
fileList: function(params) {
return POST(reUrl + '/api/file_list/', params)
},
musicId3: function(params) {
return POST(reUrl + '/api/music_id3/', params)
},
updateId3: function(params) {
return POST(reUrl + '/api/update_id3/', params)
},
fetchId3Title: function(params) {
return POST(reUrl + '/api/fetch_id3_by_title/', params)
},
fetchLyric: function(params) {
return POST(reUrl + '/api/fetch_lyric/', params)
}
}

View File

@@ -1,6 +1,6 @@
<template>
<bk-navigation :default-open="false" navigation-type="left-right" :header-title="headerTitle" :side-title="title"
@toggle="handleToggle" class="bk-wrapper">
@toggle="handleToggle" :need-menu="false" class="bk-wrapper">
<!-- 头部菜单 -->
<template slot="header">
<top-header></top-header>
@@ -9,9 +9,9 @@
<img class="monitor-logo-icon" :src="imgPath">
</template>
<!-- 左侧菜单 -->
<template slot="menu">
<leftMenu ref="leftMenu"></leftMenu>
</template>
<!-- <template slot="menu">-->
<!-- <leftMenu ref="leftMenu"></leftMenu>-->
<!-- </template>-->
<!-- 内容区域 -->
<container>
</container>
@@ -22,6 +22,7 @@
import topHeader from './header.vue'
import leftMenu from './leftMenu.vue'
import container from './container.vue'
export default {
components: {
topHeader,
@@ -72,7 +73,8 @@
.bk-wrapper {
.bk-navigation-wrapper {
.navigation-container {
max-width: calc(100% - 60px) !important;
max-width: calc(100%) !important;
.container-content {
padding: 0px;
}

View File

@@ -20,7 +20,7 @@ router.beforeEach((to, from, next) => {
'name': 'home',
'component': 'Home',
'meta': {
'title': '首页'
'title': '音乐标签Web版'
}
},
{

View File

@@ -1,61 +1,255 @@
<template>
<div>
<div>
<bk-input :clearable="true" v-model="value"></bk-input>
<bk-tree
ref="tree1"
:data="treeListOne"
:node-key="'id'"
:has-border="true"
@on-click="nodeClickOne"
@on-expanded="nodeExpandedOne">
</bk-tree>
<div style="display: flex;">
<div style="width: 350px;margin-top: 20px;margin-left: 10px;">
<bk-input :clearable="true" v-model="filePath"
@enter="handleSearchFile"
:placeholder="'请输入文件夹路径:'"
behavior="simplicity">
</bk-input>
<transition name="bk-slide-fade-down">
<div style="margin-top: 10px;" v-show="fadeShowDir">
<bk-tree
ref="tree1"
:data="treeListOne"
:node-key="'id'"
:has-border="true"
@on-click="nodeClickOne"
@on-expanded="nodeExpandedOne">
</bk-tree>
</div>
</transition>
</div>
<transition name="bk-slide-fade-left">
<div style="margin-left: 40px;width: 500px;margin-top: 20px;" v-show="musicInfo.title">
<div style="width: 100%;">
<bk-button :theme="'success'" :loading="isLoading" @click="handleClick" class="mr10"
style="width: 100%;">
保存信息
</bk-button>
</div>
<div style="display: flex;margin-bottom: 10px;align-items: center;margin-top: 10px;">
<div class="label1">标题</div>
<div style="width: 70%;">
<bk-input :clearable="true" v-model="musicInfo.title"></bk-input>
</div>
<div>
<bk-icon type="arrows-right-circle" @click="toggleLock('title')"
style="cursor: pointer;font-size: 22px;color: #64c864;margin-left: 10px;"></bk-icon>
</div>
</div>
<div style="display: flex;margin-bottom: 10px;align-items: center;">
<div class="label1">艺术家</div>
<div style="width: 70%;">
<bk-input :clearable="true" v-model="musicInfo.artist"></bk-input>
</div>
</div>
<div style="display: flex;margin-bottom: 10px;align-items: center;">
<div class="label1">专辑</div>
<div style="width: 70%;">
<bk-input :clearable="true" v-model="musicInfo.album"></bk-input>
</div>
</div>
<div style="display: flex;margin-bottom: 10px;align-items: center;">
<div class="label1">风格</div>
<div style="width: 70%;">
<bk-select
:disabled="false"
v-model="musicInfo.genre"
style="width: 250px;background: #fff;"
ext-cls="select-custom"
ext-popover-cls="select-popover-custom"
:placeholder="'请选择歌曲风格'"
searchable>
<bk-option v-for="option in genreList"
:key="option.id"
:id="option.id"
:name="option.name">
</bk-option>
</bk-select>
</div>
</div>
<div style="display: flex;margin-bottom: 10px;align-items: center;">
<div class="label1">年份</div>
<div style="width: 70%;">
<bk-input :clearable="true" v-model="musicInfo.year"></bk-input>
</div>
</div>
<div style="display: flex;margin-bottom: 10px;align-items: center;">
<div class="label1">歌词</div>
<div style="width: 70%;">
<bk-input :clearable="true" v-model="musicInfo.lyrics" type="textarea" :rows="15"
></bk-input>
</div>
</div>
<div style="display: flex;margin-bottom: 10px;align-items: center;">
<div class="label1">描述</div>
<div style="width: 70%;">
<bk-input :clearable="true" v-model="musicInfo.comment" type="textarea"></bk-input>
</div>
</div>
<div style="display: flex;margin-bottom: 10px;align-items: center;">
<div class="label1">专辑封面</div>
<div style="width: 70%;">
<bk-image fit="contain" :src="musicInfo.album_img" style="width: 128px;"
v-show="reloadImg"></bk-image>
<bk-image fit="contain" :src="musicInfo.artwork" style="width: 128px;"
v-show="!musicInfo.album_img"></bk-image>
</div>
</div>
</div>
</transition>
<transition name="bk-slide-fade-left">
<div
style="display: flex;flex-direction: column;margin-top: 20px;flex: 1;margin-right: 20px;margin-left: 20px;"
v-show="fadeShowDetail">
<div v-if="SongList.length === 0">
<span style="margin-left: 30%;margin-top: 30%;">暂无歌曲信息</span>
</div>
<div v-else>
<div class="parent">
<div class="title2">应用</div>
<div class="title2">专辑封面</div>
<div class="title2">歌曲名</div>
<div class="title2">歌手</div>
<div class="title2">专辑</div>
<div class="title2">歌词</div>
<div class="title2">年份</div>
</div>
<div v-for="(item,index) in SongList" :key="index" style="margin-bottom: 10px;">
<div class="song-card">
<div>
<div class="parent">
<bk-icon type="arrows-left-circle" @click="copyAll(item)"
style="font-size: 20px;color: #64c864;margin-right: 5px;cursor: pointer;"></bk-icon>
<bk-image fit="contain" :src="item.album_img" style="width: 64px;cursor: pointer;"
@click="handleCopy('album_img',item.album_img)">
</bk-image>
<div @click="handleCopy('title',item.name)" class="music-item">{{ item.name }}</div>
<div @click="handleCopy('artist',item.artist)" class="music-item">
{{item.artist }}
</div>
<div @click="handleCopy('album',item.album)" class="music-item">{{ item.album }}</div>
<div @click="handleCopy('lyric',item.id)" class="music-item">加载歌词</div>
<div @click="handleCopy('year',item.year)" class="music-item">{{ item.year }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</transition>
</div>
</template>
<script>
export default {
data() {
return {
treeListOne: [
{
name: 'tree node1',
title: 'tree node1',
expanded: true,
id: 1,
children: [
{
name: 'tree node 1-1',
title: 'tree node 1-1',
expanded: true,
children: [
{name: 'tree node 1-1-1', title: 'tree node 1-1-1', id: 2},
{name: 'tree node 1-1-2', title: 'tree node 1-1-2', id: 3},
{name: 'tree node 1-1-3', title: 'tree node 1-1-3', id: 4}
]
},
{
title: 'tree node 1-2',
name: 'tree node 1-2',
id: 5,
expanded: true,
children: [
{name: 'tree node 1-2-1', title: 'tree node 1-2-1', id: 6},
{name: 'tree node 1-2-2', title: 'tree node 1-2-2', id: 7}
]
}
]
}
treeListOne: [],
filePath: '/Users/macbookair/Music/my_music',
fileName: '',
musicInfo: {
'genre': '流行'
},
fadeShowDir: false,
fadeShowDetail: false,
isLoading: false,
SongList: [],
reloadImg: true,
genreList: [
{'id': '流行', name: '流行'},
{'id': '摇滚', name: '摇滚'},
{'id': '说唱', name: '说唱'},
{'id': '民谣', name: '民谣'},
{'id': '电子', name: '电子'},
{'id': '爵士', name: '爵士'},
{'id': '纯音乐', name: '纯音乐'},
{'id': '金属', name: '金属'},
{'id': '世界音乐', name: '世界音乐'},
{'id': '新世纪', name: '新世纪'},
{'id': '古典', name: '古典'},
{'id': '独立', name: '独立'},
{'id': '氛围音乐', name: '氛围音乐'}
]
}
},
methods: {
nodeClickOne(node) {
console.log(node)
this.musicInfo = {}
this.fileName = node.name
this.$api.Task.musicId3({'file_path': this.filePath, 'file_name': node.name}).then((res) => {
console.log(res)
this.musicInfo = res.data
})
},
handleCopy(k, v) {
if (k === 'lyric') {
this.$api.Task.fetchLyric({'song_id': v}).then((res) => {
console.log(res)
if (res.result) {
this.musicInfo['lyrics'] = res.data
} else {
this.$cwMessage('未找到歌词', 'error')
}
})
} else if (k === 'album_img') {
this.musicInfo[k] = v
this.reloadImg = false
this.$nextTick(() => {
this.reloadImg = true
})
} else {
this.musicInfo[k] = v
}
},
copyAll(item) {
this.handleCopy('title', item.name)
this.handleCopy('year', item.year)
this.handleCopy('lyric', item.id)
this.handleCopy('album', item.album)
this.handleCopy('artist', item.artist)
this.handleCopy('album_img', item.album_img)
},
nodeExpandedOne(node, expanded) {
console.log(node)
console.log(expanded)
},
// 查询网易云接口
toggleLock(mode) {
if (mode === 'title') {
if (!this.musicInfo.title) {
this.$cwMessage('标题不能为空', 'error')
return
}
this.fadeShowDetail = false
this.$api.Task.fetchId3Title({title: this.musicInfo.title}).then((res) => {
this.fadeShowDetail = true
this.SongList = res.data
})
}
},
// 文件目录
handleSearchFile() {
this.fadeShowDir = false
this.$api.Task.fileList({'file_path': this.filePath}).then((res) => {
if (res.result) {
this.treeListOne = res.data
this.fadeShowDir = true
}
})
},
// 保存音乐信息
handleClick() {
const params = [{
'file_full_path': this.filePath + '/' + this.fileName,
...this.musicInfo
}]
this.isLoading = true
this.$api.Task.updateId3({'music_id3_info': params}).then((res) => {
this.isLoading = false
if (res.result) {
this.$cwMessage('修改成功', 'success')
}
})
}
}
}
@@ -67,4 +261,35 @@
text-decoration-style: dashed;
text-underline-position: under;
}
.music-item {
cursor: pointer;
}
.music-item:hover {
color: #1facdd;
}
.label1 {
width: 80px;
}
.parent {
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-template-rows: repeat(1, 1fr);
grid-column-gap: 0;
grid-row-gap: 0;
place-items: center;
}
.title2 {
font-weight: 500;
}
.song-card {
display: flex;
align-items: center;
border-bottom: 1px solid #E2E2E2;
}
.song-card:hover {
background: #E2E2E2;
}
</style>