feature:新增咪咕音乐标签源

This commit is contained in:
charlesxie
2023-04-04 18:01:52 +08:00
parent 85845af963
commit 22588c6565
21 changed files with 20968 additions and 657 deletions

View File

View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class MusicConfig(AppConfig):
name = 'music'

View File

@@ -0,0 +1,33 @@
# Generated by Django 2.2.6 on 2023-04-03 10:26
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Music',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255)),
('artist', models.CharField(default='', max_length=255)),
('album', models.CharField(default='', max_length=255)),
('genre', models.CharField(default='', max_length=255)),
('year', models.CharField(default='', max_length=255)),
('lyrics', models.TextField(default='')),
('comment', models.TextField(default='')),
('fs_id', models.CharField(default='', max_length=255)),
('path', models.CharField(default='', max_length=255)),
('parent_path', models.CharField(default='', max_length=255)),
('size', models.CharField(default='', max_length=255)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 2.2.6 on 2023-04-03 10:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('music', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='music',
name='path',
field=models.TextField(default=''),
),
]

View File

@@ -0,0 +1,21 @@
from django.db import models
class Music(models.Model):
title = models.CharField(max_length=255)
artist = models.CharField(max_length=255, default="")
album = models.CharField(max_length=255, default="")
genre = models.CharField(max_length=255, default="")
year = models.CharField(max_length=255, default="")
lyrics = models.TextField(default="")
comment = models.TextField(default="")
fs_id = models.CharField(max_length=255, default="")
path = models.TextField(default="")
parent_path = models.CharField(max_length=255, default="")
size = models.CharField(max_length=255, default="")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -0,0 +1,21 @@
from django.db import transaction
from applications.music.models import Music
def create_music(filename):
bulk_list = []
with transaction.atomic():
with open(f"/Users/macbookair/coding/demo/music/{filename}.txt", "r") as f:
song_list = f.readlines()
for song in song_list:
category, fs_id, isdir, local_ctime, local_mtime, path, server_ctime, server_mtime, server_filename, size = song.split(
"||")
parent_path = path.split("/")[-2]
bulk_list.append(Music(**{
"title": server_filename,
"fs_id": fs_id,
"path": path,
"parent_path": parent_path,
"size": size
}))
Music.objects.bulk_create(bulk_list)

View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@@ -29,7 +29,9 @@ class UpdateId3Serializer(serializers.Serializer):
class FetchId3ByTitleSerializer(serializers.Serializer):
title = serializers.CharField(required=True)
resource = serializers.CharField(required=True)
class FetchLlyricSerializer(serializers.Serializer):
song_id = serializers.CharField(required=True)
resource = serializers.CharField(required=True)

View File

View File

@@ -0,0 +1,82 @@
import requests
from applications.task.utils import timestamp_to_dt
from applications.utils.send import send
class MusicResource:
def __init__(self, info):
self.resource = self.get_resource(info)
def get_resource(self, info):
if info == "netease":
return NetEaseMusicClient()
elif info == "migu":
return MiGuMusicClient()
raise Exception("暂不支持该音乐平台")
def fetch_lyric(self, song_id):
return self.resource.fetch_lyric(song_id)
def fetch_id3_by_title(self, title):
return self.resource.fetch_id3_by_title(title)
class NetEaseMusicClient:
BASE_URL = "https://music.163.com/"
def fetch_lyric(self, song_id):
data = send({"url": self.BASE_URL + "api/song/lyric?lv=-1&kv=-1&tv=-1",
"params": {"id": song_id}}, "linuxapi").POST("")
return data.json().get("lrc", {}).get("lyric")
def fetch_id3_by_title(self, title):
data = send({'s': title, 'type': '1', 'limit': '10', 'offset': '0'}).POST("weapi/cloudsearch/get/web")
songs = data.json().get("result", {}).get("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")
song["artist"] = artist
song["artist_id"] = artist_id
song["album"] = album.get("name", "")
song["album_id"] = album.get("id", "")
song["album_img"] = album.get("picUrl", {})
song["year"] = year
return songs
class MiGuMusicClient:
BASE_URL = "https://m.music.migu.cn/"
header = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:80.0) Gecko/20100101 Firefox/80.0',
'Referer': 'https://m.music.migu.cn/'
}
def fetch_lyric(self, song_id):
url = f'https://music.migu.cn/v3/api/music/audioPlayer/getLyric?copyrightId={song_id}'
res = requests.get(url, headers=self.header)
return res.json()["lyric"]
def fetch_id3_by_title(self, title):
url = self.BASE_URL + f"migu/remoting/scr_search_tag?rows=10&type=2&keyword={title}&pgc=1"
res = requests.get(url, headers=self.header)
songs = res.json()["musics"]
for song in songs:
song["id"] = song['copyrightId']
song["name"] = song['songName']
song["artist"] = song['singerName']
song["artist_id"] = song['singerId']
song["album"] = song['albumName']
song["album_id"] = song['albumId']
song["album_img"] = song['cover']
song["year"] = ""
return songs

