This repository has been archived on 2023-08-30. You can view files and clone it, but cannot push or open issues/pull-requests.
appcat
/
sonist
Archived
1
0
Fork 0

修复音乐扫描;本地音乐支持读写ID3

2.x
宇天 2018-12-29 02:10:04 +08:00
parent 05e6e37021
commit 8efd8f2257
10 changed files with 164 additions and 13 deletions

File diff suppressed because one or more lines are too long

View File

@ -143,7 +143,7 @@ table {overflow:auto;display:table;width:100%;line-height:2.5rem;
} }
// //
.module {flex:1;display:flex;flex-flow:column wrap;} .module {position:relative;flex:1;display:flex;flex-flow:column wrap;}
@ -194,7 +194,13 @@ table {overflow:auto;display:table;width:100%;line-height:2.5rem;
.do-mod-contextmenu {width:145px;height:auto;padding:8px 0;line-height:35px;font-size:1.3rem;
li {overflow:hidden;width:100%;height:35px;padding:0 10px;@include ts(background);cursor:default;
&:hover {background:nth($cp, 1)}
i {padding:0 3px;font-size:1.6rem;vertical-align:bottom;}
}
}
.do-layer .layer-box.do-mod-contextmenu__fixed {padding:0}

File diff suppressed because one or more lines are too long

View File

@ -95,6 +95,27 @@
} }
.edit-form {position:absolute;left:0;top:0;z-index:90;display:flex;justify-content:center;align-items:center;width:100%;height:100%;
.form {position:relative;display:flex;flex-flow:column wrap;flex:0 40rem;width:5rem;height:auto;padding:.5rem 4rem 2rem;background:#fff;box-shadow:0 .5rem 2rem rgba(0, 0, 0, .2);
.section {display:flex;flex:1;justify-content:center;align-items:center;margin:1rem 0;
&.title {line-height:2;font-size:1.6rem;
i {position:absolute;right:1rem;top:1rem;color:nth($list: $cr, $n: 1);}
i:hover {transform:scale(1.2);font-weight:bold}
}
.label {flex:0 5rem;padding-right:2rem;color:nth($cgr, 1);text-align:right;}
.field {flex:1}
}
}
}
} }

9
dist/audio/index.js vendored
View File

