mirror of
https://github.com/xhongc/music-tag-web.git
synced 2026-02-02 17:59:07 +08:00
feature:支持翻译
This commit is contained in:
@@ -28,11 +28,11 @@ DFF等音频格式。
|
||||
|
||||
### 从阿里云Docker Registry拉取镜像
|
||||
|
||||
1`docker pull registry.cn-hangzhou.aliyuncs.com/charles0519/music_tag_web:1.0.9`
|
||||
1`docker pull registry.cn-hangzhou.aliyuncs.com/charles0519/music_tag_web:1.1.0`
|
||||
|
||||
### dokcer run
|
||||
|
||||
2. `docker run -d -p 8001:8001 -v /path/to/your/music:/app/media --restart=always registry.cn-hangzhou.aliyuncs.com/charles0519/music_tag_web:1.0.9`
|
||||
2. `docker run -d -p 8001:8001 -v /path/to/your/music:/app/media --restart=always registry.cn-hangzhou.aliyuncs.com/charles0519/music_tag_web:1.1.0`
|
||||
|
||||
或者 使用portainer stacks部署
|
||||

|
||||
@@ -42,7 +42,7 @@ version: '3'
|
||||
|
||||
services:
|
||||
music-tag:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/charles0519/music_tag_web:1.0.9
|
||||
image: registry.cn-hangzhou.aliyuncs.com/charles0519/music_tag_web:1.1.0
|
||||
container_name: music-tag-web
|
||||
ports:
|
||||
- "8001:8001"
|
||||
|
||||
@@ -44,3 +44,7 @@ class FetchId3ByTitleSerializer(serializers.Serializer):
|
||||
class FetchLlyricSerializer(serializers.Serializer):
|
||||
song_id = serializers.CharField(required=True)
|
||||
resource = serializers.CharField(required=True)
|
||||
|
||||
|
||||
class TranslationLycSerializer(serializers.Serializer):
|
||||
lyc = serializers.CharField(required=True)
|
||||
|
||||
@@ -10,10 +10,11 @@ from rest_framework.decorators import action
|
||||
|
||||
from applications.task.models import TaskRecord, Task
|
||||
from applications.task.serialziers import FileListSerializer, Id3Serializer, UpdateId3Serializer, \
|
||||
FetchId3ByTitleSerializer, FetchLlyricSerializer, BatchUpdateId3Serializer
|
||||
FetchId3ByTitleSerializer, FetchLlyricSerializer, BatchUpdateId3Serializer, TranslationLycSerializer
|
||||
from applications.task.services.music_resource import MusicResource
|
||||
from applications.task.services.update_ids import update_music_info
|
||||
from applications.task.tasks import full_scan_folder, scan, clear_music, batch_auto_tag_task
|
||||
from applications.utils.translation import translation_lyc_text
|
||||
from component.drf.viewsets import GenericViewSet
|
||||
from django_vue_cli.celery_app import app as celery_app
|
||||
|
||||
@@ -33,6 +34,8 @@ class TaskViewSets(GenericViewSet):
|
||||
return FetchLlyricSerializer
|
||||
elif self.action in ["batch_update_id3", "batch_auto_update_id3"]:
|
||||
return BatchUpdateId3Serializer
|
||||
elif self.action == "translation_lyc":
|
||||
return TranslationLycSerializer
|
||||
return FileListSerializer
|
||||
|
||||
@action(methods=['POST'], detail=False)
|
||||
@@ -46,7 +49,7 @@ class TaskViewSets(GenericViewSet):
|
||||
except FileNotFoundError:
|
||||
return self.failure_response(msg="文件夹不存在")
|
||||
children_data = []
|
||||
allow_type = ["flac", "mp3", "ape", "wav", "aiff", "wv", "tta", "m4a", "ogg", "mpc",
|
||||
allow_type = ["flac", "mp3", "ape", "wav", "aiff", "wv", "tta", "m4a", "ogg", "mpc",
|
||||
"opus", "wma", "dsf", "dff"]
|
||||
frc_map = {}
|
||||
file_data = []
|
||||
@@ -154,7 +157,7 @@ class TaskViewSets(GenericViewSet):
|
||||
if data.get('icon') == 'icon-folder':
|
||||
file_full_path = f"{full_path}/{data.get('name')}"
|
||||
data = os.scandir(file_full_path)
|
||||
allow_type = ["flac", "mp3", "ape", "wav", "aiff", "wv", "tta", "m4a", "ogg", "mpc",
|
||||
allow_type = ["flac", "mp3", "ape", "wav", "aiff", "wv", "tta", "m4a", "ogg", "mpc",
|
||||
"opus", "wma", "dsf", "dff"]
|
||||
for index, entry in enumerate(data, 1):
|
||||
each = entry.name
|
||||
@@ -220,6 +223,40 @@ class TaskViewSets(GenericViewSet):
|
||||
songs = MusicResource(resource).fetch_id3_by_title(title)
|
||||
return self.success_response(data=songs)
|
||||
|
||||
@action(methods=['POST'], detail=False)
|
||||
def translation_lyc(self, request, *args, **kwargs):
|
||||
validate_data = self.is_validated_data(request.data)
|
||||
lyc = validate_data["lyc"]
|
||||
clean_lyc_list = []
|
||||
raw_lyc_list = []
|
||||
for line in lyc.split("\n"):
|
||||
if not line:
|
||||
continue
|
||||
clean_line = line.split("]")[-1]
|
||||
clean_line = clean_line.strip()
|
||||
if not clean_line:
|
||||
continue
|
||||
raw_lyc_list.append(line)
|
||||
clean_lyc_list.append(clean_line)
|
||||
clean_lyc_str = "\n".join(clean_lyc_list)
|
||||
results = translation_lyc_text(clean_lyc_str)
|
||||
new_lyc = []
|
||||
results_list = results.split("\n")
|
||||
for index, result in enumerate(results_list):
|
||||
if not result:
|
||||
new_lyc.append(raw_lyc_list[index])
|
||||
else:
|
||||
try:
|
||||
src = clean_lyc_list[index]
|
||||
raw_src = raw_lyc_list[index]
|
||||
except Exception as e:
|
||||
continue
|
||||
if src.replace(" ", "") == result.replace(" ", ""):
|
||||
new_lyc.append(raw_src)
|
||||
else:
|
||||
new_lyc.append(f"{raw_src}\n「{result}」\n")
|
||||
return self.success_response(data="\n".join(new_lyc))
|
||||
|
||||
@action(methods=["get"], detail=False)
|
||||
def clear_celery(self, request, *args, **kwargs):
|
||||
active_tasks = celery_app.control.inspect().active()
|
||||
|
||||
39
applications/utils/translation.py
Normal file
39
applications/utils/translation.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import hashlib
|
||||
import random
|
||||
import time
|
||||
|
||||
import requests
|
||||
|
||||
import translators as ts
|
||||
|
||||
|
||||
def translation_lyc_text(contents):
|
||||
if len(contents) > 1000:
|
||||
results = []
|
||||
contents = contents.split('\n')
|
||||
content_1k = ""
|
||||
for each in contents:
|
||||
if len(content_1k + each + '\n') > 1000:
|
||||
results.append(content_1k)
|
||||
content_1k = each + '\n'
|
||||
else:
|
||||
content_1k += each + '\n'
|
||||
if content_1k:
|
||||
results.append(content_1k)
|
||||
translate_res = ""
|
||||
for content in results:
|
||||
res = ts.translate_text(content, translator="youdao", to_language="zh-CHS")
|
||||
if translate_res:
|
||||
translate_res += "\n" + res
|
||||
else:
|
||||
translate_res = res
|
||||
return translate_res
|
||||
else:
|
||||
res = ts.translate_text(contents, translator="youdao", to_language="zh-CHS")
|
||||
print(res)
|
||||
return res
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
a = translation_lyc_text('スピード')
|
||||
print(a)
|
||||
0
component/async_request/__init__.py
Normal file
0
component/async_request/__init__.py
Normal file
110
component/async_request/aysnc_req.py
Normal file
110
component/async_request/aysnc_req.py
Normal file
@@ -0,0 +1,110 @@
|
||||
# import asyncio
|
||||
#
|
||||
# import aiohttp
|
||||
# from django.conf import settings
|
||||
#
|
||||
# concurrence_limit = 5
|
||||
#
|
||||
#
|
||||
# def callback(future): # 这里默认传入一个future对象
|
||||
# pass
|
||||
#
|
||||
#
|
||||
# class AsyncRequest:
|
||||
# """
|
||||
# 协程请求
|
||||
# """
|
||||
#
|
||||
# # 限制同时打开的连接的数量 limit=0为不限制
|
||||
# limit = concurrence_limit
|
||||
# timeout = 60
|
||||
# raise_for_status = False
|
||||
#
|
||||
# session = None
|
||||
# result = {}
|
||||
#
|
||||
# @classmethod
|
||||
# def run(cls, main=None):
|
||||
# loop = asyncio.new_event_loop()
|
||||
# try:
|
||||
# asyncio.set_event_loop(loop)
|
||||
# asyncio.get_event_loop().run_until_complete(main)
|
||||
# finally:
|
||||
# try:
|
||||
# if hasattr(loop, "shutdown_asyncgens"):
|
||||
# loop.run_until_complete(loop.shutdown_asyncgens())
|
||||
# finally:
|
||||
# asyncio.set_event_loop(None)
|
||||
# loop.close()
|
||||
#
|
||||
# @classmethod
|
||||
# async def async_request(cls, method, url_data_map, headers=None):
|
||||
# """
|
||||
# method : get,post
|
||||
# """
|
||||
# headers = headers or {}
|
||||
# conn = aiohttp.TCPConnector(limit=cls.limit, verify_ssl=False)
|
||||
# async with aiohttp.ClientSession(connector=conn, headers=headers) as session:
|
||||
# cls.session = session
|
||||
# result_list = await cls.fetch_multi(url_data_map, method=method)
|
||||
# return result_list
|
||||
#
|
||||
# @classmethod
|
||||
# def _request(cls, method, url=None, data_list=None, url_data_map=None, headers=None):
|
||||
# assert url or url_data_map
|
||||
# is_simple = False
|
||||
# if not url_data_map:
|
||||
# data_list = data_list or []
|
||||
# url_data_map = {url: data_list}
|
||||
# is_simple = True
|
||||
# main = cls.async_request(method, url_data_map, headers=headers)
|
||||
# cls.run(main)
|
||||
# if is_simple:
|
||||
# return cls.result.get(url, [])
|
||||
# else:
|
||||
# return cls.result
|
||||
#
|
||||
# @classmethod
|
||||
# def get(cls, url=None, data_list=None, url_data_map=None, headers=None):
|
||||
# """
|
||||
# get请求
|
||||
# example: AsyncRequest.get('url', [{'params': i} for i in range(30)])
|
||||
# AsyncRequest.get(url_data_map={'url':[{'params': i} for i in range(30)]})
|
||||
#
|
||||
# :param url: 请求地址, 与url_data_map只需传一种
|
||||
# :param data_list: 参数列表 [{},{}]
|
||||
# :param url_data_map: url和data_list的映射表
|
||||
# :return: 结果集
|
||||
# """
|
||||
# return cls._request("get", url, data_list, url_data_map, headers)
|
||||
#
|
||||
# @classmethod
|
||||
# def post(cls, url=None, data_list=None, url_data_map=None, headers=None):
|
||||
# return cls._request("post", url, data_list, url_data_map, headers)
|
||||
#
|
||||
# @classmethod
|
||||
# async def fetch(cls, url, data, method, index):
|
||||
# post_data = [{"params": data}, {"data": data}][method == "post"]
|
||||
# try:
|
||||
# async with getattr(cls.session, method)(
|
||||
# url, **post_data, timeout=cls.timeout, raise_for_status=cls.raise_for_status
|
||||
# ) as resp:
|
||||
# ret_data = await resp.json()
|
||||
# cls.result[url][index] = ret_data
|
||||
# return ret_data
|
||||
# except Exception as e:
|
||||
# print("asd", e)
|
||||
#
|
||||
# @classmethod
|
||||
# async def fetch_multi(cls, url_data_map, method):
|
||||
# tasks = []
|
||||
# for url, data_list in url_data_map.items():
|
||||
# cls.result[url] = [None] * len(data_list)
|
||||
# for index, data in enumerate(data_list):
|
||||
# task = asyncio.ensure_future(cls.fetch(url, data, method, index))
|
||||
# task.add_done_callback(callback)
|
||||
# tasks.append(task)
|
||||
#
|
||||
# # gather: 搜集所有future对象,并等待返回
|
||||
# results = await asyncio.wait(tasks)
|
||||
# return results
|
||||
@@ -19,4 +19,5 @@ redis==3.2.0
|
||||
mysqlclient==1.4.4
|
||||
sqlalchemy==1.4.23
|
||||
PyExecJS==1.5.1
|
||||
translators==5.8.0
|
||||
|
||||
|
||||
2
static/dist/index.prod.html
vendored
2
static/dist/index.prod.html
vendored
@@ -1,3 +1,3 @@
|
||||
<!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 rel="shortcut icon" href=/static/dist/img/music-tag.png type=image/x-icon><link href=./static/dist/css/app.css rel=stylesheet></head><body><script>window.siteUrl = "/"
|
||||
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.9ba6c0d4f4490e9a4f28.js></script><script type=text/javascript src=./static/dist/js/vendor.051dd49be048f27f51f9.js></script><script type=text/javascript src=./static/dist/js/app.95d1ec148ea3ea870662.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.9ba6c0d4f4490e9a4f28.js></script><script type=text/javascript src=./static/dist/js/vendor.051dd49be048f27f51f9.js></script><script type=text/javascript src=./static/dist/js/app.f9f7f7356e09293abe67.js></script></body></html>
|
||||
1
static/dist/js/app.95d1ec148ea3ea870662.js
vendored
1
static/dist/js/app.95d1ec148ea3ea870662.js
vendored
File diff suppressed because one or more lines are too long
@@ -30,5 +30,8 @@ export default {
|
||||
},
|
||||
fetchLyric: function(params) {
|
||||
return POST(reUrl + '/api/fetch_lyric/', params)
|
||||
},
|
||||
translationLyc: function(params) {
|
||||
return POST(reUrl + '/api/translation_lyc/', params)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,8 @@
|
||||
</div>
|
||||
<div>
|
||||
<bk-icon type="arrows-right-shape" @click="toggleLock('title')"
|
||||
style="cursor: pointer;color: #64c864;margin-left: 20px;"></bk-icon>
|
||||
style="cursor: pointer;color: #64c864;margin-left: 20px;">
|
||||
</bk-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex;margin-bottom: 10px;align-items: center;">
|
||||
@@ -117,8 +118,13 @@
|
||||
<div style="display: flex;">
|
||||
<div class="label1">歌词:</div>
|
||||
<div style="width: 70%;">
|
||||
<bk-input :clearable="true" v-model="musicInfo.lyrics" type="textarea" :rows="15"
|
||||
></bk-input>
|
||||
<bk-input :clearable="true" v-model="musicInfo.lyrics" type="textarea" :rows="15">
|
||||
</bk-input>
|
||||
</div>
|
||||
<div>
|
||||
<bk-icon type="arrows-right-shape" @click="translation()"
|
||||
style="cursor: pointer;color: #64c864;margin-left: 20px;">
|
||||
</bk-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex;margin-top: 10px;">
|
||||
@@ -308,7 +314,21 @@
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<div v-show="!fadeShowDetail" style="width: 90%;height: 90%; margin: 50px 20px 20px 50px;">
|
||||
<transition name="bk-slide-fade-left">
|
||||
<div v-show="showTranslation">
|
||||
<div style="display: flex;height: 100%;">
|
||||
<bk-icon type="arrows-left-shape" @click="handleCopy('lyric_tran',translationText)"
|
||||
style="margin-right: 5px;margin-left: 15px;margin-top: 50%;cursor: pointer;"></bk-icon>
|
||||
<div style="width: 100%;height: 100%;">
|
||||
<bk-input :clearable="true" v-model="translationText" type="textarea" :rows="50"
|
||||
style="height: 100%;">
|
||||
</bk-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<div v-show="!fadeShowDetail && !showTranslation"
|
||||
style="width: 90%;height: 90%; margin: 50px 20px 20px 50px;">
|
||||
<bk-image fit="contain" :src="'/static/dist/img/music_null-cutout.png'"
|
||||
style="width: 100%;height: 98%;"></bk-image>
|
||||
</div>
|
||||
@@ -354,6 +374,7 @@
|
||||
fullPath: '',
|
||||
fileName: '',
|
||||
resource: 'netease',
|
||||
translationText: '',
|
||||
resourceList: [
|
||||
{id: 'acoustid', name: '音乐指纹识别'},
|
||||
{id: 'netease', name: '网易云音乐'},
|
||||
@@ -376,6 +397,7 @@
|
||||
},
|
||||
fadeShowDir: false,
|
||||
fadeShowDetail: false,
|
||||
showTranslation: false,
|
||||
isLoading: false,
|
||||
SongList: [],
|
||||
reloadImg: true,
|
||||
@@ -520,6 +542,9 @@
|
||||
this.$nextTick(() => {
|
||||
this.reloadImg = true
|
||||
})
|
||||
} else if (k === 'lyric_tran') {
|
||||
console.log(v)
|
||||
this.musicInfo['lyrics'] = v
|
||||
} else {
|
||||
this.musicInfo[k] = v
|
||||
}
|
||||
@@ -541,6 +566,7 @@
|
||||
this.$cwMessage('标题不能为空', 'error')
|
||||
return
|
||||
}
|
||||
this.showTranslation = false
|
||||
this.fadeShowDetail = false
|
||||
this.$api.Task.fetchId3Title({
|
||||
title: this.musicInfo.title,
|
||||
@@ -552,6 +578,19 @@
|
||||
})
|
||||
}
|
||||
},
|
||||
translation() {
|
||||
if (!this.musicInfo.lyrics) {
|
||||
this.$cwMessage('歌词不能为空', 'error')
|
||||
}
|
||||
this.fadeShowDetail = false
|
||||
this.showTranslation = true
|
||||
this.$api.Task.translationLyc({
|
||||
lyc: this.musicInfo.lyrics
|
||||
}).then((res) => {
|
||||
this.showTranslation = true
|
||||
this.translationText = res.data
|
||||
})
|
||||
},
|
||||
// 文件目录
|
||||
handleSearchFile() {
|
||||
this.fadeShowDir = false
|
||||
|
||||
Reference in New Issue
Block a user