mirror of
https://github.com/xhongc/music-tag-web.git
synced 2026-04-24 18:50:27 +08:00
feature:新增整理文件夹
This commit is contained in:
@@ -50,3 +50,11 @@ class FetchLlyricSerializer(serializers.Serializer):
|
||||
|
||||
class TranslationLycSerializer(serializers.Serializer):
|
||||
lyc = serializers.CharField(required=True)
|
||||
|
||||
|
||||
class TidyFolderSerializer(serializers.Serializer):
|
||||
root_path = serializers.CharField(required=True)
|
||||
first_dir = serializers.CharField(required=True)
|
||||
second_dir = serializers.CharField(required=False, allow_null=True, allow_blank=True)
|
||||
file_full_path = serializers.JSONField(required=True)
|
||||
select_data = serializers.JSONField(required=True)
|
||||
@@ -14,12 +14,16 @@ from PIL import Image
|
||||
|
||||
|
||||
class MusicInfo:
|
||||
def __init__(self, folder: Folder):
|
||||
self.file = music_tag.load_file(folder.path)
|
||||
self.path = folder.path
|
||||
self.folder_name = folder.name
|
||||
self.file_type = folder.file_type
|
||||
self.parent_id = folder.parent_id
|
||||
def __init__(self, folder):
|
||||
if isinstance(folder, Folder):
|
||||
self.file = music_tag.load_file(folder.path)
|
||||
self.path = folder.path
|
||||
self.folder_name = folder.name
|
||||
self.file_type = folder.file_type
|
||||
self.parent_id = folder.parent_id
|
||||
else:
|
||||
self.file = music_tag.load_file(folder)
|
||||
self.path = folder
|
||||
|
||||
@property
|
||||
def album_name(self):
|
||||
@@ -29,10 +33,23 @@ class MusicInfo:
|
||||
album_name = "未知专辑"
|
||||
return self.file["album"].value
|
||||
|
||||
@property
|
||||
def album(self):
|
||||
album_name = self.file["album"].value
|
||||
album_name = album_name.replace(" ", "")
|
||||
if not album_name:
|
||||
album_name = "未知专辑"
|
||||
return album_name
|
||||
return self.file["album"].value
|
||||
|
||||
@property
|
||||
def artist_name(self):
|
||||
return self.file["artist"].value
|
||||
|
||||
@property
|
||||
def artist(self):
|
||||
return self.file["artist"].value
|
||||
|
||||
@property
|
||||
def year(self):
|
||||
try:
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import datetime
|
||||
import os
|
||||
import shutil
|
||||
import time
|
||||
import uuid
|
||||
from collections import defaultdict
|
||||
|
||||
import music_tag
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
|
||||
@@ -10,7 +13,7 @@ from applications.music.models import Folder, Track, Album, Genre, Artist, Attac
|
||||
from applications.subsonic.constants import AUDIO_EXTENSIONS_AND_MIMETYPE, COVER_TYPE
|
||||
from applications.task.models import TaskRecord, Task
|
||||
from applications.task.services.music_resource import MusicResource
|
||||
from applications.task.services.scan_utils import ScanMusic
|
||||
from applications.task.services.scan_utils import ScanMusic, MusicInfo
|
||||
from applications.task.utils import folder_update_time, exists_dir, match_song
|
||||
from django_vue_cli.celery_app import app
|
||||
|
||||
@@ -245,7 +248,7 @@ def batch_auto_tag_task(batch, source_list, select_mode):
|
||||
folder_list = TaskRecord.objects.filter(batch=batch, icon="icon-folder").all()
|
||||
for folder in folder_list:
|
||||
data = os.scandir(folder.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"]
|
||||
bulk_set = []
|
||||
for entry in data:
|
||||
@@ -287,3 +290,39 @@ def batch_auto_tag_task(batch, source_list, select_mode):
|
||||
"parent_path": parent_path,
|
||||
"filename": os.path.basename(task.full_path)
|
||||
})
|
||||
|
||||
|
||||
def tidy_folder_task(music_path_list, tidy_config):
|
||||
root_path = tidy_config.get("root_path")
|
||||
first_dir = tidy_config.get("first_dir")
|
||||
second_dir = tidy_config.get("second_dir")
|
||||
|
||||
if second_dir:
|
||||
tidy_map = defaultdict(lambda: defaultdict(list))
|
||||
for music_path in music_path_list:
|
||||
file = MusicInfo(music_path)
|
||||
first_value = getattr(file, first_dir, "未知")
|
||||
second_value = getattr(file, second_dir, "未知")
|
||||
tidy_map[first_value][second_value].append(music_path)
|
||||
for first_value, second_map in tidy_map.items():
|
||||
first_path = os.path.join(root_path, first_value)
|
||||
if not os.path.exists(first_path):
|
||||
os.makedirs(first_path)
|
||||
for second_value, music_path_list in second_map.items():
|
||||
second_path = os.path.join(first_path, second_value)
|
||||
if not os.path.exists(second_path):
|
||||
os.makedirs(second_path)
|
||||
for music_path in music_path_list:
|
||||
shutil.move(music_path, second_path)
|
||||
else:
|
||||
tidy_map = defaultdict(list)
|
||||
for music_path in music_path_list:
|
||||
file = MusicInfo(music_path)
|
||||
first_value = getattr(file, first_dir, "未知")
|
||||
tidy_map[first_value].append(music_path)
|
||||
for first_value, music_path_list in tidy_map.items():
|
||||
first_path = os.path.join(root_path, first_value)
|
||||
if not os.path.exists(first_path):
|
||||
os.makedirs(first_path)
|
||||
for music_path in music_path_list:
|
||||
shutil.move(music_path, first_path)
|
||||
|
||||
@@ -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, TranslationLycSerializer
|
||||
FetchId3ByTitleSerializer, FetchLlyricSerializer, BatchUpdateId3Serializer, TranslationLycSerializer, \
|
||||
TidyFolderSerializer
|
||||
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.task.tasks import full_scan_folder, scan, clear_music, batch_auto_tag_task, tidy_folder_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
|
||||
@@ -36,6 +37,8 @@ class TaskViewSets(GenericViewSet):
|
||||
return BatchUpdateId3Serializer
|
||||
elif self.action == "translation_lyc":
|
||||
return TranslationLycSerializer
|
||||
elif self.action == "tidy_folder":
|
||||
return TidyFolderSerializer
|
||||
return FileListSerializer
|
||||
|
||||
@action(methods=['POST'], detail=False)
|
||||
@@ -259,6 +262,32 @@ class TaskViewSets(GenericViewSet):
|
||||
new_lyc.append(f"{raw_src}\n「{result}」\n")
|
||||
return self.success_response(data="\n".join(new_lyc))
|
||||
|
||||
@action(methods=['POST'], detail=False)
|
||||
def tidy_folder(self, request, *args, **kwargs):
|
||||
validate_data = self.is_validated_data(request.data)
|
||||
root_path = validate_data["root_path"]
|
||||
first_dir = validate_data["first_dir"]
|
||||
full_path = validate_data["file_full_path"]
|
||||
select_data = validate_data["select_data"]
|
||||
second_dir = validate_data.get("second_dir", "")
|
||||
music_id3_info = []
|
||||
for data in select_data:
|
||||
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",
|
||||
"opus", "wma", "dsf", "dff"]
|
||||
for index, entry in enumerate(data, 1):
|
||||
each = entry.name
|
||||
file_type = each.split(".")[-1]
|
||||
if file_type not in allow_type:
|
||||
continue
|
||||
music_id3_info.append(f"{file_full_path}/{each}")
|
||||
else:
|
||||
music_id3_info.append(f"{full_path}/{data.get('name')}")
|
||||
tidy_folder_task(music_id3_info, {"root_path": root_path, "first_dir": first_dir, "second_dir": second_dir})
|
||||
return self.success_response()
|
||||
|
||||
@action(methods=["get"], detail=False)
|
||||
def clear_celery(self, request, *args, **kwargs):
|
||||
active_tasks = celery_app.control.inspect().active()
|
||||
|
||||
@@ -156,12 +156,12 @@ REST_FRAMEWORK = {
|
||||
|
||||
JWT_AUTH = {
|
||||
# 过期时间,生成的took七天之后不能使用
|
||||
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
|
||||
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7),
|
||||
# 刷新时间 之后的token时间值
|
||||
# 'JWT_ALLOW_REFRESH': True,
|
||||
'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=1),
|
||||
'JWT_ALLOW_REFRESH': True,
|
||||
'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7),
|
||||
# 请求头携带的参数
|
||||
'JWT_AUTH_HEADER_PREFIX': 'JWT',
|
||||
'JWT_AUTH_HEADER_PREFIX': 'JWT'
|
||||
}
|
||||
BASE_URL = "https://music.163.com/"
|
||||
REVERSE_PROXY_TYPE = "nginx"
|
||||
|
||||
2
static/dist/css/app.css
vendored
2
static/dist/css/app.css
vendored
File diff suppressed because one or more lines are too long
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.fec4fc7ab40038232872.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.f642b95eddd98320a0c9.js></script></body></html>
|
||||
1
static/dist/js/app.f642b95eddd98320a0c9.js
vendored
Normal file
1
static/dist/js/app.f642b95eddd98320a0c9.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/dist/js/app.fec4fc7ab40038232872.js
vendored
1
static/dist/js/app.fec4fc7ab40038232872.js
vendored
File diff suppressed because one or more lines are too long
@@ -25,6 +25,9 @@ export default {
|
||||
batchAutoUpdateId3: function(params) {
|
||||
return POST(reUrl + '/api/batch_auto_update_id3/', params)
|
||||
},
|
||||
tidyFolder: function(params) {
|
||||
return POST(reUrl + '/api/tidy_folder/', params)
|
||||
},
|
||||
fetchId3Title: function(params) {
|
||||
return POST(reUrl + '/api/fetch_id3_by_title/', params)
|
||||
},
|
||||
|
||||
@@ -56,7 +56,7 @@ Vue.prototype.setCookie = function(name, value, day) {
|
||||
const curTamp = curDate.getTime()
|
||||
const curWeeHours = new Date(curDate.toLocaleDateString()).getTime() - 1
|
||||
const passedTamp = curTamp - curWeeHours
|
||||
const leftTamp = 24 * 60 * 60 * 1000 - passedTamp
|
||||
const leftTamp = 7 * 24 * 60 * 60 * 1000 - passedTamp
|
||||
const leftTime = new Date()
|
||||
leftTime.setTime(leftTamp + curTamp)
|
||||
document.cookie = name + '=' + escape(value) + ';expires=' + leftTime.toGMTString()
|
||||
|
||||
@@ -176,6 +176,13 @@
|
||||
自动批量修改
|
||||
</bk-button>
|
||||
</div>
|
||||
<div style="width: 100%;display: flex;margin-top: 10px;">
|
||||
<bk-button :theme="'success'" :loading="isLoading"
|
||||
@click="exampleSetting2.primary.visible = true" class="mr10"
|
||||
style="width: 50%;">
|
||||
整理文件夹
|
||||
</bk-button>
|
||||
</div>
|
||||
<div style="display: flex;margin-bottom: 10px;align-items: center;margin-top: 10px;">
|
||||
<div class="label1" v-bk-tooltips="'变量名:${title}'">标题:</div>
|
||||
<div style="width: 70%;">
|
||||
@@ -386,6 +393,38 @@
|
||||
</bk-option>
|
||||
</bk-select>
|
||||
</bk-dialog>
|
||||
<bk-dialog v-model="exampleSetting2.primary.visible"
|
||||
theme="primary"
|
||||
:mask-close="false"
|
||||
@confirm="handleTidy"
|
||||
:header-position="exampleSetting2.primary.headerPosition"
|
||||
title="自动批量修改">
|
||||
<p>整理文件夹,按一级目录,二级目录选定的信息分类</p>
|
||||
<div>整理后的根目录</div>
|
||||
<div class="input-demo">
|
||||
<bk-input v-model="tidyFormData.root_path">
|
||||
</bk-input>
|
||||
</div>
|
||||
<div>一级目录</div>
|
||||
<bk-select style="width: 250px;"
|
||||
:clearable="false"
|
||||
v-model="tidyFormData.first_dir">
|
||||
<bk-option v-for="option in tidyList"
|
||||
:key="option.id"
|
||||
:id="option.id"
|
||||
:name="option.name">
|
||||
</bk-option>
|
||||
</bk-select>
|
||||
<div>二级目录</div>
|
||||
<bk-select style="width: 250px;"
|
||||
v-model="tidyFormData.second_dir">
|
||||
<bk-option v-for="option in tidyList"
|
||||
:key="option.id"
|
||||
:id="option.id"
|
||||
:name="option.name">
|
||||
</bk-option>
|
||||
</bk-select>
|
||||
</bk-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
@@ -407,6 +446,13 @@
|
||||
{id: 'kugou', name: '酷狗音乐'},
|
||||
{id: 'kuwo', name: '酷我音乐'}
|
||||
],
|
||||
tidyList: [
|
||||
{id: 'title', name: '标题'},
|
||||
{id: 'artist', name: '艺术家'},
|
||||
{id: 'album', name: '专辑'},
|
||||
{id: 'genre', name: '风格'},
|
||||
{id: 'comment', name: '描述'}
|
||||
],
|
||||
baseMusicInfo: {
|
||||
'genre': '流行',
|
||||
'is_save_lyrics_file': false
|
||||
@@ -444,11 +490,22 @@
|
||||
checkedData: [],
|
||||
selectAutoMode: 'hard',
|
||||
sourceList: [],
|
||||
tidyFormData: {
|
||||
root_path: '/app/media/',
|
||||
first_dir: 'artist',
|
||||
second_dir: ''
|
||||
},
|
||||
exampleSetting1: {
|
||||
primary: {
|
||||
visible: false,
|
||||
headerPosition: 'left'
|
||||
}
|
||||
},
|
||||
exampleSetting2: {
|
||||
primary: {
|
||||
visible: false,
|
||||
headerPosition: 'left'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -704,6 +761,32 @@
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
handleTidy() {
|
||||
this.$bkInfo({
|
||||
title: '确认要整理文件夹?',
|
||||
confirmLoading: true,
|
||||
confirmFn: () => {
|
||||
try {
|
||||
this.isLoading = true
|
||||
this.tidyFormData['file_full_path'] = this.filePath
|
||||
this.tidyFormData['select_data'] = this.checkedData
|
||||
this.$api.Task.tidyFolder(this.tidyFormData).then((res) => {
|
||||
this.isLoading = false
|
||||
console.log(res)
|
||||
if (res.result) {
|
||||
this.$cwMessage('创建成功', 'success')
|
||||
} else {
|
||||
this.$cwMessage('创建失败', 'error')
|
||||
}
|
||||
})
|
||||
return true
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
methods: {
|
||||
submitLogin() {
|
||||
this.$api.Task.login(this.formData).then((res) => {
|
||||
this.setCookie('AUTHORIZATION', 'jwt ' + res.token)
|
||||
this.setCookie('AUTHORIZATION', 'jwt ' + res.token, 7)
|
||||
this.$router.push({name: 'home'})
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user