@ -14,8 +14,8 @@ class AudioPlayer {
constructor() { constructor() {
this.__PLAYER__ = new Audio() this.__PLAYER__ = new Audio()
this.__IS_PLAYED__ = false this.__IS_PLAYED__ = false
this.__LIST__ = [] // 播放列表
this.__CURR__ = -1 // 当前播放的歌曲的id this.__CURR__ = -1 // 当前播放的歌曲的id
this.__LIST__ = [] //播放列表
this.__PLAY_MODE__ = 'all' // all | single | random this.__PLAY_MODE__ = 'all' // all | single | random
this.__PLAYER__.volume = 0.7 this.__PLAYER__.volume = 0.7
@ -176,18 +176,17 @@ util.inherits(AudioPlayer, EventEmitter)
export const ID3 = song => { export const ID3 = song => {
let cmd = `ffprobe -v quiet -print_format json -show_entries format "${song}"` let cmd = `ffprobe -v quiet -print_format json -show_entries format "${song}"`
let pc = exec(cmd) let pc = exec(cmd)
let buf = [] let buf = ''
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
pc.stdout.on('data', _ => { pc.stdout.on('data', _ => {
buf.push(_) buf += _
}) })
pc.stderr.on('data', reject) pc.stderr.on('data', reject)
pc.stdout.on('close', _ => { pc.stdout.on('close', _ => {
let { format } = Buffer.from(buf)
try { try {
res = JSON.parse(res) let { format } = JSON.parse(buf)
resolve({ resolve({
title: format.tags.TITLE || format.tags.title, title: format.tags.TITLE || format.tags.title,
album: format.tags.ALBUM || format.tags.album, album: format.tags.ALBUM || format.tags.album,

2
dist/store/index.js vendored
View File

@ -1 +1 @@
const __STORE__={};function parse$And(_){let t="";for(let e in _){let i=_[e];switch(Anot.type(i)){case"object":if(i.$has){t+=`it.${e}.indexOf(${JSON.stringify(i.$has)}) > -1`;break}if(i.$in){t+=`${JSON.stringify(i.$in)}.indexOf(it.${e}) > -1`;break}if(i.$regex){t+=`${i.$regex}.test(it.${e})`;break}if(i.$lt||i.$lte){t+=`it.${e} <${i.$lte?"=":""} ${i.$lt||i.$lte}`,(i.$gt||i.$gte)&&(t+=` && it.${e} >${i.$gte?"=":""} ${i.$gt||i.$gte}`);break}if(i.$gt||i.$gte){t+=`it.${e} >${i.$gte?"=":""} ${i.$gt||i.$gte}`;break}if(i.$eq){t+=`it.${e} === ${i.$eq}`;break}default:t+=`it.${e} === ${JSON.stringify(_[e])}`}t+=" && "}return(t=t.slice(0,-4))||(t="true"),t}function parse$Or(_){let t="";return _.forEach(_=>{t+="(",t+=parse$And(_),t+=") || "}),t.slice(0,-4)}class AnotStore{constructor(_){Anot.hideProperty(this,"__name__",_),Anot.hideProperty(this,"__LAST_QUERY__",""),Anot.hideProperty(this,"__QUERY_HISTORY__",[]),__STORE__[_]||(__STORE__[_]=[],__STORE__[`${_}Dict`]={})}static collection(_){return new this(_)}__MAKE_FN__(_){let t="\n let result = [];\n let num = 0;\n for (let it of arr) {\n if(";return _.$or?t+=parse$Or(_.$or):t+=parse$And(_),t+="){\n result.push(it)\n num++\n if(limit > 0 && num >= limit){\n break\n }\n }\n }\n return result;",Function("arr","limit",t)}clear(_){this.__QUERY_HISTORY__=[],this.__LAST_QUERY__="",_&&(__STORE__[this.__name__]=[],__STORE__[`${this.__name__}Dict`]={})}getAll({filter:_,limit:t=[]}={}){const e=__STORE__[this.__name__];let i=[],r=!1;if(!e||!e.length)return i;if(t.length<1&&(t=[0]),t.length<2&&_&&(r=!0,t[0]>0&&t.unshift(0)),_){let n=JSON.stringify(_);if(this.__LAST_QUERY__===n)i=this.__QUERY_HISTORY__.slice.apply(this.__QUERY_HISTORY__,t);else{i=this.__MAKE_FN__(_)(e,r&&t[1]||0),r||(this.__LAST_QUERY__=n,this.__QUERY_HISTORY__=i,i=this.__QUERY_HISTORY__.slice.apply(this.__QUERY_HISTORY__,t))}}else i=e.slice.apply(e,t);return Anot.deepCopy(i)}get(_){const t=__STORE__[`${this.__name__}Dict`];return Anot.deepCopy(t[_])||null}count({filter:_}={}){return _?this.__LAST_QUERY__===JSON.stringify(_)?this.__QUERY_HISTORY__.length:this.getAll({filter:_,limit:[0]}).length:__STORE__[this.__name__].length}__INSERT__(_,t){let e=__STORE__[this.__name__],i=__STORE__[`${this.__name__}Dict`],r=_[t||"id"];i[r]?this.update(r,_):(e.push(_),i[r]=_)}insert(_,t){Array.isArray(_)||(_=[_]),_.forEach(_=>{this.__INSERT__(_,t)}),this.clear()}sort(_,t,e){let i="";t&&window.Intl&&(i+="\n let col = new Intl.Collator('zh')\n "),i+=e?"return arr.sort((b, a) => {":"return arr.sort((a, b) => {",i+=`\n let filter = function(val) {\n try {\n return val.${_} || ''\n } catch (err) {\n return ''\n }\n }\n `,t?window.Intl?i+="return col.compare(filter(a), filter(b))":i+="return (filter(a) + '').localeCompare(filter(b), 'zh')":i+="return filter(a) - filter(b)",i+="\n})",Function("arr",i).call(this,__STORE__[this.__name__]),this.clear()}update(_,t){let e=__STORE__[this.__name__],i=__STORE__[`${this.__name__}Dict`],r=i[_],n=e.indexOf(r);Object.assign(r,t),e.splice(n,1,r),i[_]=r}remove(_){let t=__STORE__[this.__name__],e=__STORE__[`${this.__name__}Dict`],i=e[_id],r=t.indexOf(i);t.splice(r,1),delete e[_id]}}Anot.store=window.store=AnotStore;export default AnotStore; const __STORE__={};function parse$And(_){let t="";for(let e in _){let i=_[e];switch(Anot.type(i)){case"object":if(i.$has){t+=`it.${e}.indexOf(${JSON.stringify(i.$has)}) > -1`;break}if(i.$in){t+=`${JSON.stringify(i.$in)}.indexOf(it.${e}) > -1`;break}if(i.$regex){t+=`${i.$regex}.test(it.${e})`;break}if(i.$lt||i.$lte){t+=`it.${e} <${i.$lte?"=":""} ${i.$lt||i.$lte}`,(i.$gt||i.$gte)&&(t+=` && it.${e} >${i.$gte?"=":""} ${i.$gt||i.$gte}`);break}if(i.$gt||i.$gte){t+=`it.${e} >${i.$gte?"=":""} ${i.$gt||i.$gte}`;break}if(i.$eq){t+=`it.${e} === ${i.$eq}`;break}default:t+=`it.${e} === ${JSON.stringify(_[e])}`}t+=" && "}return(t=t.slice(0,-4))||(t="true"),t}function parse$Or(_){let t="";return _.forEach(_=>{t+="(",t+=parse$And(_),t+=") || "}),t.slice(0,-4)}class AnotStore{constructor(_){Anot.hideProperty(this,"__name__",_),Anot.hideProperty(this,"__LAST_QUERY__",""),Anot.hideProperty(this,"__QUERY_HISTORY__",[]),__STORE__[_]||(__STORE__[_]=[],__STORE__[`${_}Dict`]={})}static collection(_){return new this(_)}__MAKE_FN__(_){let t="\n let result = [];\n let num = 0;\n for (let it of arr) {\n if(";return _.$or?t+=parse$Or(_.$or):t+=parse$And(_),t+="){\n result.push(it)\n num++\n if(limit > 0 && num >= limit){\n break\n }\n }\n }\n return result;",Function("arr","limit",t)}clear(_){this.__QUERY_HISTORY__=[],this.__LAST_QUERY__="",_&&(__STORE__[this.__name__]=[],__STORE__[`${this.__name__}Dict`]={})}getAll({filter:_,limit:t=[]}={}){const e=__STORE__[this.__name__];let i=[],r=!1;if(!e||!e.length)return i;if(t.length<1&&(t=[0]),t.length<2&&_&&(r=!0,t[0]>0&&t.unshift(0)),_){let n=JSON.stringify(_);if(this.__LAST_QUERY__===n)i=this.__QUERY_HISTORY__.slice.apply(this.__QUERY_HISTORY__,t);else{i=this.__MAKE_FN__(_)(e,r&&t[1]||0),r||(this.__LAST_QUERY__=n,this.__QUERY_HISTORY__=i,i=this.__QUERY_HISTORY__.slice.apply(this.__QUERY_HISTORY__,t))}}else i=e.slice.apply(e,t);return Anot.deepCopy(i)}get(_){const t=__STORE__[`${this.__name__}Dict`];return Anot.deepCopy(t[_])||null}count({filter:_}={}){return _?this.__LAST_QUERY__===JSON.stringify(_)?this.__QUERY_HISTORY__.length:this.getAll({filter:_,limit:[0]}).length:__STORE__[this.__name__].length}__INSERT__(_,t){let e=__STORE__[this.__name__],i=__STORE__[`${this.__name__}Dict`],r=_[t||"id"];i[r]?this.update(r,_):(e.push(_),i[r]=_)}insert(_,t){Array.isArray(_)||(_=[_]),_.forEach(_=>{this.__INSERT__(_,t)}),this.clear()}sort(_,t,e){let i="";t&&window.Intl&&(i+="\n let col = new Intl.Collator('zh')\n "),i+=e?"return arr.sort((b, a) => {":"return arr.sort((a, b) => {",i+=`\n let filter = function(val) {\n try {\n return val.${_} || ''\n } catch (err) {\n return ''\n }\n }\n `,t?window.Intl?i+="return col.compare(filter(a), filter(b))":i+="return (filter(a) + '').localeCompare(filter(b), 'zh')":i+="return filter(a) - filter(b)",i+="\n})",Function("arr",i).call(this,__STORE__[this.__name__]),this.clear()}update(_,t){let e=__STORE__[this.__name__],i=__STORE__[`${this.__name__}Dict`],r=i[_],n=e.indexOf(r);Object.assign(r,t),e.splice(n,1,r),i[_]=r}remove(_){let t=__STORE__[this.__name__],e=__STORE__[`${this.__name__}Dict`],i=e[_],r=t.indexOf(i);t.splice(r,1),delete e[_]}}Anot.store=window.store=AnotStore;export default AnotStore;

View File

@ -8,7 +8,7 @@
<link href="dist/css/elem-ui.css" rel="stylesheet"> <link href="dist/css/elem-ui.css" rel="stylesheet">
<link href="css/app.css" rel="stylesheet"> <link href="css/app.css" rel="stylesheet">
<link href="css/modules.css" rel="stylesheet"> <link href="css/modules.css" rel="stylesheet">
<script>window.LIBS_BASE_URL = location.origin + '/dist'</script> <script>window.LIBS_BASE_URL = location.origin + '/dist';window.__ENV_LANG__ = 'zh'</script>
<script type="module" src="js/app.js"></script> <script type="module" src="js/app.js"></script>
</head> </head>
<body class="do-fn-noselect"> <body class="do-fn-noselect">

View File

@ -59,7 +59,6 @@ Anot({
volume: Anot.ls('volume') || 70, volume: Anot.ls('volume') || 70,
curr: { curr: {
id: '', id: '',
index: 0,
title: '', title: '',
artist: '', artist: '',
album: '', album: '',

View File

@ -28,7 +28,14 @@ export default Anot({
state: { state: {
list: [], list: [],
curr: '', curr: '',
__APP__: null editMode: false,
form: {
id: '',
title: '',
artist: '',
album: '',
path: ''
}
}, },
mounted() { mounted() {
LS.insert(dbCache) LS.insert(dbCache)
@ -96,6 +103,7 @@ export default Anot({
_P.then(next => { _P.then(next => {
if (next) { if (next) {
Api.getSongInfoByHash(it.kgHash, it.albumId).then(json => { Api.getSongInfoByHash(it.kgHash, it.albumId).then(json => {
delete it.time
it.album = json.album_name it.album = json.album_name
it.albumId = json.album_id it.albumId = json.album_id
it.kgHash = json.hash it.kgHash = json.hash
@ -105,6 +113,9 @@ export default Anot({
this.list.set(idx, it) this.list.set(idx, it)
LS.insert(it) LS.insert(it)
SONIST.clear()
SONIST.push(LS.getAll())
this.__APP__.updateCurr(it) this.__APP__.updateCurr(it)
this.__APP__.draw() this.__APP__.draw()
@ -123,11 +134,14 @@ export default Anot({
el.textContent = '重新扫描' el.textContent = '重新扫描'
el = null el = null
if (this.__NEW_NUM__ > 0) { if (this.__NEW_NUM__ > 0) {
LS.sort('artist', true)
dbCache = LS.getAll() dbCache = LS.getAll()
this.list.clear() this.list.clear()
this.list.pushArray(dbCache) this.list.pushArray(dbCache)
SONIST.clear() SONIST.clear()
SONIST.push(dbCache) SONIST.push(dbCache)
fs.echo(JSON.stringify(dbCache, '', 2), MUSIC_DB_PATH) fs.echo(JSON.stringify(dbCache, '', 2), MUSIC_DB_PATH)
dbCache = null dbCache = null
} }
@ -172,6 +186,88 @@ export default Anot({
ev.target.textContent = '正在扫描, 请稍候...' ev.target.textContent = '正在扫描, 请稍候...'
this.__checkSong__(ev.target) this.__checkSong__(ev.target)
} }
},
closeEdit() {
this.editMode = false
let song = this.list[this.__idx__].$model
Object.assign(song, {
title: this.form.title,
artist: this.form.artist,
album: this.form.album
})
this.list.set(this.__idx__, song)
delete this.__idx__
let col = new Intl.Collator('zh')
this.list.sort((a, b) => {
return col.compare(a.artist, b.artist)
})
LS.update(song.id, song)
LS.sort('artist', true)
SONIST.clear()
SONIST.push(LS.getAll())
fs.echo(JSON.stringify(LS.getAll(), '', 2), MUSIC_DB_PATH)
},
handleMenu(it, idx, ev) {
let that = this
layer.open({
type: 7,
menubar: false,
maskClose: true,
fixed: true,
extraClass: 'do-mod-contextmenu__fixed',
offset: [ev.pageY, 'auto', 'auto', ev.pageX],
shift: {
top: ev.pageY,
left: ev.pageX
},
content: `<ul class="do-mod-contextmenu" :click="onClick">
<li data-key="del"><i class="do-icon-trash"></i></li>
<li data-key="edit"><i class="do-icon-edit"></i></li>
</ul>`,
onClick(ev) {
if (ev.currentTarget === ev.target) {
return
}
let target = ev.target
let act = null
if (target.nodeName === 'I') {
target = target.parentNode
}
act = target.dataset.key
this.close()
if (act === 'del') {
layer.confirm(
'此操作只会将当前选中的歌曲从列表中移出<br>并不会将其从硬盘中删除!',
`是否删除 (${it.title}) ?`,
function() {
this.close()
that.list.splice(idx, 1)
LS.remove(it.id)
SONIST.clear()
SONIST.push(LS.getAll())
fs.echo(JSON.stringify(LS.getAll(), '', 2), MUSIC_DB_PATH)
}
)
} else {
that.__idx__ = idx
that.editMode = true
that.form.id = it.id
that.form.path = it.path.slice(7)
that.form.title = it.title
that.form.artist = it.artist
that.form.album = it.album
}
}
})
} }
} }
}) })

View File

@ -18,6 +18,7 @@
<tr <tr
:class="{active: it.id === curr}" :class="{active: it.id === curr}"
:for="it in list" :for="it in list"
:on-contextmenu="handleMenu(it, $index, $event)"
:dblclick="play(it, $index)"> :dblclick="play(it, $index)">
<td class="ac"><i class="stat s-icon-music"></i></td> <td class="ac"><i class="stat s-icon-music"></i></td>
<td :text="it.title"></td> <td :text="it.title"></td>
@ -28,4 +29,33 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="edit-form" :if="editMode">
<div class="form">
<section class="section title">
歌曲信息编辑
<i class="do-icon-close" :click="closeEdit"></i>
</section>
<section class="section">
<span class="field" :text="form.path"></span>
</section>
<section class="section">
<span class="label">标题</span>
<input class="field do-ui-input" :duplex="form.title">
</section>
<section class="section">
<span class="label">歌手</span>
<input class="field do-ui-input" :duplex="form.artist">
</section>
<section class="section">
<span class="label">专辑</span>
<input class="field do-ui-input" :duplex="form.album">
</section>
</div>
</div>
</div> </div>