1+
2+ /*
3+ * SimMusic 内置本地音乐加载器
4+ * 亦可用作扩展开发示例以添加其他音乐源
5+ * 若无特殊说明,基本所有的file变量格式都是“scheme: + <id>”,自己开发时候请不要忘了添加scheme:前缀
6+ */
7+
8+
9+ /**************** 基础配置 ****************/
10+ // ExtensionRuntime在加载时会自动添加json中的scheme字段到ExtensionConfig下,所以无需担心ExtensionConfig.xxx是否存在
11+ ExtensionConfig . file . uiName = "目录" ;
12+ // 当没有config.setItem时,调用config.getItem会返回defaultConfig中的值
13+ defaultConfig [ "folderLists" ] = [ ] ;
14+
15+
16+ /**************** 工具函数 ****************/
17+ // 这些函数是插件自己需要的函数,个人推荐const一个object然后都用它存放,防止和主程序内置函数名冲突
18+ const FileExtensionTools = {
19+ scanMusic ( directory ) {
20+ try {
21+ const supportedExtensions = config . getItem ( "musicFormats" ) . split ( " " ) ;
22+ let list = [ ] ;
23+ fs . readdirSync ( directory ) . forEach ( file => {
24+ const fullPath = path . join ( directory , file ) ;
25+ if ( fs . statSync ( fullPath ) . isDirectory ( ) ) {
26+ list = list . concat ( this . scanMusic ( fullPath ) ) ;
27+ } else {
28+ const ext = path . extname ( fullPath ) . toLowerCase ( ) ;
29+ if ( supportedExtensions . includes ( ext ) ) {
30+ // 请务必以插件定义的“scheme:”开头,不然扫描元数据和获取播放链接的时候不知道问哪个插件调方法
31+ // 后面的随意,例如在线歌曲可以使用“xxmusic:114514”,用歌曲id来,你喜欢就好
32+ // 一首歌的内部id应该是唯一的(用于播放和元数据索引),不然歌曲索引的时候会重复请求数据,消耗无意义的资源
33+ list . push ( "file:" + fullPath ) ;
34+ }
35+ }
36+ } ) ;
37+ return list ;
38+ } catch { return [ ] ; }
39+ }
40+ }
41+
42+
43+ /**************** 左侧导航 ****************/
44+ // 如果你懒,这个字段可以不写,这样插件就没有左侧导航功能(你可以参考下面的写搜索功能)
45+ ExtensionConfig . file . musicList = {
46+ // 这个函数用于处理用户点击歌单“加号”的事件
47+ // 如果没有(例如你的插件是自动同步一个用户的所有歌单),可以不写,这样加号图标就不会显示
48+ add ( callback ) {
49+ // 这里自己实现添加逻辑,简单输入可直接调内置的 prompt(placeholder:str, callback:function) 方法
50+ ipcRenderer . invoke ( "pickFolder" )
51+ . then ( dir => {
52+ if ( ! dir || ! dir [ 0 ] ) return ;
53+ dir = dir [ 0 ] . trim ( ) . replaceAll ( "/" , "\\" ) ;
54+ // 内置config读取可用getItem
55+ const lists = config . getItem ( "folderLists" ) ;
56+ // 由于数据格式由开发者自行定义,重复导入 & 其他错误需要开发者自行处理
57+ if ( dir . split ( "\\" ) . length == 2 ) return alert ( "您不能导入磁盘根目录。" ) ;
58+ if ( lists . includes ( dir ) ) return alert ( "此目录已被添加到目录列表中。" ) ;
59+ lists . push ( dir ) ;
60+ // 内置config写入可用setItem
61+ config . setItem ( "folderLists" , lists ) ;
62+ // 导入成功后需开发者自行调用callback以更新左侧显示内容(必须),switchList以打开刚才导入的歌单(可选)
63+ callback ( ) ;
64+ ExtensionConfig . file . musicList . switchList ( dir ) ;
65+ } ) ;
66+ } ,
67+ // 这个函数用于渲染左侧的歌单列表
68+ renderList ( container ) {
69+ const lists = config . getItem ( "folderLists" ) ;
70+ lists . forEach ( name => {
71+ const spilitted = name . split ( "\\" ) ;
72+ const folderName = spilitted [ spilitted . length - 1 ] ;
73+ // 创建一个div即可,可以不需要有类名
74+ const element = document . createElement ( "div" ) ;
75+ element . innerText = folderName ;
76+ element . dataset . folderName = name ;
77+ // 处理点击,一般直接switchList即可
78+ element . onclick = ( ) => { this . switchList ( name ) ; } ;
79+ // 创建右键菜单,具体使用方法参考 zhujin917/3sqrt7-context-menu/README.md
80+ element . oncontextmenu = event => {
81+ new ContextMenu ( [
82+ { label : "查看歌曲" , click ( ) { element . click ( ) ; } } ,
83+ { label : "在资源管理器中显示" , click ( ) { shell . openPath ( name ) ; } } ,
84+ { type : "separator" } ,
85+ { label : "从列表中移除" , click ( ) {
86+ confirm ( `目录「${ folderName } 」将从 SimMusic 目录列表中移除,但不会从文件系统中删除。是否继续?` , ( ) => {
87+ const lists = config . getItem ( "folderLists" ) ;
88+ lists . splice ( lists . indexOf ( name ) , 1 ) ;
89+ config . setItem ( "folderLists" , lists ) ;
90+ if ( element . classList . contains ( "active" ) ) switchRightPage ( "rightPlaceholder" ) ;
91+ element . remove ( ) ;
92+ } ) ;
93+ } } ,
94+ ] ) . popup ( [ event . clientX , event . clientY ] ) ;
95+ } ;
96+ // 把div附加到左侧界面,container会由ExtensionRuntime自动传入,无需担心是否存在
97+ container . appendChild ( element ) ;
98+ } ) ;
99+ } ,
100+ // 这个函数用于切换歌单
101+ switchList ( name ) {
102+ const spilitted = name . split ( "\\" ) ;
103+ // 由于搜索和歌单共用界面,需要先隐藏搜索顶栏、显示歌单顶栏,因此这两行请勿删除
104+ document . querySelector ( ".musicListTitle" ) . hidden = false ;
105+ document . querySelector ( ".searchTitle" ) . hidden = true ;
106+ // 这仨是渲染右侧顶部界面使用,分别为标题和目录,如果你的插件没有“目录”的概念(例如在线歌单)把第二行hidden=true然后删掉第三行就可以了
107+ document . getElementById ( "musicListName" ) . innerText = spilitted [ spilitted . length - 1 ] ;
108+ document . getElementById ( "folderDir" ) . hidden = false ;
109+ document . getElementById ( "musicListDir" ) . innerText = name ;
110+ // 统一调用renderMusicList即可,第二个参数需要传入一个用于识别“当前歌单”的唯一的参数,推荐使用插件名+歌单id以防重复
111+ // 如果你的scanMusic必须是异步的,可以先renderMusicList([], id)以切换界面,再renderMusicList(list, id),id一样就可以
112+ renderMusicList ( FileExtensionTools . scanMusic ( name ) , "folder-" + name ) ;
113+ // 这个用于把当前歌单标蓝,放在renderMusicList函数后运行,推荐借鉴我的写法在renderList函数里自己设一个dataset,然后遍历dataset
114+ document . querySelectorAll ( ".left .leftBar div" ) . forEach ( ele => {
115+ if ( ele . dataset . folderName != name ) ele . classList . remove ( "active" ) ;
116+ else ele . classList . add ( "active" ) ;
117+ } ) ;
118+ } ,
119+ } ;
120+
121+
122+ /**************** 获取数据 ****************/
123+ // 这个函数用于读取音乐元数据,不管你是本地还是在线,无所谓你咋获取,最后都调callback(data)就行。
124+ // 如果是在线的用fetch就更好做,直接修改我musicmetadata的promise就得
125+ //【注意:读取失败可以返回null,各字段值可以没有】
126+ ExtensionConfig . file . readMetadata = async ( file ) => {
127+ file = file . replace ( "file:" , "" ) ;
128+ try {
129+ const metadata = await musicMetadata . parseFile ( file )
130+ let nativeLyrics ;
131+ for ( const tagType in metadata . native ) {
132+ if ( metadata . native [ tagType ] . forEach ) metadata . native [ tagType ] . forEach ( tag => {
133+ if ( tag . value && tag . value . text && tag . value . text . match && tag . value . text . match ( / \[ \d + \: \d + \. \d + \] / g) ) {
134+ nativeLyrics = tag . value . text ;
135+ }
136+ } ) ;
137+ }
138+ const metadataArtist = metadata . common . artists ? metadata . common . artists . join ( ", " ) : null || metadata . common . artist ;
139+ const metadataCover = metadata . common . picture ? metadata . common . picture [ 0 ] ? metadata . common . picture [ 0 ] . data : null : null ;
140+ return {
141+ title : metadata . common . title ,
142+ artist : metadataArtist ,
143+ album : metadata . common . album ? metadata . common . album : file . split ( "\\" ) [ file . split ( "\\" ) . length - 2 ] ,
144+ time : metadata . format . duration ,
145+ cover : metadataCover ? metadataCover : "" ,
146+ lyrics : metadata . common . lyrics || nativeLyrics ,
147+ } ;
148+ } catch {
149+ return { } ;
150+ }
151+ } ;
152+
153+
154+ /**************** 歌曲播放 ****************/
155+ ExtensionConfig . file . player = {
156+ // 这个函数用于获取播放地址,返回值可以是本地文件地址 / http(s)地址 / blob地址 / base64 dataurl,不成功可以用空参数调callback
157+ //【注意:读取失败return可以用空串】
158+ async getPlayUrl ( file ) {
159+ return file . replace ( "file:" , "" ) ;
160+ } ,
161+ // 这个函数用于(在本地索引没有歌词的情况下获取歌词),例如在线播放时把歌词全部写到索引不太现实,就会调用这个方法直接读取
162+ //【注意:读取失败return可以用空串】
163+ async getLyrics ( file ) {
164+ file = file . replace ( "file:" , "" ) ;
165+ const lastDotIndex = file . lastIndexOf ( "." ) ;
166+ lrcPath = file . substring ( 0 , lastDotIndex ) + ".lrc" ;
167+ try { return fs . readFileSync ( lrcPath , "utf8" ) ; }
168+ catch { return "" ; }
169+ } ,
170+ } ;
171+
172+
173+ /**************** 歌曲搜索 ****************/
174+ ExtensionConfig . file . search = async keyword => {
175+ let fileArray = [ ] ;
176+ let resultArray = [ ] ;
177+ config . getItem ( "folderLists" ) . forEach ( folder => {
178+ fileArray = fileArray . concat ( FileExtensionTools . scanMusic ( folder ) ) ;
179+ } ) ;
180+ fileArray . forEach ( file => {
181+ if ( SimMusicTools . getTitleFromPath ( file ) . includes ( keyword ) ) resultArray . push ( file ) ;
182+ else if ( lastMusicIndex [ file ] ) {
183+ if ( Object . values ( lastMusicIndex [ file ] ) . join ( " " ) . includes ( keyword ) ) resultArray . push ( file ) ;
184+ }
185+ } ) ;
186+ return resultArray ;
187+ }
0 commit comments