View File

@@ -6,6 +6,7 @@ from rest_framework.decorators import action
from applications.task.serialziers import FileListSerializer, Id3Serializer, UpdateId3Serializer, \
FetchId3ByTitleSerializer, FetchLlyricSerializer
from applications.task.services.music_resource import MusicResource
from applications.task.utils import timestamp_to_dt
from applications.utils.send import send
from component.drf.viewsets import GenericViewSet
@@ -104,9 +105,7 @@ class TaskViewSets(GenericViewSet):
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")
lyric = MusicResource(resource).fetch_lyric(song_id)
except Exception as e:
lyric = f"未找到歌词 {e}"
return self.success_response(data=lyric)
@@ -114,30 +113,8 @@ class TaskViewSets(GenericViewSet):
@action(methods=['POST'], detail=False)
def fetch_id3_by_title(self, request, *args, **kwargs):
validate_data = self.is_validated_data(request.data)
resource = validate_data["resource"]
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)
songs = MusicResource(resource).fetch_id3_by_title(title)
return self.success_response(data=songs)

View File

@@ -0,0 +1,24 @@
# Generated by Django 2.2.6 on 2023-04-03 10:26
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='UserProfile',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@@ -31,6 +31,7 @@ INSTALLED_APPS = [
"rest_framework",
"applications.task",
"applications.user",
"applications.music",
]
@@ -75,7 +76,16 @@ DATABASES = {
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# DATABASES = {
# "default": {
# "ENGINE": "django.db.backends.mysql",
# "NAME": 'music', # noqa
# "USER": "root",
# "PASSWORD": "123456",
# "HOST": "localhost",
# "PORT": "3306",
# },
# }
# Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators

File diff suppressed because one or more lines are too long

21096
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -23,6 +23,7 @@
"jquery": "^2.2.4",
"lodash": "^4.17.15",
"moment": "^2.24.0",
"node-sass": "^8.0.0",
"qrcodejs2": "0.0.2",
"screenfull": "^5.1.0",
"stylelint": "^13.5.0",
@@ -71,7 +72,6 @@
"friendly-errors-webpack-plugin": "^1.6.1",
"html-webpack-plugin": "^2.30.1",
"node-notifier": "^5.1.2",
"node-sass": "^4.14.1",
"optimize-css-assets-webpack-plugin": "^3.2.0",
"ora": "^1.2.0",
"portfinder": "^1.0.13",

View File

@@ -108,4 +108,4 @@ export function DELETE(url, params, config) {
// reUrl = ''; 不需要重定向
// reUrl = VueEnv === 'production' ? '' : '/api'; 重定向
// todo do
export const reUrl = ''
export const reUrl = '/api-proxy'

View File

@@ -1,143 +1,162 @@
<template>
<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 style="background: #fff;height: 100vh;">
<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>
</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%;">
<div style="background: #fff;height: 100vh;margin-left: 20px;margin-right: 20px;">
<transition name="bk-slide-fade-left">
<div style="margin-left: 40px;width: 500px;margin-top: 20px;" v-show="musicInfo.title">
<div style="width: 100%;display: flex;">
<bk-button :theme="'success'" :loading="isLoading" @click="handleClick" class="mr10"
style="width: 50%;">
保存信息
</bk-button>
<bk-select
:disabled="false"
v-model="musicInfo.genre"
style="width: 250px;background: #fff;"
:clearable="false"
v-model="resource"
style="width: 200px;"
ext-cls="select-custom"
ext-popover-cls="select-popover-custom"
:placeholder="'请选择歌曲风格'"
searchable>
<bk-option v-for="option in genreList"
ext-popover-cls="select-popover-custom">
<bk-option v-for="option in resourceList"
: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 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>
<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>
</transition>
</div>
<div style="background: #fff;height: 100vh;">
<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>
<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 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 @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>
</transition>
</div>
</div>
</template>
<script>
@@ -147,6 +166,8 @@
treeListOne: [],
filePath: '/Users/macbookair/Music/my_music',
fileName: '',
resource: 'netease',
resourceList: [{id: 'netease', name: '网易云音乐'}, {id: 'migu', name: '咪咕音乐'}],
musicInfo: {
'genre': '流行'
},
@@ -221,7 +242,7 @@
return
}
this.fadeShowDetail = false
this.$api.Task.fetchId3Title({title: this.musicInfo.title}).then((res) => {
this.$api.Task.fetchId3Title({title: this.musicInfo.title, resource: this.resource}).then((res) => {
this.fadeShowDetail = true
this.SongList = res.data
})