music-downloader/html/index.html

335 lines
15 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>音乐下载器-AML</title>
<link rel="stylesheet" href="static/js/iview/styles/iview.css">
<script type="text/javascript" src="static/js/vue.min.js"></script>
<script type="text/javascript" src="static/js/iview/iview.min.js"></script>
<script type="text/javascript" src="static/js/axios.min.js"></script>
<script type="text/javascript" src="static/js/webtoolkit.base64.js"></script>
<style>
.cell {
height: 25px;
border:1px solid #ddd;
}
.gantetu-head {
height: 75px;
border:1px solid #ddd;
}
.stepbar {
height: 20px;
background-color: rgb(21, 196, 88);
}
</style>
</head>
<body>
<div id="app">
<div style="padding:5px;margin-bottom: 20px;">
<div style="color:#bbb;">
<p>暂未做登录验证使用DDNS等方式外网访问存在风险!!!</p>
<p>服务器地址:{{ server_url }}</p>
</div>
</div>
<div style="width:1000px;margin: 0 auto;padding:5px;">
<div style="margin: 10px 0">
<label>选择搜索源:</label>
<Checkbox-Group v-model="selected_sources" style="display: inline-block;">
<Checkbox v-for="item in sources" :label="item" style="padding-right:15px;">{{ item }}</Checkbox>
</Checkbox-Group>
</div>
<div style="margin: 10px 0">
<i-input v-model="keywords" @on-enter="searches" style="display: inline-block;width: 80%;"></i-input>
<i-button :loading="is_searching" @click="searches" style="width:18%">搜索</i-button>
</div>
<Alert v-if="messages.length">
<Tag v-for="msg in messages" :color="msg.status" >{{ msg.message }}</Tag>
</Alert>
<Card v-if="result.length">
<Row style="height:250px;">
<i-col span="6">
<img :src="current_item.pic" style="max-height: 90%; max-width: 90%;" alt="">
</i-col>
<i-col span="8" style="padding:5px;padding-right:20px;">
<template>
<Row style="vertical-align: middle; line-height: 32px;">
<i-col span="4">标题:</i-col>
<i-col span="20">
{{ current_item.title }}
<Tag>{{ current_item.extension || '未知格式' }}</Tag>
<Tag>{{ current_item.source_name }}</Tag>
</i-col>
</Row>
<Row style="vertical-align: middle; line-height: 32px;">
<i-col span="4">作者:</i-col>
<i-col span="20">{{ current_item.author }}</i-col>
</Row>
<Row style="vertical-align: middle; line-height: 32px;">
<i-col span="4">链接:</i-col>
<i-col span="20">
<a v-if="current_item.url" :href="current_item.url" target="_blank" download="download" style="margin-right: 20px;">下载歌曲</a>
<span v-if="!current_item.url" style="margin-right: 20px;color:#aaa;">下载歌曲</span>
<a :href="current_item.link" target="_blank">源链接</a>
</i-col>
</Row>
<Row style="margin: 10px 0;">
<i-col span="4">存储:</i-col>
<i-col span="20">
<p>
<Checkbox v-model="is_store_music">歌曲</Checkbox>
<Checkbox v-model="is_store_lrc">歌词</Checkbox>
<Checkbox v-model="is_store_pic">封面</Checkbox>
</p>
<p>
<i-input v-model="store_path" style="display: inline-block;"></i-input>
<p style="color:#888;word-break:break-all;">{{ renderedStorePath }}</p>
<i-button :loading="is_storing" @click="handleStoreClick">确认存储</i-button>
<p style="color:green;">{{ store_message }}</p>
</p>
</i-col>
</Row>
</template>
</i-col>
<i-col span="10" style="padding:5px;height:250px;">
<i-input v-model="current_item.lrc" type="textarea" :autosize="{minRows: 11,maxRows: 11}"></i-input>
</i-col>
</Row>
<div style="margin-top:10px;">
<audio :src="current_item.url" controls style="width:100%;"></audio>
</div>
</Card>
<div style="margin:10px 0;">
<Row>
<i-col v-for="(item2, index) in result" span="8" style="padding:5px;">
<Card style="height:125px;overflow:hidden;">
<div @click="handleSelectItem(item2)">
<Row>
<i-col span="8">
<img :src="item2.pic" style="max-height: 90px; max-width: 90%;" alt="">
</i-col>
<i-col span="16">
<p style="display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;" :title="item2.title">{{ item2.title }}</p>
<p style="color:#bbb">{{ item2.author }}</p>
<p><Tag>{{ item2.extension || '未知格式' }}</Tag><Tag :color="['default','primary','success','error','warning','magenta','red','volcano','orange','gold','yellow'][item2.number%11]">{{ item2.source_name }}</Tag></p>
</i-col>
</Row>
</div>
</Card>
</i-col>
</Row>
</div>
</div>
</div>
</body>
<script type="text/javascript">
new Vue({
el: "#app",
data() {
let server_url = document.location.hostname == '' ? 'http://localhost:5750' : `http://${document.location.hostname}:5750`;
const ajax = axios.create({
baseURL: server_url,
timeout: 20000,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
//'Access-Control-Allow-Origin': true
}
});
return {
server_url: server_url,
keywords: '',
is_searching: false,
sources: [],
selected_sources: [],
result: [],
messages: [],
current_item: {},
store_path_root: "/downloads/",
store_path: "{author}/{title}-{author}",
is_store_pic: true,
is_store_music: true,
is_store_lrc: true,
is_storing: false,
store_message: '',
ajax: ajax
}
},
computed: {
renderedStorePath() {
let current_item = this.current_item;
let title = current_item.title.replace(/[\/\\\:]+/g, '_');
let author = current_item.author.replace(/[\/\\\:]+/g, '_');
return this.store_path_root + this.store_path.replaceAll("{title}", title).replaceAll("{author}", author) + '.' + this.current_item.extension;
}
},
methods: {
ajaxHandle(prms) {
return new Promise((resolve, reject) => {
prms.then(response => {
if(response.data.errno == -1) {
this.$Message.error(response.data.message);
console.error(response.data.message);
}else {
resolve(response);
}
}).catch(err => {
reject(err);
});
});
},
get(url, query) {
if(query) url = url + '?' + qs(query);
return this.ajaxHandle(this.ajax.get(url));
},
post(url, data) {
return this.ajaxHandle(this.ajax.post(url, qs(data || {})));
},
async searches() {
let selected_sources = this.selected_sources.map(item => item);
let keywords = this.keywords;
if(selected_sources.length == 0 || !keywords || keywords.trim() == '') {
return;
}
this.is_searching = true;
this.messages = []; // 清空消息
this.$set(this, 'result', []);
let result = [];
let htmlparser = new DOMParser();
for(let i = 0; i < selected_sources.length; i++) {
console.log(i);
let list = await this.search(selected_sources[i], keywords, result);
list.forEach(item => {
item.number = i; //主要用来打标记
item.lrc = item.lrc ? htmlparser.parseFromString(item.lrc, 'text/html').documentElement.textContent : ''; // 防止有转义字符
result.push(item);
});
}
this.$set(this, 'result', result);
this.is_searching = false;
if(result.length > 0) {
this.handleSelectItem(result[0]);
}
},
handleSelectItem(item) {
this.store_message = '';
this.$set(this, 'current_item', item);
},
search(source_name, keywords, result) {
return new Promise((resolve, reject) => {
this.get('/api/search', {
source_name: source_name,
keywords: keywords
}).then(response => {
var list = response.data.data;
if(list instanceof Array && list.length > 0) {
this.messages.push({status: 'success', message: source_name + '共' + list.length + '条搜索结果'});
resolve(list);
}else {
this.messages.push({status: 'warning', message: source_name + '无搜索结果'});
resolve([]);
}
}).catch(err => {
this.messages.push({status: 'error', message: source_name + err});
console.error(err.message);
resolve([]);
});
});
},
async handleStoreClick() {
this.is_storing = true;
let is_exists = await this.handleCheckMusicExists();
if(is_exists) {
this.$Modal.confirm({
title: '同名文件处理方式',
content: '歌曲已经存在,是否覆盖?',
onOk: () => {
this.handleStore();
},
onCancel: () => {
this.is_storing = false;
}
});
}else {
this.handleStore();
}
},
async handleCheckMusicExists() {
let response = await this.get('/api/store/check', {
music_path: this.renderedStorePath
});
return response.data.data;
},
handleStore() {
this.store_message = '';
if(getExtension(this.renderedStorePath) == '') {
this.$Message.error("请指定有效歌曲后缀名");
return;
}
this.get('/api/store', {
music_path: this.renderedStorePath,
music_url: this.current_item.url,
lrc_content: this.current_item.lrc,
pic_url: this.current_item.pic,
store_music: this.is_store_music + 0,
store_lrc: this.is_store_lrc + 0,
store_pic: this.is_store_pic + 0
}).then(response => {
this.store_message = response.data.message;
this.$Message.success(response.data.message);
this.is_storing = false;
}).catch(err => {
this.$Message.error(err.message);
this.is_storing = false;
});
},
getSources() {
this.is_searching = true;
this.get('/api/source/all').then(response => {
this.sources = response.data.data;
if(this.sources.length > 0) {
// 默认选中第一个
this.selected_sources = [this.sources[0]];
}
this.is_searching = false;
}).catch(err => {
this.$Message.error(err.message);
this.is_searching = false;
});
}
},
created() {
this.getSources();
}
});
function qs(data) {
let result = "";
for(const i in data) {
let value = data[i];
if(value instanceof Date) value = value.getTime();
// encodeURI不能转义&字符,但是命令中描述符重定向需要用到&
result += i + "=" + encodeURIComponent(value) + "&";
}
return result;
}
function getExtension(url) {
var ext = url.split('/').pop().split('.').pop().toLowerCase();
var exts = ['mp3', 'acc', 'aiff', 'ape', 'au', 'flac', 'm4a', 'mmf', 'opus', 'voc', 'wav', 'ogg', 'ra', 'dvf', 'taa', 'dsf', 'diff', 'dts', 'wma'];
if(exts.indexOf(ext) >= 0) {
return ext;
}else {
return '';
}
}
// 图片加载失败使用默认图片
document.addEventListener('error', function (e) {
var elem = e.target;
if (elem.tagName.toLowerCase() == 'img') {
elem.src = './static/img/default_cover.png';
}
}, true);
</script>
</html>