296 Commits

Author SHA1 Message Date
aynakeya
8674e936e2 update deps 2025-11-26 23:14:02 +08:00
aynakeya
0bea12d0b3 fix source login using fyne.Do 2025-11-26 23:13:01 +08:00
aynakeya
8c0a3cf224 rev workflow 2025-11-06 02:06:10 +08:00
aynakeya
8d6ec36609 update go mod 2025-11-06 01:54:35 +08:00
aynakeya
68915b4f7e revert dep 2025-11-06 01:50:28 +08:00
aynakeya
c7fb3e76f9 fix img? 2025-11-06 01:45:37 +08:00
aynakeya
437b4a0f15 tmp 2025-11-06 01:39:48 +08:00
aynakeya
d1e77e35a4 update go version 2025-11-06 01:38:27 +08:00
aynakeya
d06ee8f61e update workflow 2025-11-06 01:25:37 +08:00
aynakeya
135c022cec using embed to embed static resource 2025-11-06 01:21:16 +08:00
aynakeya
3c8c8f3834 try move start after gui initialized 2025-11-06 01:05:52 +08:00
aynakeya
f070ee3f47 update miaosic 2025-11-06 00:08:46 +08:00
aynakeya
f59aebd2f8 add macos system media control 2025-10-19 01:12:19 +08:00
aynakeya
650da87f64 fix bili-video playlist 2025-10-08 21:14:29 +08:00
aynakeya
2838a02c83 update fixedSize 2025-10-07 00:30:54 +08:00
aynakeya
5c508b9664 gui refactor 2025-10-06 23:52:10 +08:00
aynakeya
7c3f8587f6 fix deadlock situation 2025-10-03 23:57:17 +08:00
aynakeya
918e2e81b3 migrate to eventbus, add support to macos 2025-10-02 21:57:45 +08:00
aynakeya
a81eb4a131 update qqmusic wxlogin 2025-09-29 23:23:23 +08:00
aynakeya
f13577f890 upgrade event to eventbus; remove old event package 2025-09-29 00:02:49 +08:00
aynakeya
82662c308a update fyne version 2025-09-28 22:59:08 +08:00
aynakeya
f6f306edc3 update miaosic dependency 2025-09-14 00:22:28 +08:00
aynakeya
a26b58b083 new event bus 2025-09-02 14:31:17 +08:00
aynakeya
093c10092b update version 2025-08-19 20:52:34 +08:00
aynakeya
4f53f870f3 update anyway, fix refresh token logic 2025-08-19 20:51:20 +08:00
aynakeya
5a699a1e2e use state machine to manage player state 2025-08-07 01:09:07 +08:00
aynakeya
3aebdb00f9 fanle 2025-08-01 21:31:02 +08:00
aynakeya
575e1863fd add dev version 2025-08-01 21:15:10 +08:00
Aynakeya
1e538646f4 Merge branch 'master' into dev 2025-07-31 01:42:19 +08:00
aynakeya
5c016ae145 fix workflow 2025-07-31 01:40:50 +08:00
Aynakeya
162779f25e Merge pull request #49 from AynaLivePlayer/dev
fix workflow
2025-07-31 00:48:05 +08:00
aynakeya
d2c89b031f fix workflow 2025-07-31 00:47:32 +08:00
Aynakeya
ccbe2117e0 Merge pull request #48 from AynaLivePlayer/dev
1.2.1
2025-07-31 00:33:39 +08:00
aynakeya
466e4a761e add nosource 2025-07-31 00:32:22 +08:00
Aynakeya
07d0a5debc Merge pull request #47 from AynaLivePlayer/dev
update miaosic, remove default source
2025-07-30 22:49:12 +08:00
aynakeya
7e680bc6bf update miaosic, remove default source 2025-07-30 22:48:21 +08:00
Aynakeya
45878dfe99 Merge pull request #46 from AynaLivePlayer/dev
add distribution channel to title
2025-07-30 22:45:30 +08:00
aynakeya
b136a374c4 add distribution channel to title 2025-07-30 22:45:05 +08:00
Aynakeya
6bf17a0dcb Merge pull request #45 from AynaLivePlayer/dev
1.2.0 release
2025-07-30 22:39:11 +08:00
aynakeya
da4ae97923 update todo.txt 2025-07-30 22:37:44 +08:00
aynakeya
2bab6044bf delete default music sources 2025-07-30 22:35:37 +08:00
Aynakeya
2cf80698d9 Merge pull request #44 from AynaLivePlayer/dev
Dev
2025-07-27 12:12:21 +08:00
aynakeya
4ac75dd882 run workflow on master 2025-07-27 12:11:58 +08:00
aynakeya
6af984cfbb fix linux fyne install command & fix window env
try fix windows

try fix windows

fix again

add windows debug build

try ucrt

fix ucrt env

use different mpv build

aaaa

bbb

disable vlc for now

test

build mpv only

update vlc version for windows

add go.sum
2025-07-27 12:10:42 +08:00
aynakeya
82ced0b9a9 test workflow 2025-07-27 03:54:26 +08:00
aynakeya
d11a5a5f76 capitalized first character in all wshub event key 2025-07-24 18:47:24 +08:00
Aynakeya
df771ed9f8 Merge pull request #43 from AynaLivePlayer/dev
fix netease login
2025-07-24 15:31:45 +08:00
aynakeya
3bd8945247 Merge branch 'master' into dev 2025-07-24 15:31:04 +08:00
aynakeya
c31342c708 fix netease login failed/get url failure 2025-07-23 02:44:04 +08:00
aynakeya
46ea1968b6 downgrade fyne to v2.5.5 2025-07-17 00:28:12 +08:00
aynakeya
d4f0c3438c update dependency 2025-07-17 00:28:12 +08:00
aynakeya
1a0e053377 update source 2025-07-17 00:28:12 +08:00
aynakeya
7b87efb076 save session when application close 2025-07-17 00:28:12 +08:00
aynakeya
3f5623f117 downgrade fyne to v2.5.5 2025-07-16 22:43:11 +08:00
aynakeya
261166e67d update dependency 2025-07-16 19:39:08 +08:00
aynakeya
dc727935a8 update source 2025-07-16 19:21:54 +08:00
aynakeya
68a172aa4f save session when application close 2025-07-08 00:33:11 +08:00
Aynakeya
bab3a14d2f Merge pull request #41 from AynaLivePlayer/dev
1.2.0
2025-07-08 00:09:30 +08:00
aynakeya
ee99bd939e Merge branch 'master' into dev 2025-07-08 00:08:00 +08:00
aynakeya
0934bd2d45 update miaosic dependency 2025-07-07 23:29:36 +08:00
aynakeya
7de62ef6ba update miaosic 2025-07-07 00:52:03 +08:00
aynakeya
af6ef96754 revert change 2025-07-07 00:52:03 +08:00
aynakeya
09bacbf7da add qq music 2025-07-07 00:52:03 +08:00
aynakeya
21fe844b5f 1.2.0 feature: qqmusic 2025-07-07 00:52:03 +08:00
aynakeya
72bdef6a91 update go-mpv version 2025-07-07 00:52:03 +08:00
aynakeya
c554f1effc update miaosic 2025-07-07 00:27:39 +08:00
aynakeya
8244380a6a revert change 2025-07-07 00:24:58 +08:00
aynakeya
0bbb9f378f add qq music 2025-07-07 00:18:01 +08:00
aynakeya
eafb074398 1.2.0 feature: qqmusic 2025-07-07 00:14:28 +08:00
aynakeya
7661e966be update go-mpv version 2025-07-02 23:54:34 +08:00
Aynakeya
06df1a1cc4 Merge pull request #39 from AynaLivePlayer/dev
1.2.0
2025-07-02 23:39:49 +08:00
aynakeya
5b4104664e update todo 2025-06-30 04:19:08 +08:00
aynakeya
2b99f7b23f add vlc output device selection method 2025-06-30 04:13:10 +08:00
aynakeya
3b58f6f972 able to switch vlc 2025-06-30 03:48:50 +08:00
aynakeya
374be8ef03 add vlc core 2025-06-30 03:40:33 +08:00
aynakeya
f4aa9b4ac9 fix bug: when diange limit is 1, user can only diange once & update version number to 1.2.0 2025-06-30 00:38:21 +08:00
aynakeya
1fa82c2a01 change config UseSystemFonts to CustomFonts, allow user to specify which font to use 2025-06-30 00:21:41 +08:00
aynakeya
cdc3c5d118 update dependency 2025-06-30 00:17:15 +08:00
aynakeya
c7b70740f6 fix program not closed after hit close button 2025-06-15 00:29:16 +08:00
aynakeya
dd71c3b9ba fyne 2.6.1 update, make all gui threadsafe 2025-06-15 00:04:13 +08:00
aynakeya
45e4c15b8d use config to decide if use system font or not 2025-06-15 00:00:19 +08:00
aynakeya
da81d43584 add use system font config 2025-06-14 23:57:59 +08:00
aynakeya
82df68fa8a update todo 2025-06-05 23:56:32 +08:00
Aynakeya
f897dd0800 Merge pull request #37 from AynaLivePlayer/dev
1.1.3 release
2025-05-28 20:38:16 +08:00
aynakeya
0ea07dc295 update version 2025-05-28 20:38:01 +08:00
aynakeya
edf3b679cc update log 2025-05-27 22:53:57 +08:00
aynakeya
041b678859 live blivedmgo using wbi 2025-05-27 22:44:39 +08:00
aynakeya
1b58c7bc08 fix host zero length 2025-05-27 22:16:56 +08:00
Aynakeya
826e868916 Merge pull request #36 from AynaLivePlayer/dev
1.1.2 fix: bilibili collection format. kugou format
2025-04-09 21:44:39 +08:00
aynakeya
32eab60cad update todo 2025-03-27 22:41:47 +08:00
aynakeya
83fbefaf68 add new bili video collection url format 2025-03-27 22:41:14 +08:00
Aynakeya
f62e9ea43a Merge pull request #35 1.1.2
1.1.2 update
2025-03-23 19:25:26 +08:00
aynakeya
3456bfcb8c update version 2025-03-23 19:23:39 +08:00
aynakeya
df614a16f6 update kugou new playlist url type 2025-03-22 21:22:25 +08:00
aynakeya
8a96506703 make sure log is deleted 2025-03-09 21:31:32 +08:00
aynakeya
53a6ad4aed fix enabled button not working properly 2025-03-05 20:20:17 +08:00
aynakeya
387e96e27e update log 2025-03-05 20:10:08 +08:00
aynakeya
8da41f81e1 remove go.sum 2025-03-05 20:07:42 +08:00
aynakeya
42c80f3ea8 add enabled button 2025-03-05 20:07:17 +08:00
Aynakeya
e311c40f5e Merge pull request #33 from xuanerwa/dev
 Feat: add `yinliang` plugin
2025-03-05 20:03:38 +08:00
xuanerwa
cbba7dd1b3 Merge branch 'AynaLivePlayer:dev' into dev 2025-03-04 17:39:11 +08:00
Aynakeya
e598ed676c Merge pull request #34 from AynaLivePlayer/dev
Fix kugou playlist & fix bili-video search
2025-02-28 11:03:14 +08:00
aynakeya
62a122a9e5 fix kugou playlist 2025-02-28 11:01:55 +08:00
aynakeya
98028885b8 fix bilivideo 2025-02-28 10:14:41 +08:00
xuanerwa
64c7780b60 Feat: add yinliang plugin 2025-02-27 11:10:36 +08:00
aynakeya
d40ef5d79d add default config for kugou-instr 2025-02-21 21:20:04 +08:00
Aynakeya
32e688bb91 Merge pull request #29 upgrade to fyne 2.5.4
upgrade to fyne 2.5.4
2025-02-19 19:29:15 +08:00
aynakeya
df501be65a upgrade to fyne 2.5.4 2025-02-19 19:28:44 +08:00
Aynakeya
fdf12bde26 Merge pull request #28 from AynaLivePlayer/dev
update version
2025-02-18 16:14:14 -08:00
aynakeya
cbde61d99b update version 2025-02-18 16:09:45 -08:00
Aynakeya
2b300af7b7 Merge pull request #27 from AynaLivePlayer/dev
update workflow
2025-02-18 16:08:46 -08:00
aynakeya
1bbaf478bb update workflow 2025-02-18 16:08:07 -08:00
Aynakeya
72589a5eaa Merge pull request #26 from AynaLivePlayer/dev v1.1.1
v1.1.1
2025-02-18 16:05:27 -08:00
aynakeya
0424513d86 1.1.1 2025-02-18 16:04:06 -08:00
aynakeya
f7b6a5eafb update kugou source 2025-02-18 15:59:43 -08:00
aynakeya
1c5e85010b update kugou source 2024-12-06 03:05:59 -08:00
aynakeya
d9581c0bab update kugou source 2024-12-06 01:39:51 -08:00
Aynakeya
8114e69b9f Merge pull request #24 from AynaLivePlayer/dev
add kugou instrumental source
2024-12-03 17:05:59 -08:00
aynakeya
b178f729a2 add kugou instr source 2024-11-17 07:16:52 -08:00
Aynakeya
3d5286b319 Merge pull request #23 from AynaLivePlayer/dev
Add headless mode
2024-11-12 17:02:30 -08:00
aynakeya
5afbe695f2 update git ignore 2024-11-12 17:00:28 -08:00
aynakeya
f36f56cd80 add headless mode 2024-11-12 16:59:36 -08:00
aynakeya
97faa1a538 fix diange command 2024-11-08 16:07:34 -08:00
aynakeya
93e8d4d846 delete unused 2024-11-08 05:28:13 -08:00
aynakeya
4e3dd6400f fix typo 2024-10-24 04:16:31 -07:00
Aynakeya
27a2f1f3a8 Merge pull request #22 from AynaLivePlayer/dev
1.1.0 pre-release.
2024-10-24 00:55:44 -07:00
aynakeya
bc85f86f98 update todo 2024-10-24 00:52:43 -07:00
aynakeya
c1940c0a98 update version number 2024-10-24 00:51:34 -07:00
aynakeya
4e6659935e update timeline props. although windows are not display it 2024-10-24 00:51:23 -07:00
aynakeya
9941fc6993 fix random next 2024-10-23 22:14:57 -07:00
aynakeya
e7c499ca7f finish windows system media control 2024-10-05 21:17:39 -07:00
aynakeya
e61f096bb2 update update log.txt 2024-09-26 00:03:29 -07:00
aynakeya
c1c230a5f0 update dependency: fix medal level for webdm 2024-09-25 23:51:26 -07:00
aynakeya
a2420db1d9 update dependency 2024-09-21 20:14:52 -07:00
aynakeya
0e4a429a19 update dependency 2024-09-02 22:36:32 -07:00
aynakeya
d69f953508 update todo 2024-09-01 20:35:13 -07:00
Aynakeya
04406bf006 Merge pull request #21 from AynaLivePlayer/dev
merge v1.0.9
2024-08-30 23:45:37 -07:00
aynakeya
b5c2a0ce96 1.0.9 release 2024-08-30 23:45:01 -07:00
aynakeya
13ffb609a9 update icon 2024-08-29 21:35:52 -07:00
Aynakeya
2e165d5d0e Merge pull request #20 from AynaLivePlayer/dev
1.0.9 prerelease1
2024-08-29 21:33:15 -07:00
aynakeya
0fe77febdf update ws obsinfo hint text 2024-08-29 21:23:36 -07:00
aynakeya
539e8eabe3 add configure to prevent wshub event execution. 2024-08-27 17:18:12 -07:00
aynakeya
7960299f09 update fyne to 2.5.1 2024-08-26 17:15:31 -07:00
aynakeya
e7f7ddfe4a maybe fix panic when close the software 2024-08-25 10:43:15 -07:00
aynakeya
23d6944a52 add usesysplaylist option 2024-08-25 10:42:18 -07:00
Aynakeya
ef994defb9 Merge pull request #19 from AynaLivePlayer/dev
1.0.9 feature finsh
2024-08-08 23:46:05 +08:00
aynakeya
2056bd310a update dependency 2024-08-08 23:42:00 +08:00
aynakeya
46c2e2710e temporary disable 2.5.0 entry redo/undo 2024-08-07 21:42:31 +08:00
aynakeya
156901f14c fix openblive admin not able qiege 2024-08-07 21:01:30 +08:00
aynakeya
9f8b103be1 fix cover url start with number not able to load 2024-07-24 23:39:35 +08:00
Aynakeya
25bc0f01ca Merge pull request #18 from AynaLivePlayer/dev
fyne update. able to choose to display cover or not
2024-07-24 20:27:10 +08:00
aynakeya
c5d3fb407b fyne update. able to choose to display cover or not 2024-07-24 20:24:42 +08:00
Aynakeya
5d507598e6 Merge pull request #17 from AynaLivePlayer/dev
upgrade fyne to 2.5.0
2024-07-20 21:39:19 +08:00
aynakeya
5480ebdd86 upgrade fyne to 2.5.0 2024-07-20 20:29:59 +08:00
Aynakeya
0e3527c166 Merge pull request #16 from AynaLivePlayer/dev
1.0.8prerelease3
2024-06-23 23:54:15 +08:00
aynakeya
48e5d2f516 critical: fix favlist page not increment 2024-06-23 23:53:20 +08:00
Aynakeya
b7f10431d6 Merge pull request #15 from AynaLivePlayer/dev
1.0.8prelease2
2024-06-23 15:24:43 +08:00
aynakeya
0c976acf80 update todo.txt 2024-06-23 15:24:02 +08:00
aynakeya
a06412e833 update single user max diange count 2024-06-23 15:22:57 +08:00
aynakeya
7e089913be add diange kugou default config 2024-06-23 00:03:26 +08:00
Aynakeya
61596a5b79 Merge pull request #14 from AynaLivePlayer/dev
1.0.8 prerelease
2024-06-22 23:51:42 +08:00
aynakeya
e44285841f update todo 2024-06-22 16:25:34 +08:00
aynakeya
93b3bbbb1f update kugou provider 2024-06-22 15:57:07 +08:00
Aynakeya
29509af04e Merge pull request #13 from AynaLivePlayer/dev
ready for 1.0.8
2024-06-22 10:40:52 +08:00
aynakeya
6073719d69 add multiple diange command handling 2024-06-22 10:21:13 +08:00
aynakeya
c9d84b1134 update go mod 2024-06-22 10:04:10 +08:00
aynakeya
4a76cbdb8b update bilivideo music provider 2024-06-21 23:23:38 +08:00
aynakeya
4cf79a0ab9 ignore smtc for now 2024-06-19 15:55:53 +08:00
aynakeya
56b484257b update workflow 2024-06-11 08:16:21 +08:00
aynakeya
e8953f1e56 update dependency 2024-06-08 22:01:17 +08:00
Aynakeya
1aace54b92 Merge pull request #12 from AynaLivePlayer/dev
fix kuwo. fix local lyric.
2024-06-08 14:14:32 +08:00
aynakeya
0e64d1dc91 update 2024-06-08 14:00:39 +08:00
aynakeya
ea05fcd902 update miaosic source dependency 2024-06-04 19:48:51 +08:00
Aynakeya
afa2729282 Merge pull request #11 from AynaLivePlayer/dev
Merge 1.0.7
2024-05-23 22:02:26 +08:00
aynakeya
36cdfd0824 only on master 2024-05-23 22:01:39 +08:00
aynakeya
ce042bad0a fix build 2024-05-23 21:54:04 +08:00
aynakeya
3959eac396 fix upload executable 2024-05-23 21:24:59 +08:00
aynakeya
2ab27b1b57 uw 2024-05-23 21:12:18 +08:00
aynakeya
7316818093 uw 2024-05-23 21:08:24 +08:00
aynakeya
fb43dcab5d separate jobs 2024-05-23 21:05:12 +08:00
aynakeya
fa7448e2c5 test 2024-05-23 21:00:17 +08:00
aynakeya
5d002becc4 fff 2024-05-23 20:46:02 +08:00
aynakeya
a2d674666a asdf 2024-05-23 20:32:13 +08:00
aynakeya
a6cdf3c903 hello 2024-05-23 20:23:57 +08:00
aynakeya
a892ec3043 cnm 2024-05-23 20:16:17 +08:00
aynakeya
2bc19e2e0c asdf 2024-05-23 20:10:38 +08:00
aynakeya
552f115a33 aaa 2024-05-23 20:03:59 +08:00
aynakeya
92376bdd12 sb 2024-05-23 19:57:02 +08:00
aynakeya
9142d74f19 asdf 2024-05-23 19:29:47 +08:00
aynakeya
65aba16f44 update 2024-05-23 19:23:26 +08:00
aynakeya
37f0dadd8b asdf 2024-05-23 19:16:42 +08:00
aynakeya
c57b7c992f asdf 2024-05-23 18:48:44 +08:00
aynakeya
e47b48bb42 ad 2024-05-23 18:45:47 +08:00
aynakeya
deec6157b9 adsf 2024-05-23 18:37:44 +08:00
aynakeya
7634bd5864 fi 2024-05-23 18:31:52 +08:00
aynakeya
50d3172816 fu 2024-05-23 18:29:49 +08:00
aynakeya
e466e21bc8 fk 2024-05-23 18:27:04 +08:00
aynakeya
7a57a1dcc0 uw 2024-05-23 18:20:27 +08:00
aynakeya
627e5ff9bc uw 2024-05-23 18:14:47 +08:00
aynakeya
ce1322fbde uw 2024-05-23 13:37:21 +08:00
aynakeya
8db6f49c7b uw 2024-05-23 12:55:37 +08:00
aynakeya
161920686f uw 2024-05-23 12:43:04 +08:00
aynakeya
6b595e453b uw 2024-05-22 21:50:17 +08:00
aynakeya
36df238ac1 uw 2024-05-22 21:32:57 +08:00
aynakeya
92f36057a4 uw 2024-05-22 21:09:55 +08:00
aynakeya
b175efa6e3 uw 2024-05-22 20:58:28 +08:00
aynakeya
4aa5071699 uw 2024-05-22 20:53:16 +08:00
aynakeya
318e062198 uw 2024-05-22 20:26:32 +08:00
aynakeya
16c9d1c10e uw 2024-05-22 20:18:14 +08:00
aynakeya
23d59417d5 uw 2024-05-22 20:09:41 +08:00
aynakeya
c6a8a2b272 uw 2024-05-22 20:03:18 +08:00
aynakeya
61342ebfa2 uw 2024-05-22 20:01:29 +08:00
aynakeya
1df1f3a609 uw 2024-05-22 19:59:10 +08:00
aynakeya
891f3d2879 update workflow 2024-05-22 19:56:49 +08:00
aynakeya
0dbb9976d7 update workflow 2024-05-22 19:46:46 +08:00
aynakeya
eabd9fcea3 update workflow 2024-05-22 19:41:14 +08:00
aynakeya
ec110cca9c update workflow 2024-05-22 19:36:47 +08:00
aynakeya
f6fd6d362b update workflow 2024-05-22 19:33:00 +08:00
aynakeya
6ec3987b50 update workflow 2024-05-22 19:29:36 +08:00
aynakeya
812dee0145 fix workflow 2024-05-22 19:23:45 +08:00
aynakeya
16f0aa34a2 fix typo 2024-05-22 19:22:47 +08:00
aynakeya
03862237b4 upload workflow 2024-05-22 19:21:15 +08:00
aynakeya
05322ab0b3 add backward compatibility for mpv client version 1.109 2024-05-22 01:30:12 +08:00
aynakeya
f97c460c46 update 2024-05-22 01:01:36 +08:00
aynakeya
8d24ac8cba update error info 2024-05-22 00:26:36 +08:00
aynakeya
f19babe30a add gui video player window close event 2024-05-21 11:23:56 +08:00
aynakeya
9b55e42811 update mpv 2024-05-21 11:14:15 +08:00
aynakeya
963dedbe65 add cover to video channel 2024-05-19 22:53:41 +08:00
aynakeya
b031bdd3df update dependency 2024-05-16 22:32:02 +08:00
aynakeya
aeca816774 fix comment 2024-05-16 11:34:20 +08:00
aynakeya
9b6f681d4a add event cache. sending all cached event when new websocket connection establish 2024-05-14 22:28:15 +08:00
aynakeya
46ea45580c update lyric 2024-05-11 11:02:47 +08:00
aynakeya
da96f711ae update dependency: fix bilivideo search issue 2024-05-10 10:26:58 +08:00
aynakeya
399f09ba9f update dependency: fix bilivideo issue 2024-05-10 10:21:41 +08:00
aynakeya
2326ef6955 add qiege logging. update dependency 2024-05-07 19:41:40 +08:00
Aynakeya
23db890d47 Merge pull request #9 from AynaLivePlayer/dev
merge 1.0.6
2024-05-06 09:50:38 +08:00
aynakeya
5145680a04 Merge branch 'master' into dev 2024-05-06 09:49:36 +08:00
aynakeya
d12a0155b0 fix todo note 2024-05-06 09:43:35 +08:00
aynakeya
ac8633b4a7 add comment 2024-05-06 08:23:44 +08:00
aynakeya
9b9895e654 update gitignore 2024-05-06 08:19:43 +08:00
aynakeya
f54e01f7ea fix image loading error causing crash. add todo.txt 2024-05-06 08:19:06 +08:00
aynakeya
f24b3e73fb add updater 2024-05-06 08:17:30 +08:00
aynakeya
68c7c591ff update wshub 2024-04-30 02:01:05 -07:00
aynakeya
95a0a97264 change log warn to error 2024-04-29 23:03:16 -07:00
aynakeya
e5076667db fix blacklist not working with id diange 2024-04-29 23:03:04 -07:00
aynakeya
726ac8b449 update dep 2024-04-29 22:51:58 -07:00
aynakeya
88066bd3b9 update wshub 2024-04-29 20:22:35 -07:00
aynakeya
d514f96c28 fix id diange bug 2024-04-28 14:04:44 -07:00
aynakeya
9a277482b4 update todo.txt 2024-04-26 21:19:02 -07:00
aynakeya
39db106a74 update go mod 2024-04-26 21:18:50 -07:00
aynakeya
0e5140f907 set message handle to nil after delete 2024-04-26 21:18:37 -07:00
aynakeya
3786355997 update liveroom-sdk. fix webdanmu missing message after second connection 2024-04-26 21:18:14 -07:00
aynakeya
a5656b8ef0 update version number 2024-04-26 19:48:53 -07:00
aynakeya
884a5afcb5 update to 1.0.5 2024-04-26 19:43:39 -07:00
aynakeya
bec84790fd update dependency 2024-04-26 19:24:05 -07:00
aynakeya
e7775019f7 fix liveroom gui race condition 2024-04-26 18:51:18 -07:00
aynakeya
3fb7941433 update liveroomsdk to fix race condition 2024-04-26 18:50:56 -07:00
aynakeya
b56a1073a6 update liveroomsdk 2024-04-26 18:31:53 -07:00
aynakeya
eba37c04bc raise error when fail to create liveroom 2024-04-26 18:31:40 -07:00
aynakeya
de3d2f6c66 fix random player playlist panic when 0 media in the list 2024-04-26 18:14:09 -07:00
aynakeya
a8091247ec update readme 2024-04-25 20:29:54 -07:00
aynakeya
9c0b711aa0 update dep 2024-04-25 19:56:26 -07:00
aynakeya
58d7ebd43d add skip on fail 2024-04-24 01:27:57 -07:00
aynakeya
ee775dee8d fix translation 2024-04-23 21:29:45 -07:00
aynakeya
5d27040c8b add log file 2024-04-23 09:29:02 -07:00
aynakeya
2169817afa update version 2024-04-22 22:29:14 -07:00
aynakeya
e9149474de fix playlist index not reset to 0 when switch from random 2024-04-22 22:26:42 -07:00
Aynakeya
5cc5948a85 Merge 1.0.x branch (#8)
* rewrite

* update submodule

* make width height configurable

* update dependency

* update

* update file

* update dep

* fix basic config layout

* update plugin management

* more stuff

* add blacklist

* fix todo

* fix windows gethandle

* update windows update guide

* update windows build guide

* include go mod tidy in script

* update todo

* fix source session

* fix text output

* add plugin play duration control

* fix id diange not working

* update todo

* update version number
2024-04-22 21:21:02 -07:00
aynakeya
92ea73ff5a update version number 2024-04-22 20:54:00 -07:00
aynakeya
e567b5b47b update todo 2024-04-22 14:11:44 -07:00
aynakeya
802961576c fix id diange not working 2024-04-22 13:49:17 -07:00
aynakeya
0ad0296e51 add plugin play duration control 2024-04-22 13:49:06 -07:00
aynakeya
f484b28d0e fix text output 2024-04-20 19:14:33 -07:00
aynakeya
e2fa2bdb8e fix source session 2024-04-20 17:31:09 -07:00
aynakeya
e01d067cbc update todo 2024-04-17 12:21:34 -07:00
aynakeya
44203f9887 include go mod tidy in script 2024-04-17 12:17:14 -07:00
aynakeya
ccb3ccbc00 update windows build guide 2024-04-17 12:16:22 -07:00
aynakeya
d64cac9b1b update windows update guide 2024-04-17 12:15:30 -07:00
aynakeya
6ef4ba3fe8 fix windows gethandle 2024-04-17 11:47:28 -07:00
aynakeya
d30db8a95a fix todo 2024-04-15 17:26:19 -07:00
aynakeya
30a2f9ebea add blacklist 2024-04-15 17:18:03 -07:00
aynakeya
6ddcb507f5 more stuff 2024-04-15 16:07:31 -07:00
aynakeya
fbf8c7f149 update plugin management 2024-04-15 14:00:48 -07:00
aynakeya
139f331a14 fix basic config layout 2024-04-15 10:04:27 -07:00
aynakeya
c9cd78b0eb update dep 2024-04-14 09:39:25 -07:00
aynakeya
e7325b6383 update file 2024-04-13 01:00:28 -07:00
aynakeya
fd872c1f5b update 2024-04-13 00:59:44 -07:00
aynakeya
119862a023 update dependency 2024-04-10 01:00:31 -07:00
aynakeya
d52f97429d make width height configurable 2024-04-10 00:52:59 -07:00
aynakeya
084050fcf7 update submodule 2024-04-10 00:44:27 -07:00
aynakeya
f926f15606 rewrite 2024-04-10 00:42:33 -07:00
NearlOfficial
8d73a3c284 Merge pull request #7 from AynaLivePlayer/dev
Temp fix for danmu issue
2023-07-26 23:59:18 +08:00
aynakeya
c993684497 update version number 2023-07-26 12:17:23 +08:00
aynakeya
0ae52e349d tempfix danmu require loginned user 2023-07-26 12:13:54 +08:00
NearlOfficial
cb092366f3 checkUpdateBtn fix bug (#6)
Co-authored-by: NearlOfficial <kamiya814767377@gmail.com>
2023-03-17 03:38:45 -07:00
Aynakeya
18df9ff64c add assets 2023-03-04 19:51:58 -08:00
253 changed files with 12254 additions and 9219 deletions

124
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,124 @@
name: Build
on:
push:
branches:
- master
jobs:
build-windows:
runs-on: windows-2022
timeout-minutes: 20
env:
GOOS: windows
GOARCH: amd64
EXECUTABLE: AynaLivePlayer.exe
CGO_CFLAGS: "-I${{ github.workspace }}/libmpv/include -I${{ github.workspace }}/libvlc/VideoLAN.LibVLC.Windows.3.0.21/build/x64/include"
CGO_LDFLAGS: "-L${{ github.workspace }}/libmpv -L${{ github.workspace }}/libvlc/VideoLAN.LibVLC.Windows.3.0.21/build/x64"
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install 7-Zip
run: choco install 7zip
- name: Download libmpv
uses: robinraju/release-downloader@v1
with:
repository: "shinchiro/mpv-winbuild-cmake"
latest: true
fileName: "mpv-dev-x86_64-[0-9]*.7z"
extract: false
out-file-path: "libmpv"
- name: Extract libmpv
run: |
7z x "libmpv/mpv-dev-x86_64-*.7z" -o"libmpv"
- name: Setup NuGet.exe
uses: nuget/setup-nuget@v2
with:
nuget-version: 'latest'
- name: Install VLC dependency
run: nuget install VideoLAN.LibVLC.Windows -OutputDirectory ${{ github.workspace }}/libvlc -Version 3.0.21
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
- name: Install Go Dependencies
run: |
go mod tidy
go install fyne.io/tools/cmd/fyne@latest
- name: Build application
run: |
go build -tags="mpvOnly,nosource" -v -o ./AynaLivePlayerMpvNoSource.exe -ldflags -H=windowsgui app/main.go
go build -tags="vlcOnly,nosource" -v -o ./AynaLivePlayerVlcNoSource.exe -ldflags -H=windowsgui app/main.go
go build -tags="nosource" -v -o ./AynaLivePlayerAllPlayerNoSource.exe -ldflags -H=windowsgui app/main.go
go build -tags="mpvOnly" -v -o ./AynaLivePlayerMpv.exe -ldflags -H=windowsgui app/main.go
go build -tags="vlcOnly" -v -o ./AynaLivePlayerVlc.exe -ldflags -H=windowsgui app/main.go
go build -v -o ./AynaLivePlayerAllPlayer.exe -ldflags -H=windowsgui app/main.go
- name: Upload artifact (NoSource)
uses: actions/upload-artifact@v4
with:
name: windows-build-nosource
path: |
./AynaLivePlayerMpvNoSource.exe
./AynaLivePlayerVlcNoSource.exe
./AynaLivePlayerAllPlayerNoSource.exe
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: windows-build
path: |
./AynaLivePlayerMpv.exe
./AynaLivePlayerVlc.exe
./AynaLivePlayerAllPlayer.exe
build-ubuntu:
runs-on: ubuntu-latest
timeout-minutes: 10
env:
GOOS: linux
GOARCH: amd64
EXECUTABLE: AynaLivePlayer
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y libvlc-dev vlc libmpv-dev libgl-dev libxcursor-dev libxrandr-dev libxinerama-dev libxi-dev libgl1-mesa-dev xorg-dev
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
- name: Install Go Dependencies
run: |
go mod tidy
go install fyne.io/tools/cmd/fyne@latest
- name: Build application
run: go build -o ./${{ env.EXECUTABLE }} app/main.go
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ubuntu-build
path: ./${{ env.EXECUTABLE }}

11
.gitignore vendored
View File

@@ -1,3 +1,12 @@
.idea
assets/webinfo/*.html
assets/webinfo/assets
assets/webinfo/assets
resource/bundle.go
music
/txtinfo/
CMakeCache.txt
/config/
/release/
log.txt
config.ini
config.ini.bak

6
.gitmodules vendored Normal file
View File

@@ -0,0 +1,6 @@
[submodule "pkg/miaosic"]
path = pkg/miaosic
url = git@github.com:AynaLivePlayer/miaosic.git
[submodule "pkg/liveroom-sdk"]
path = pkg/liveroom-sdk
url = git@github.com:AynaLivePlayer/liveroom-sdk.git

View File

@@ -1,65 +1,58 @@
NAME = AynaLivePlayer
EXECUTABLE=AynaLivePlayer
WINDOWS=$(EXECUTABLE).exe
LINUX=$(EXECUTABLE)_linux
DARWIN=$(EXECUTABLE)_darwin
ifeq ($(OS),Windows_NT)
RM = del /Q /F
RRM = rmdir /Q /S
MKDIR = mkdir
COPY = XCOPY /Y
MOVE = move
else
RM = rm -f
RRM = rm -rf
MKDIR = mkdir
COPY = cp -r
MOVE = mv
endif
ifeq ($(OS), Windows_NT)
EXECUTABLE=$(NAME).exe
SCRIPTPATH = .\assets\scripts\windows
else
EXECUTABLE=$(NAME)
SCRIPTPATH = ./assets/scripts/linux
endif
gui: bundle
go build -o $(EXECUTABLE) -ldflags -H=windowsgui main.go
run: bundle
go run main.go
clear:
$(RM) config.ini log.txt playlists.txt liverooms.json
bundle:
fyne bundle --name resImageEmpty --package resource ./assets/empty.png > ./resource/bundle.go
fyne bundle --append --name resImageIcon --package resource ./assets/icon.jpg >> ./resource/bundle.go
# fyne bundle --append --name resFontMSYaHei --package resource ./assets/msyh.ttc >> ./resource/bundle.go
# fyne bundle --append --name resFontMSYaHeiBold --package resource ./assets/msyhbd.ttc >> ./resource/bundle.go
fyne bundle --name resImageIcon --package resource ./assets/icon.png > ./resource/bundle.go
fyne bundle --append --name resFontMSYaHei --package resource ./assets/msyh0.ttf >> ./resource/bundle.go
fyne bundle --append --name resFontMSYaHeiBold --package resource ./assets/msyhbd0.ttf >> ./resource/bundle.go
# fyne bundle --append --name resFontMSYaHei --package resource ./assets/msyh.ttc >> ./resource/bundle.go
# fyne bundle --append --name resFontMSYaHeiBold --package resource ./assets/msyhbd.ttc >> ./resource/bundle.go
release: gui
-mkdir release
ifeq ($(OS), Windows_NT)
COPY .\$(EXECUTABLE) .\release\$(EXECUTABLE)
COPY .\webtemplates.json .\release\webtemplates.json
mkdir .\release\assets
COPY .\assets\mpv-2.dll .\release\mpv-2.dll
COPY .\assets\translation.json .\release\assets\translation.json
COPY LICENSE.md .\release\LICENSE.md
XCOPY .\assets\scripts\windows\* .\release\ /k /i /y /q
XCOPY .\assets\webinfo .\release\assets\webinfo /s /e /i /y /q
XCOPY .\music .\release\music /s /e /i /y /q
XCOPY .\template .\release\template /s /e /i /y /q
else
cp ./$(EXECUTABLE) ./release/$(EXECUTABLE)
cp ./webtemplates.json ./release/webtemplates.json
cp ./assets/translation.json ./release/assets/translation.json
mkdir ./release/assets
cp LICENSE.md ./release/LICENSE.md
cp ./assets/scripts/linux/* ./release/
cp -r ./assets/webinfo ./release/assest/webinfo
cp -r ./music ./release/music
cp -r ./template ./release/template
endif
prebuild: bundle
$(RRM) ./release
$(MKDIR) ./release
$(MKDIR) ./release/assets
$(COPY) LICENSE.md ./release/LICENSE.md
$(COPY) ./assets/translation.json ./release/assets/translation.json
$(COPY) ./assets/config ./release/config
$(COPY) ./music ./release/music
go mod tidy
$(LINUX): prebuild
env GOOS=linux GOARCH=amd64 go build -o ./release/$(LINUX) app/main.go
$(MOVE) ./release/$(LINUX) ./release/$(EXECUTABLE)
$(WINDOWS): prebuild
env GOOS=windows GOARCH=amd64 go build -o ./release/$(WINDOWS) -ldflags -H=windowsgui app/main.go
$(DARWIN): prebuild
env GOOS=darwin GOARCH=amd64 go build -o ./release/$(DARWIN) app/main.go
$(MOVE) ./release/$(LINUX) ./release/$(EXECUTABLE)
windows: $(WINDOWS) ## Build for Windows
$(COPY) ./assets/windows/mpv-2.dll ./release/mpv-2.dll
linux: $(LINUX) ## Build for Linux
darwin: $(DARWIN) ## Build for Darwin (macOS)
clean:
$(RM) $(EXECUTABLE) config.ini log.txt playlists.txt liverooms.json
$(RRM) release
.PHONY: ${EXECUTABLE}
$(RRM) ./release

View File

@@ -8,11 +8,50 @@ QQ group: 621035845
## build
> outdated, please refer to workflow file
```
go build -o AynaLivePlayer.exe -ldflags -H=windowsgui app/gui/main.go
```
## packaging
> outdated, please refer to workflow file
```
fyne package --src path_to_gui --exe AynaLivePlayer.exe --appVersion 0.8.4 --icon path_to_icon
```
## Windows build guide
> outdated, please refer to workflow file
1. install golang [link](https://go.dev/doc/install)
2. install chocolatey [link](https://chocolatey.org/install)
3. install required packages
```
choco install git
choco install mingw
```
4. install fyne
```
go install fyne.io/fyne/v2/cmd/fyne@latest
```
5. clone this repo
```bash
git clone --recurse-submodules git@github.com:AynaLivePlayer/AynaLivePlayer.git
```
if you are using https links
```
git clone https://github.com/AynaLivePlayer/AynaLivePlayer.git
git submodule set-url pkg/miaosic https://github.com/AynaLivePlayer/miaosic.git
git submodule set-url pkg/liveroom-sdk https://github.com/AynaLivePlayer/liveroom-sdk.git
git submodule update
```
6. now you can build (please check makefile for more details)
```powershell
$env:CGO_LDFLAGS="-LC:\Users\Admin\Desktop\AynaLivePlayer\libmpv\lib";$env:CGO_CFLAGS="-IC:\Users\Admin\Desktop\AynaLivePlayer\libmpv\include"
# ... more setup, see makefile
go build -o AynaLivePlayer.exe -ldflags -H=windowsgui app/main.go
```

View File

@@ -1,24 +0,0 @@
package adapters
import (
"AynaLivePlayer/adapters/liveclient"
"AynaLivePlayer/adapters/logger"
"AynaLivePlayer/adapters/player"
"AynaLivePlayer/common/event"
"AynaLivePlayer/core/adapter"
)
var Logger = &logger.LoggerFactory{}
var LiveClient = &liveclient.LiveClientFactory{
LiveClients: map[string]adapter.LiveClientCtor{
"bilibili": liveclient.BilibiliCtor,
},
EventManager: event.MainManager,
Logger: &logger.EmptyLogger{},
}
var Player = &player.PlayerFactory{
EventManager: event.MainManager,
Logger: &logger.EmptyLogger{},
}

View File

@@ -1,115 +0,0 @@
package liveclient
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/core/adapter"
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"errors"
"github.com/aynakeya/blivedm"
"strconv"
"time"
)
type Bilibili struct {
client *blivedm.BLiveWsClient
eventManager *event.Manager
roomName string
status bool
log adapter.ILogger
}
func BilibiliCtor(id string, em *event.Manager, log adapter.ILogger) (adapter.LiveClient, error) {
room, err := strconv.Atoi(id)
if err != nil {
return nil, errors.New("room id for bilibili should be a integer")
}
return NewBilibili(room, em, log), nil
}
func NewBilibili(roomId int, em *event.Manager, log adapter.ILogger) adapter.LiveClient {
cl := &Bilibili{
client: &blivedm.BLiveWsClient{ShortId: roomId, Account: blivedm.DanmuAccount{UID: 0}, HearbeatInterval: 10 * time.Second},
eventManager: em,
roomName: "",
log: log,
}
cl.client.OnDisconnect = func(client *blivedm.BLiveWsClient) {
cl.log.Warn("[Bilibili LiveChatSDK] disconnect from websocket connection, maybe try reconnect")
cl.status = false
cl.eventManager.CallA(events.LiveRoomStatusChange, events.StatusChangeEvent{Connected: false, Client: cl})
}
cl.client.RegHandler(blivedm.CmdDanmaku, cl.handleMsg)
return cl
}
func (b *Bilibili) ClientName() string {
return "bilibili"
}
func (b *Bilibili) RoomName() string {
return b.roomName
}
func (b *Bilibili) Status() bool {
return b.status
}
func (b *Bilibili) EventManager() *event.Manager {
return b.eventManager
}
func (b *Bilibili) Connect() bool {
if b.status {
return true
}
b.log.Info("[Bilibili LiveChatSDK] Trying Connect Danmu Server")
if b.client.InitRoom() && b.client.ConnectDanmuServer() {
b.roomName = b.client.RoomInfo.Title
b.status = true
b.eventManager.CallA(events.LiveRoomStatusChange, events.StatusChangeEvent{Connected: true, Client: b})
b.log.Info("[Bilibili LiveChatSDK] Connect Success")
return true
}
b.log.Info("[Bilibili LiveChatSDK] Connect Failed")
return false
}
func (b *Bilibili) Disconnect() bool {
b.log.Info("[Bilibili LiveChatSDK] Disconnect from danmu server")
if b.client == nil {
return true
}
b.client.Disconnect()
b.eventManager.CallA(events.LiveRoomStatusChange, events.StatusChangeEvent{Connected: false, Client: b})
return true
}
func (b *Bilibili) handleMsg(context *blivedm.Context) {
msg, ok := context.ToDanmakuMessage()
if !ok {
b.log.Warn("[Bilibili LiveChatSDK] handle message failed, can't convert context to danmu message")
return
}
dmsg := model.DanmuMessage{
User: model.DanmuUser{
Uid: strconv.FormatInt(msg.Uid, 10),
Username: msg.Uname,
Medal: model.UserMedal{
Name: msg.MedalName,
Level: int(msg.MedalLevel),
},
Admin: msg.Admin,
Privilege: int(msg.PrivilegeType),
},
Message: msg.Msg,
}
b.log.Debug("[Bilibili LiveChatSDK] receive message", dmsg)
go func() {
b.eventManager.Call(&event.Event{
Id: events.LiveRoomMessageReceive,
Cancelled: false,
Data: &dmsg,
})
}()
}

View File

@@ -1,29 +0,0 @@
package liveclient
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/core/adapter"
"errors"
)
type LiveClientFactory struct {
LiveClients map[string]adapter.LiveClientCtor
EventManager *event.Manager
Logger adapter.ILogger
}
func (f *LiveClientFactory) GetAllClientNames() []string {
names := make([]string, 0)
for key, _ := range f.LiveClients {
names = append(names, key)
}
return names
}
func (f *LiveClientFactory) NewLiveClient(clientName, id string) (adapter.LiveClient, error) {
ctor, ok := f.LiveClients[clientName]
if !ok {
return nil, errors.New("no such client")
}
return ctor(id, f.EventManager.NewChildManager(), f.Logger)
}

View File

@@ -1,48 +0,0 @@
package logger
import (
"AynaLivePlayer/core/adapter"
)
type EmptyLogger struct {
}
func (e EmptyLogger) Debug(args ...interface{}) {
return
}
func (e EmptyLogger) Debugf(format string, args ...interface{}) {
return
}
func (e EmptyLogger) Info(args ...interface{}) {
return
}
func (e EmptyLogger) Infof(format string, args ...interface{}) {
return
}
func (e EmptyLogger) Warn(args ...interface{}) {
return
}
func (e EmptyLogger) Warnf(format string, args ...interface{}) {
return
}
func (e EmptyLogger) Error(args ...interface{}) {
return
}
func (e EmptyLogger) Errorf(format string, args ...interface{}) {
return
}
func (e EmptyLogger) WithModule(prefix string) adapter.ILogger {
return &EmptyLogger{}
}
func (e EmptyLogger) SetLogLevel(level adapter.LogLevel) {
return
}

View File

@@ -1,13 +0,0 @@
package logger
import (
"AynaLivePlayer/core/adapter"
)
type LoggerFactory struct {
LiveClients map[string]adapter.LiveClientCtor
}
func (f *LoggerFactory) NewLogrus(filename string, redirectStderr bool) adapter.ILogger {
return NewLogrusLogger(filename, redirectStderr)
}

View File

@@ -1,66 +0,0 @@
package logger
import (
"AynaLivePlayer/core/adapter"
nested "github.com/antonfisher/nested-logrus-formatter"
"github.com/sirupsen/logrus"
"github.com/virtuald/go-paniclog"
"io"
"os"
)
type LogrusLogger struct {
*logrus.Entry
module string
}
func (l *LogrusLogger) SetLogLevel(level adapter.LogLevel) {
switch level {
case adapter.LogLevelDebug:
l.Logger.SetLevel(logrus.DebugLevel)
case adapter.LogLevelInfo:
l.Logger.SetLevel(logrus.InfoLevel)
case adapter.LogLevelWarn:
l.Logger.SetLevel(logrus.WarnLevel)
case adapter.LogLevelError:
l.Logger.SetLevel(logrus.ErrorLevel)
default:
l.Logger.SetLevel(logrus.InfoLevel)
}
}
func NewLogrusLogger(fileName string, redirectStderr bool) *LogrusLogger {
l := logrus.New()
l.SetFormatter(
&nested.Formatter{
FieldsOrder: []string{"Module"},
HideKeys: true,
NoColors: true,
})
var file *os.File
var err error
if fileName != "" {
file, err = os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err == nil {
l.Out = io.MultiWriter(file, os.Stdout)
} else {
l.Info("Failed to log to file, using default stdout")
}
}
if redirectStderr && file != nil {
l.Info("panic/stderr redirect to log file")
if _, err = paniclog.RedirectStderr(file); err != nil {
l.Infof("Failed to redirect stderr to to file: %s", err)
}
}
return &LogrusLogger{
Entry: logrus.NewEntry(l),
}
}
func (l *LogrusLogger) WithModule(prefix string) adapter.ILogger {
return &LogrusLogger{
Entry: l.Entry.WithField("Module", prefix),
module: prefix,
}
}

View File

@@ -1,15 +0,0 @@
package player
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/core/adapter"
)
type PlayerFactory struct {
EventManager *event.Manager
Logger adapter.ILogger
}
func (f *PlayerFactory) NewMPV() adapter.IPlayer {
return NewMpvPlayer(f.EventManager, f.Logger)
}

View File

@@ -1,226 +0,0 @@
package player
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/common/util"
"AynaLivePlayer/core/adapter"
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"fmt"
"github.com/aynakeya/go-mpv"
"github.com/tidwall/gjson"
)
var mpvPropertyMap = map[model.PlayerProperty]string{
model.PlayerPropDuration: "duration",
model.PlayerPropTimePos: "time-pos",
model.PlayerPropIdleActive: "idle-active",
model.PlayerPropPercentPos: "percent-pos",
model.PlayerPropPause: "pause",
model.PlayerPropVolume: "volume",
}
var mpvPropertyMapInv = map[string]model.PlayerProperty{
"duration": model.PlayerPropDuration,
"time-pos": model.PlayerPropTimePos,
"idle-active": model.PlayerPropIdleActive,
"percent-pos": model.PlayerPropPercentPos,
"pause": model.PlayerPropPause,
"volume": model.PlayerPropVolume,
}
type MpvPlayer struct {
running bool
libmpv *mpv.Mpv
Playing *model.Media
propertyWatchedFlag map[model.PlayerProperty]int
eventManager *event.Manager
log adapter.ILogger
}
func NewMpvPlayer(em *event.Manager, log adapter.ILogger) adapter.IPlayer {
player := &MpvPlayer{
running: true,
libmpv: mpv.Create(),
propertyWatchedFlag: make(map[model.PlayerProperty]int),
eventManager: em,
log: log,
}
err := player.libmpv.Initialize()
if err != nil {
player.log.Error("[MPV Player] initialize libmpv failed")
return nil
}
_ = player.libmpv.SetOptionString("vo", "null")
player.log.Info("[MPV Player] initialize libmpv success")
_ = player.ObserveProperty(model.PlayerPropIdleActive, "player.setplaying", func(evnt *event.Event) {
isIdle := evnt.Data.(events.PlayerPropertyUpdateEvent).Value.(bool)
if isIdle {
player.Playing = nil
}
})
player.Start()
return player
}
func (p *MpvPlayer) Start() {
p.log.Info("[MPV Player] starting mpv player")
go func() {
for p.running {
e := p.libmpv.WaitEvent(1)
if e == nil {
p.log.Warn("[MPV Player] event loop got nil event")
}
//p.log.Trace("[MPV Player] new event", e)
if e.EventId == mpv.EVENT_PROPERTY_CHANGE {
eventProperty := e.Property()
property, ok := mpvPropertyMapInv[eventProperty.Name]
if !ok {
continue
}
var value interface{} = nil
if eventProperty.Data != nil {
value = eventProperty.Data.(mpv.Node).Value
}
p.eventManager.CallA(
events.EventPlayerPropertyUpdate(property),
events.PlayerPropertyUpdateEvent{
Property: property,
Value: value,
})
}
if e.EventId == mpv.EVENT_SHUTDOWN {
p.log.Info("[MPV Player] libmpv shutdown")
p.Stop()
}
}
}()
return
}
func (p *MpvPlayer) Stop() {
p.log.Info("[MPV Player] stopping mpv player")
p.running = false
p.libmpv.TerminateDestroy()
}
func (p *MpvPlayer) GetPlaying() *model.Media {
return p.Playing
}
func (p *MpvPlayer) SetWindowHandle(handle uintptr) error {
p.log.Infof("[MPV Player] set window handle %d", handle)
_ = p.libmpv.SetOptionString("wid", fmt.Sprintf("%d", handle))
return p.libmpv.SetOptionString("vo", "gpu")
}
func (p *MpvPlayer) Play(media *model.Media) error {
p.log.Infof("[MPV Player] Play media %s", media.Url)
if val, ok := media.Header["User-Agent"]; ok {
p.log.Debug("[MPV PlayControl] set user-agent for mpv player")
err := p.libmpv.SetPropertyString("user-agent", val)
if err != nil {
p.log.Warn("[MPV PlayControl] set player user-agent failed", err)
return err
}
}
if val, ok := media.Header["Referer"]; ok {
p.log.Debug("[MPV PlayControl] set referrer for mpv player")
err := p.libmpv.SetPropertyString("referrer", val)
if err != nil {
p.log.Warn("[MPV PlayControl] set player referrer failed", err)
return err
}
}
p.log.Debugf("mpv command load file %s %s", media.Title, media.Url)
if err := p.libmpv.Command([]string{"loadfile", media.Url}); err != nil {
p.log.Warn("[MPV PlayControl] mpv load media failed", media)
return err
}
p.Playing = media
return nil
}
func (p *MpvPlayer) IsPaused() bool {
property, err := p.libmpv.GetProperty("pause", mpv.FORMAT_FLAG)
if err != nil {
p.log.Warn("[MPV PlayControl] get property pause failed", err)
return false
}
return property.(bool)
}
func (p *MpvPlayer) Pause() error {
p.log.Debugf("[MPV Player] pause")
return p.libmpv.SetProperty("pause", mpv.FORMAT_FLAG, true)
}
func (p *MpvPlayer) Unpause() error {
p.log.Debugf("[MPV Player] unpause")
return p.libmpv.SetProperty("pause", mpv.FORMAT_FLAG, false)
}
// SetVolume set mpv volume, from 0.0 - 100.0
func (p *MpvPlayer) SetVolume(volume float64) error {
p.log.Debugf("[MPV Player] set volume to %f", volume)
return p.libmpv.SetProperty("volume", mpv.FORMAT_DOUBLE, volume)
}
func (p *MpvPlayer) IsIdle() bool {
property, err := p.libmpv.GetProperty("idle-active", mpv.FORMAT_FLAG)
if err != nil {
p.log.Warn("[MPV Player] get property idle-active failed", err)
return false
}
return property.(bool)
}
// Seek change position for current file
// absolute = true : position is the time in second
// absolute = false: position is in percentage eg 0.1 0.2
func (p *MpvPlayer) Seek(position float64, absolute bool) error {
p.log.Debugf("[MPV Player] seek to %f (absolute=%t)", position, absolute)
if absolute {
return p.libmpv.SetProperty("time-pos", mpv.FORMAT_DOUBLE, position)
} else {
return p.libmpv.SetProperty("percent-pos", mpv.FORMAT_DOUBLE, position)
}
}
func (p *MpvPlayer) ObserveProperty(property model.PlayerProperty, name string, handler event.HandlerFunc) error {
p.log.Debugf("[MPV Player] add property observer for mpv")
p.eventManager.RegisterA(
events.EventPlayerPropertyUpdate(property),
name, handler)
if _, ok := p.propertyWatchedFlag[property]; !ok {
p.propertyWatchedFlag[property] = 1
return p.libmpv.ObserveProperty(util.Hash64(mpvPropertyMap[property]), mpvPropertyMap[property], mpv.FORMAT_NODE)
}
return nil
}
// GetAudioDeviceList get output device for mpv
// return format is []AudioDevice
func (p *MpvPlayer) GetAudioDeviceList() ([]model.AudioDevice, error) {
p.log.Debugf("[MPV Player] getting audio device list for mpv")
property, err := p.libmpv.GetProperty("audio-device-list", mpv.FORMAT_STRING)
if err != nil {
return nil, err
}
dl := make([]model.AudioDevice, 0)
gjson.Parse(property.(string)).ForEach(func(key, value gjson.Result) bool {
dl = append(dl, model.AudioDevice{
Name: value.Get("name").String(),
Description: value.Get("description").String(),
})
return true
})
return dl, nil
}
func (p *MpvPlayer) SetAudioDevice(device string) error {
p.log.Debugf("[MPV Player] set audio device %s for mpv", device)
return p.libmpv.SetPropertyString("audio-device", device)
}

View File

@@ -1,27 +0,0 @@
package player
import (
"AynaLivePlayer/adapters/logger"
"AynaLivePlayer/common/event"
"AynaLivePlayer/core/model"
"fmt"
"testing"
"time"
)
func TestPlayer(t *testing.T) {
player := NewMpvPlayer(event.MainManager, &logger.EmptyLogger{})
player.Start()
defer player.Stop()
player.ObserveProperty("time-pos", "testplayer.timepos", func(evnt *event.Event) {
fmt.Println(1, evnt.Data)
})
player.ObserveProperty("percent-pos", "testplayer.percentpos", func(evnt *event.Event) {
fmt.Println(2, evnt.Data)
})
player.Play(&model.Media{
Url: "https://ia600809.us.archive.org/19/items/VillagePeopleYMCAOFFICIALMusicVideo1978/Village%20People%20-%20YMCA%20OFFICIAL%20Music%20Video%201978.mp4",
})
time.Sleep(time.Second * 15)
}

View File

@@ -1,139 +0,0 @@
package provider
import (
"AynaLivePlayer/core/adapter"
"AynaLivePlayer/core/model"
"fmt"
"github.com/tidwall/gjson"
"net/url"
"regexp"
)
type Bilibili struct {
InfoApi string
FileApi string
SearchApi string
IdRegex0 *regexp.Regexp
IdRegex1 *regexp.Regexp
}
func NewBilibili(config adapter.MediaProviderConfig) adapter.MediaProvider {
return &Bilibili{
InfoApi: "https://www.bilibili.com/audio/music-service-c/web/song/info?sid=%s",
FileApi: "https://api.bilibili.com/audio/music-service-c/url?device=phone&mid=8047632&mobi_app=iphone&platform=ios&privilege=2&songid=%s&quality=2",
SearchApi: "https://api.bilibili.com/audio/music-service-c/s?search_type=music&keyword=%s&page=1&pagesize=100",
IdRegex0: regexp.MustCompile("^[0-9]+"),
IdRegex1: regexp.MustCompile("^au[0-9]+"),
}
}
func _newBilibili() *Bilibili {
return &Bilibili{
InfoApi: "https://www.bilibili.com/audio/music-service-c/web/song/info?sid=%s",
FileApi: "https://api.bilibili.com/audio/music-service-c/url?device=phone&mid=8047632&mobi_app=iphone&platform=ios&privilege=2&songid=%s&quality=2",
SearchApi: "https://api.bilibili.com/audio/music-service-c/s?search_type=music&keyword=%s&page=1&pagesize=100",
IdRegex0: regexp.MustCompile("^[0-9]+"),
IdRegex1: regexp.MustCompile("^au[0-9]+"),
}
}
var BilibiliAPI *Bilibili
func init() {
BilibiliAPI = _newBilibili()
Providers[BilibiliAPI.GetName()] = BilibiliAPI
}
func (b *Bilibili) GetName() string {
return "bilibili"
}
func (b *Bilibili) MatchMedia(keyword string) *model.Media {
if id := b.IdRegex0.FindString(keyword); id != "" {
return &model.Media{
Meta: model.Meta{
Name: b.GetName(),
Id: id,
},
}
}
if id := b.IdRegex1.FindString(keyword); id != "" {
return &model.Media{
Meta: model.Meta{
Name: b.GetName(),
Id: id[2:],
},
}
}
return nil
}
func (b *Bilibili) FormatPlaylistUrl(uri string) string {
return ""
}
func (b *Bilibili) GetPlaylist(playlist *model.Meta) ([]*model.Media, error) {
return nil, ErrorExternalApi
}
func (b *Bilibili) Search(keyword string) ([]*model.Media, error) {
resp := httpGetString(fmt.Sprintf(b.SearchApi, url.QueryEscape(keyword)), map[string]string{
"user-agent": "BiliMusic/2.233.3",
})
if resp == "" {
return nil, ErrorExternalApi
}
result := make([]*model.Media, 0)
gjson.Get(resp, "data.result").ForEach(func(key, value gjson.Result) bool {
result = append(result, &model.Media{
Title: value.Get("title").String(),
Cover: model.Picture{Url: value.Get("cover").String()},
Artist: value.Get("author").String(),
Meta: model.Meta{
Name: b.GetName(),
Id: value.Get("id").String(),
},
})
return true
})
return result, nil
}
func (b *Bilibili) UpdateMedia(media *model.Media) error {
resp := httpGetString(fmt.Sprintf(b.InfoApi, media.Meta.(model.Meta).Id), map[string]string{
"user-agent": "BiliMusic/2.233.3",
})
if resp == "" {
return ErrorExternalApi
}
if gjson.Get(resp, "data.title").String() == "" {
return ErrorExternalApi
}
media.Title = gjson.Get(resp, "data.title").String()
media.Cover.Url = gjson.Get(resp, "data.cover").String()
media.Artist = gjson.Get(resp, "data.author").String()
media.Album = media.Title
return nil
}
func (b *Bilibili) UpdateMediaUrl(media *model.Media) error {
resp := httpGetString(fmt.Sprintf(b.FileApi, media.Meta.(model.Meta).Id), map[string]string{
"user-agent": "BiliMusic/2.233.3",
})
if resp == "" {
return ErrorExternalApi
}
media.Header = map[string]string{
"user-agent": "BiliMusic/2.233.3",
}
uri := gjson.Get(resp, "data.cdns.0").String()
if uri == "" {
return ErrorExternalApi
}
media.Url = uri
return nil
}
func (k *Bilibili) UpdateMediaLyric(media *model.Media) error {
return nil
}

View File

@@ -1,60 +0,0 @@
package provider
import (
"AynaLivePlayer/core/adapter"
"AynaLivePlayer/core/model"
"fmt"
"testing"
)
func TestBilibili_Search(t *testing.T) {
var api adapter.MediaProvider = BilibiliAPI
result, err := api.Search("染 reol")
if err != nil {
fmt.Println(1, err)
return
}
fmt.Println(result)
media := result[0]
fmt.Println(*media)
err = api.UpdateMediaUrl(media)
fmt.Println(err)
fmt.Println(media.Url)
}
func TestBilibili_GetMusicMeta(t *testing.T) {
var api adapter.MediaProvider = BilibiliAPI
media := model.Media{
Meta: model.Meta{
Name: api.GetName(),
Id: "1560601",
},
}
err := api.UpdateMedia(&media)
fmt.Println(err)
if err != nil {
return
}
fmt.Println(media)
}
func TestBilibili_GetMusic(t *testing.T) {
var api adapter.MediaProvider = BilibiliAPI
media := model.Media{
Meta: model.Meta{
Name: api.GetName(),
Id: "1560601",
},
}
err := api.UpdateMedia(&media)
if err != nil {
return
}
err = api.UpdateMediaUrl(&media)
if err != nil {
return
}
fmt.Println(media)
fmt.Println(media.Url)
}

View File

@@ -1,174 +0,0 @@
package provider
import (
"AynaLivePlayer/common/util"
"AynaLivePlayer/core/adapter"
"AynaLivePlayer/core/model"
"fmt"
"github.com/jinzhu/copier"
"github.com/tidwall/gjson"
"net/url"
"regexp"
)
type BilibiliVideo struct {
InfoApi string
FileApi string
SearchApi string
BVRegex *regexp.Regexp
IdRegex *regexp.Regexp
PageRegex *regexp.Regexp
header map[string]string
}
func NewBilibiliVideo(config adapter.MediaProviderConfig) adapter.MediaProvider {
return &BilibiliVideo{
InfoApi: "https://api.bilibili.com/x/web-interface/view/detail?bvid=%s&aid=&jsonp=jsonp",
FileApi: "https://api.bilibili.com/x/player/playurl?type=&otype=json&fourk=1&qn=32&avid=&bvid=%s&cid=%s",
SearchApi: "https://api.bilibili.com/x/web-interface/search/type?search_type=video&page=1&keyword=%s",
BVRegex: regexp.MustCompile("^BV[0-9A-Za-z]+"),
IdRegex: regexp.MustCompile("^BV[0-9A-Za-z]+(\\?p=[0-9]+)?"),
PageRegex: regexp.MustCompile("p=[0-9]+"),
header: map[string]string{
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36",
"Referer": "https://www.bilibili.com/",
"Origin": "https://www.bilibili.com",
},
}
}
func _newBilibiliVideo() *BilibiliVideo {
return &BilibiliVideo{
InfoApi: "https://api.bilibili.com/x/web-interface/view/detail?bvid=%s&aid=&jsonp=jsonp",
FileApi: "https://api.bilibili.com/x/player/playurl?type=&otype=json&fourk=1&qn=32&avid=&bvid=%s&cid=%s",
SearchApi: "https://api.bilibili.com/x/web-interface/search/type?search_type=video&page=1&keyword=%s",
BVRegex: regexp.MustCompile("^BV[0-9A-Za-z]+"),
IdRegex: regexp.MustCompile("^BV[0-9A-Za-z]+(\\?p=[0-9]+)?"),
PageRegex: regexp.MustCompile("p=[0-9]+"),
header: map[string]string{
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:51.0) Gecko/20100101 Firefox/51.0",
"Referer": "https://www.bilibili.com/",
"Origin": "https://www.bilibili.com",
"Cookie": "buvid3=9A8B3564-BDA9-407F-B45F-D5C40786CA49167618infoc;",
},
}
}
var BilibiliVideoAPI *BilibiliVideo
func init() {
BilibiliVideoAPI = _newBilibiliVideo()
Providers[BilibiliVideoAPI.GetName()] = BilibiliVideoAPI
}
func (b *BilibiliVideo) getPage(bv string) int {
if page := b.PageRegex.FindString(bv); page != "" {
return util.Atoi(page[2:])
}
return 0
}
func (b *BilibiliVideo) getBv(bv string) string {
return b.BVRegex.FindString(bv)
}
func (b *BilibiliVideo) GetName() string {
return "bilibili-video"
}
func (b *BilibiliVideo) MatchMedia(keyword string) *model.Media {
if id := b.IdRegex.FindString(keyword); id != "" {
return &model.Media{
Meta: model.Meta{
Name: b.GetName(),
Id: id,
},
}
}
return nil
}
func (b *BilibiliVideo) GetPlaylist(playlist *model.Meta) ([]*model.Media, error) {
return nil, ErrorExternalApi
}
func (b *BilibiliVideo) FormatPlaylistUrl(uri string) string {
return ""
}
func (b *BilibiliVideo) Search(keyword string) ([]*model.Media, error) {
resp := httpGetString(fmt.Sprintf(b.SearchApi, url.QueryEscape(keyword)), b.header)
if resp == "" {
return nil, ErrorExternalApi
}
jresp := gjson.Parse(resp)
if jresp.Get("code").String() != "0" {
return nil, ErrorExternalApi
}
result := make([]*model.Media, 0)
r := regexp.MustCompile("</?em[^>]*>")
jresp.Get("data.result").ForEach(func(key, value gjson.Result) bool {
result = append(result, &model.Media{
Title: r.ReplaceAllString(value.Get("title").String(), ""),
Cover: model.Picture{Url: "https:" + value.Get("pic").String()},
Artist: value.Get("author").String(),
Meta: model.Meta{
Name: b.GetName(),
Id: value.Get("bvid").String(),
},
})
return true
})
return result, nil
}
func (b *BilibiliVideo) UpdateMedia(media *model.Media) error {
resp := httpGetString(fmt.Sprintf(b.InfoApi, b.getBv(media.Meta.(model.Meta).Id)), nil)
if resp == "" {
return ErrorExternalApi
}
jresp := gjson.Parse(resp)
if jresp.Get("data.View.title").String() == "" {
return ErrorExternalApi
}
media.Title = jresp.Get("data.View.title").String()
media.Artist = jresp.Get("data.View.owner.name").String()
media.Cover.Url = jresp.Get("data.View.pic").String()
media.Album = media.Title
return nil
}
func (b *BilibiliVideo) UpdateMediaUrl(media *model.Media) error {
resp := httpGetString(fmt.Sprintf(b.InfoApi, b.getBv(media.Meta.(model.Meta).Id)), nil)
if resp == "" {
return ErrorExternalApi
}
jresp := gjson.Parse(resp)
page := b.getPage(media.Meta.(model.Meta).Id) - 1
cid := jresp.Get(fmt.Sprintf("data.View.pages.%d.cid", page)).String()
if cid == "" {
cid = jresp.Get("data.View.cid").String()
}
if cid == "" {
return ErrorExternalApi
}
resp = httpGetString(fmt.Sprintf(b.FileApi, b.getBv(media.Meta.(model.Meta).Id), cid), b.header)
if resp == "" {
return ErrorExternalApi
}
jresp = gjson.Parse(resp)
uri := jresp.Get("data.durl.0.url").String()
if uri == "" {
return ErrorExternalApi
}
media.Url = uri
header := make(map[string]string)
_ = copier.Copy(&header, &b.header)
header["Referer"] = fmt.Sprintf("https://www.bilibili.com/video/%s", b.getBv(media.Meta.(model.Meta).Id))
media.Header = b.header
return nil
}
func (b *BilibiliVideo) UpdateMediaLyric(media *model.Media) error {
return nil
}

View File

@@ -1,100 +0,0 @@
package provider
import (
"AynaLivePlayer/core/adapter"
"AynaLivePlayer/core/model"
"fmt"
"regexp"
"testing"
)
func TestBV_GetMusicMeta(t *testing.T) {
var api adapter.MediaProvider = BilibiliVideoAPI
media := model.Media{
Meta: model.Meta{
Name: api.GetName(),
Id: "BV1434y1q71P",
},
}
err := api.UpdateMedia(&media)
fmt.Println(err)
if err != nil {
return
}
fmt.Println(media)
}
func TestBV_GetMusic(t *testing.T) {
var api adapter.MediaProvider = BilibiliVideoAPI
media := model.Media{
Meta: model.Meta{
Name: api.GetName(),
Id: "BV1434y1q71P",
},
}
err := api.UpdateMedia(&media)
if err != nil {
return
}
err = api.UpdateMediaUrl(&media)
if err != nil {
return
}
//fmt.Println(media)
fmt.Println(media.Url)
}
func TestBV_Regex(t *testing.T) {
fmt.Println(regexp.MustCompile("^BV[0-9A-Za-z]+(\\?p=[0-9]+)?").FindString("BV1gA411P7ir?p=3"))
}
func TestBV_GetMusicMeta2(t *testing.T) {
var api adapter.MediaProvider = BilibiliVideoAPI
media := model.Media{
Meta: model.Meta{
Name: api.GetName(),
Id: "BV1gA411P7ir?p=3",
},
}
err := api.UpdateMedia(&media)
fmt.Println(err)
if err != nil {
return
}
fmt.Println(media)
}
func TestBV_GetMusic2(t *testing.T) {
var api adapter.MediaProvider = BilibiliVideoAPI
media := model.Media{
Meta: model.Meta{
Name: api.GetName(),
Id: "BV1gA411P7ir?p=3",
},
}
err := api.UpdateMedia(&media)
if err != nil {
return
}
err = api.UpdateMediaUrl(&media)
if err != nil {
return
}
//fmt.Println(media)
fmt.Println(media.Url)
}
func TestBV_Search(t *testing.T) {
var api adapter.MediaProvider = BilibiliVideoAPI
result, err := api.Search("家有女友")
if err != nil {
fmt.Println(1, err)
return
}
fmt.Println(len(result))
for _, r := range result {
fmt.Println(r.Artist)
}
}

View File

@@ -1,8 +0,0 @@
package provider
import "errors"
var (
ErrorExternalApi = errors.New("external api error")
ErrorNoSuchProvider = errors.New("not such provider")
)

View File

@@ -1,29 +0,0 @@
package provider
import (
"github.com/go-resty/resty/v2"
"time"
)
func httpGet(url string, header map[string]string) (*resty.Response, error) {
resp, err := resty.New().
SetTimeout(time.Second * 3).R().
SetHeaders(header).
Get(url)
return resp, err
}
func httpGetString(url string, header map[string]string) string {
resp, err := httpGet(url, header)
if err != nil {
return ""
}
return resp.String()
}
func httpHead(url string, header map[string]string) (*resty.Response, error) {
resp, err := resty.New().
SetTimeout(time.Second * 3).R().
SetHeaders(header).
Head(url)
return resp, err
}

View File

@@ -1,230 +0,0 @@
package provider
import (
"AynaLivePlayer/core/adapter"
"AynaLivePlayer/core/model"
"fmt"
"github.com/tidwall/gjson"
"html"
"net/url"
"regexp"
"strings"
)
type Kuwo struct {
InfoApi string
FileApi string
SearchCookie string
SearchApi string
LyricApi string
PlaylistApi string
PlaylistRegex0 *regexp.Regexp
PlaylistRegex1 *regexp.Regexp
IdRegex0 *regexp.Regexp
IdRegex1 *regexp.Regexp
}
func NewKuwo(config adapter.MediaProviderConfig) adapter.MediaProvider {
return &Kuwo{
InfoApi: "http://www.kuwo.cn/api/www/music/musicInfo?mid=%s&httpsStatus=1",
//FileApi: "http://www.kuwo.cn/api/v1/www/music/playUrl?mid=%d&type=music&httpsStatus=1",
FileApi: "http://antiserver.kuwo.cn/anti.s?type=convert_url&format=mp3&response=url&rid=MUSIC_%s",
SearchCookie: "http://kuwo.cn/search/list?key=%s",
SearchApi: "http://www.kuwo.cn/api/www/search/searchMusicBykeyWord?key=%s&pn=%d&rn=%d",
LyricApi: "http://m.kuwo.cn/newh5/singles/songinfoandlrc?musicId=%s",
PlaylistApi: "http://www.kuwo.cn/api/www/playlist/playListInfo?pid=%s&pn=%d&rn=%d&httpsStatus=1",
PlaylistRegex0: regexp.MustCompile("[0-9]+"),
PlaylistRegex1: regexp.MustCompile("playlist/[0-9]+"),
IdRegex0: regexp.MustCompile("^[0-9]+"),
IdRegex1: regexp.MustCompile("^kw[0-9]+"),
}
}
func _newKuwo() *Kuwo {
return &Kuwo{
InfoApi: "http://www.kuwo.cn/api/www/music/musicInfo?mid=%s&httpsStatus=1",
//FileApi: "http://www.kuwo.cn/api/v1/www/music/playUrl?mid=%d&type=music&httpsStatus=1",
FileApi: "http://antiserver.kuwo.cn/anti.s?type=convert_url&format=mp3&response=url&rid=MUSIC_%s",
SearchCookie: "http://kuwo.cn/search/list?key=%s",
SearchApi: "http://www.kuwo.cn/api/www/search/searchMusicBykeyWord?key=%s&pn=%d&rn=%d",
LyricApi: "http://m.kuwo.cn/newh5/singles/songinfoandlrc?musicId=%s",
PlaylistApi: "http://www.kuwo.cn/api/www/playlist/playListInfo?pid=%s&pn=%d&rn=%d&httpsStatus=1",
PlaylistRegex0: regexp.MustCompile("[0-9]+"),
PlaylistRegex1: regexp.MustCompile("playlist/[0-9]+"),
IdRegex0: regexp.MustCompile("^[0-9]+"),
IdRegex1: regexp.MustCompile("^kw[0-9]+"),
}
}
var KuwoAPI *Kuwo
func init() {
KuwoAPI = _newKuwo()
Providers[KuwoAPI.GetName()] = KuwoAPI
}
func (k *Kuwo) GetName() string {
return "kuwo"
}
func (k *Kuwo) MatchMedia(keyword string) *model.Media {
if id := k.IdRegex0.FindString(keyword); id != "" {
return &model.Media{
Meta: model.Meta{
Name: k.GetName(),
Id: id,
},
}
}
if id := k.IdRegex1.FindString(keyword); id != "" {
return &model.Media{
Meta: model.Meta{
Name: k.GetName(),
Id: id[2:],
},
}
}
return nil
}
func (k *Kuwo) FormatPlaylistUrl(uri string) string {
var id string
id = k.PlaylistRegex0.FindString(uri)
if id != "" {
return id
}
id = k.PlaylistRegex1.FindString(uri)
if id != "" {
return id[9:]
}
return ""
}
//func (k *Kuwo) _kuwoGet(url string) string {
// searchCookie, err := httpHead(fmt.Sprintf(k.SearchCookie, "any"), nil)
// if err != nil {
// return ""
// }
// kwToken, ok := util.SliceString(regexp.MustCompile("kw_token=([^;])*;").FindString(searchCookie.Header().Get("set-cookie")), 9, -1)
// if !ok {
// return ""
// }
// return httpGetString(url, map[string]string{
// "cookie": "kw_token=" + kwToken,
// "csrf": kwToken,
// "referer": "http://www.kuwo.cn/",
// })
//}
func (k *Kuwo) _kuwoGet(url string) string {
return httpGetString(url, map[string]string{
"cookie": "kw_token=" + "95MWTYC4FP",
"csrf": "95MWTYC4FP",
"referer": "http://www.kuwo.cn/",
})
}
func (k *Kuwo) Search(keyword string) ([]*model.Media, error) {
resp := k._kuwoGet(fmt.Sprintf(k.SearchApi, url.QueryEscape(keyword), 1, 64))
if resp == "" {
return nil, ErrorExternalApi
}
result := make([]*model.Media, 0)
gjson.Parse(resp).Get("data.list").ForEach(func(key, value gjson.Result) bool {
result = append(result, &model.Media{
Title: html.UnescapeString(value.Get("name").String()),
Cover: model.Picture{Url: value.Get("pic").String()},
Artist: value.Get("artist").String(),
Album: value.Get("album").String(),
Meta: model.Meta{
Name: k.GetName(),
Id: value.Get("rid").String(),
},
})
return true
})
return result, nil
}
func (k *Kuwo) UpdateMedia(media *model.Media) error {
resp := k._kuwoGet(fmt.Sprintf(k.InfoApi, media.Meta.(model.Meta).Id))
if resp == "" {
return ErrorExternalApi
}
jresp := gjson.Parse(resp)
if jresp.Get("data.musicrid").String() == "" {
return ErrorExternalApi
}
media.Title = html.UnescapeString(jresp.Get("data.name").String())
media.Cover.Url = jresp.Get("data.pic").String()
media.Artist = jresp.Get("data.artist").String()
media.Album = jresp.Get("data.album").String()
return nil
}
func (k *Kuwo) UpdateMediaUrl(media *model.Media) error {
result := httpGetString(fmt.Sprintf(k.FileApi, media.Meta.(model.Meta).Id), nil)
if result == "" {
return ErrorExternalApi
}
media.Url = result
return nil
}
func (k *Kuwo) UpdateMediaLyric(media *model.Media) error {
result := httpGetString(fmt.Sprintf(k.LyricApi, media.Meta.(model.Meta).Id), nil)
if result == "" {
return ErrorExternalApi
}
lrcs := make([]string, 0)
gjson.Parse(result).Get("data.lrclist").ForEach(func(key, value gjson.Result) bool {
lrcs = append(lrcs, fmt.Sprintf("[00:%s]%s", value.Get("time").String(), value.Get("lineLyric").String()))
return true
})
media.Lyric = strings.Join(lrcs, "\n")
return nil
}
func (k *Kuwo) GetPlaylist(playlist *model.Meta) ([]*model.Media, error) {
medias := make([]*model.Media, 0)
var resp string
var jresp gjson.Result
for i := 1; i <= 20; i++ {
resp = k._kuwoGet(fmt.Sprintf(k.PlaylistApi, playlist.Id, i, 128))
if resp == "" {
break
}
//fmt.Println(resp[:100])
jresp = gjson.Parse(resp)
//fmt.Println(jresp.Get("code").String())
if jresp.Get("code").String() != "200" {
break
}
cnt := int(jresp.Get("data.total").Int())
//fmt.Println(cnt)
//fmt.Println(len(jresp.Get("data.musicList").Array()))
jresp.Get("data.musicList").ForEach(func(key, value gjson.Result) bool {
medias = append(
medias,
&model.Media{
Title: html.UnescapeString(value.Get("name").String()),
Artist: value.Get("artist").String(),
Cover: model.Picture{Url: value.Get("pic").String()},
Album: value.Get("album").String(),
Meta: model.Meta{
Name: k.GetName(),
Id: value.Get("rid").String(),
},
})
return true
})
if cnt <= i*100 {
break
}
}
if len(medias) == 0 {
return nil, ErrorExternalApi
}
return medias, nil
}

View File

@@ -1,89 +0,0 @@
package provider
import (
"AynaLivePlayer/core/adapter"
"AynaLivePlayer/core/model"
"fmt"
"testing"
)
func TestKuwo_Search(t *testing.T) {
var api adapter.MediaProvider = KuwoAPI
result, err := api.Search("染 reol")
if err != nil {
return
}
fmt.Println(result)
media := result[0]
err = api.UpdateMediaUrl(media)
fmt.Println(err)
fmt.Println(media.Url)
}
func TestKuwo_GetMusicMeta(t *testing.T) {
var api adapter.MediaProvider = KuwoAPI
media := model.Media{
Meta: model.Meta{
Name: api.GetName(),
Id: "22804772",
},
}
err := api.UpdateMedia(&media)
fmt.Println(err)
if err != nil {
return
}
fmt.Println(media)
}
func TestKuwo_GetMusic(t *testing.T) {
var api adapter.MediaProvider = KuwoAPI
media := model.Media{
Meta: model.Meta{
Name: api.GetName(),
Id: "22804772",
},
}
err := api.UpdateMedia(&media)
if err != nil {
return
}
err = api.UpdateMediaUrl(&media)
if err != nil {
return
}
fmt.Println(media)
fmt.Println(media.Url)
}
func TestKuwo_UpdateMediaLyric(t *testing.T) {
var api adapter.MediaProvider = KuwoAPI
media := model.Media{
Meta: model.Meta{
Name: api.GetName(),
Id: "22804772",
},
}
err := api.UpdateMediaLyric(&media)
fmt.Println(err)
fmt.Println(media.Lyric)
}
func TestKuwo_GetPlaylist(t *testing.T) {
var api adapter.MediaProvider = KuwoAPI
playlist, err := api.GetPlaylist(&model.Meta{
Name: api.GetName(),
//Id: "1082685104",
Id: "2959147566",
})
if err != nil {
fmt.Println(err)
return
}
fmt.Println(len(playlist))
for _, media := range playlist {
fmt.Println(media.Title, media.Artist, media.Album)
}
}

View File

@@ -1,165 +0,0 @@
package provider
import (
"AynaLivePlayer/common/util"
"AynaLivePlayer/core/adapter"
"AynaLivePlayer/core/model"
"os"
"sort"
"strings"
)
type _LocalPlaylist struct {
Name string
Medias []*model.Media
}
type Local struct {
localDir string
Playlists []*_LocalPlaylist
}
func NewLocalCtor(config adapter.MediaProviderConfig) adapter.MediaProvider {
localDir, ok := config["local_dir"]
if !ok {
localDir = "./local"
}
l := &Local{Playlists: make([]*_LocalPlaylist, 0), localDir: localDir}
if err := os.MkdirAll(localDir, 0755); err != nil {
return l
}
for _, n := range getPlaylistNames(localDir) {
l.Playlists = append(l.Playlists, &_LocalPlaylist{Name: n})
}
for i, _ := range l.Playlists {
_ = readLocalPlaylist(localDir, l.Playlists[i])
}
LocalAPI = l
Providers[LocalAPI.GetName()] = LocalAPI
return l
}
var LocalAPI *Local
func NewLocal(localdir string) *Local {
l := &Local{Playlists: make([]*_LocalPlaylist, 0), localDir: localdir}
if err := os.MkdirAll(localdir, 0755); err != nil {
return l
}
for _, n := range getPlaylistNames(localdir) {
l.Playlists = append(l.Playlists, &_LocalPlaylist{Name: n})
}
for i, _ := range l.Playlists {
_ = readLocalPlaylist(localdir, l.Playlists[i])
}
LocalAPI = l
Providers[LocalAPI.GetName()] = LocalAPI
return l
}
func (l *Local) GetName() string {
return "local"
}
func (l *Local) MatchMedia(keyword string) *model.Media {
return nil
}
func (l *Local) UpdateMediaLyric(media *model.Media) error {
// already update in UpdateMedia, do nothing
return nil
}
func (l *Local) FormatPlaylistUrl(uri string) string {
return uri
}
func (l *Local) GetPlaylist(playlist *model.Meta) ([]*model.Media, error) {
var pl *_LocalPlaylist = nil
for _, p := range l.Playlists {
if p.Name == playlist.Id {
pl = p
}
}
if pl == nil {
l.Playlists = append(l.Playlists, &_LocalPlaylist{Name: playlist.Id})
pl = l.Playlists[len(l.Playlists)-1]
}
if readLocalPlaylist(l.localDir, pl) != nil {
return nil, ErrorExternalApi
}
return pl.Medias, nil
}
func (l *Local) Search(keyword string) ([]*model.Media, error) {
allMedias := make([]*model.Media, 0)
for _, p := range l.Playlists {
for _, m := range p.Medias {
allMedias = append(allMedias, m)
}
}
MediaSort(keyword, allMedias)
c := util.Min(len(allMedias), 32)
medias := make([]*model.Media, c)
for i := 0; i < c; i++ {
medias[i] = allMedias[i].Copy()
}
return medias, nil
}
func (l *Local) SearchV1(keyword string) ([]*model.Media, error) {
result := make([]struct {
M *model.Media
N int
}, 0)
keywords := strings.Split(keyword, " ")
for _, p := range l.Playlists {
for _, m := range p.Medias {
title := strings.ToLower(m.Title)
artist := strings.ToLower(m.Artist)
n := 0
for _, k := range keywords {
kw := strings.ToLower(k)
if strings.Contains(title, kw) || strings.Contains(artist, kw) {
n++
}
if kw == title {
n += 3
}
}
if n > 0 {
result = append(result, struct {
M *model.Media
N int
}{M: m, N: n})
}
}
}
sort.Slice(result, func(i, j int) bool {
return result[i].N > result[j].N
})
medias := make([]*model.Media, len(result))
for i, r := range result {
medias[i] = r.M.Copy()
}
return medias, nil
}
func (l *Local) UpdateMedia(media *model.Media) error {
mediaPath := media.Meta.(model.Meta).Id
_, err := os.Stat(mediaPath)
if err != nil {
return err
}
return readMediaFile(media)
}
func (l *Local) UpdateMediaUrl(media *model.Media) error {
mediaPath := media.Meta.(model.Meta).Id
_, err := os.Stat(mediaPath)
if err != nil {
return err
}
media.Url = mediaPath
return nil
}

View File

@@ -1,72 +0,0 @@
package provider
import (
"AynaLivePlayer/common/util"
"AynaLivePlayer/core/model"
"github.com/dhowden/tag"
"io/ioutil"
"os"
"path/filepath"
)
func getPlaylistNames(localdir string) []string {
names := make([]string, 0)
items, _ := ioutil.ReadDir(localdir)
for _, item := range items {
if item.IsDir() {
names = append(names, item.Name())
}
}
return names
}
// readLocalPlaylist read files under a directory
// and return a _LocalPlaylist object.
// This function assume this directory exists
func readLocalPlaylist(localdir string, playlist *_LocalPlaylist) error {
p1th := playlist.Name
playlist.Medias = make([]*model.Media, 0)
fullPath := filepath.Join(localdir, p1th)
if _, err := os.Stat(fullPath); os.IsNotExist(err) {
return err
}
items, _ := ioutil.ReadDir(fullPath)
for _, item := range items {
// if item is a file, read file
if !item.IsDir() {
fn := item.Name()
media := model.Media{
Meta: model.Meta{
Name: LocalAPI.GetName(),
Id: filepath.Join(fullPath, fn),
},
}
if readMediaFile(&media) != nil {
continue
}
playlist.Medias = append(playlist.Medias, &media)
}
}
return nil
}
func readMediaFile(media *model.Media) error {
p := media.Meta.(model.Meta).Id
f, err := os.Open(p)
if err != nil {
return err
}
defer f.Close()
meta, err := tag.ReadFrom(f)
if err != nil {
return err
}
media.Title = util.GetOrDefault(meta.Title(), filepath.Base(p))
media.Artist = util.GetOrDefault(meta.Artist(), "Unknown")
media.Album = util.GetOrDefault(meta.Album(), "Unknown")
media.Lyric = meta.Lyrics()
if meta.Picture() != nil {
media.Cover.Data = meta.Picture().Data
}
return nil
}

View File

@@ -1,25 +0,0 @@
package provider
import (
"fmt"
"io/ioutil"
"testing"
)
func TestLocal_Read(t *testing.T) {
items, _ := ioutil.ReadDir(".")
for _, item := range items {
if item.IsDir() {
subitems, _ := ioutil.ReadDir(item.Name())
for _, subitem := range subitems {
if !subitem.IsDir() {
// handle file there
fmt.Println(item.Name() + "/" + subitem.Name())
}
}
} else {
// handle file there
fmt.Println(item.Name())
}
}
}

View File

@@ -1,239 +0,0 @@
package provider
import (
"AynaLivePlayer/common/util"
"AynaLivePlayer/core/adapter"
"AynaLivePlayer/core/model"
neteaseApi "github.com/XiaoMengXinX/Music163Api-Go/api"
neteaseTypes "github.com/XiaoMengXinX/Music163Api-Go/types"
neteaseUtil "github.com/XiaoMengXinX/Music163Api-Go/utils"
"regexp"
"strconv"
"strings"
)
type Netease struct {
PlaylistRegex0 *regexp.Regexp
PlaylistRegex1 *regexp.Regexp
ReqData neteaseUtil.RequestData
IdRegex0 *regexp.Regexp
IdRegex1 *regexp.Regexp
loginStatus neteaseTypes.LoginStatusData
}
func NewNetease(config adapter.MediaProviderConfig) adapter.MediaProvider {
return &Netease{
PlaylistRegex0: regexp.MustCompile("^[0-9]+$"),
// https://music.163.com/playlist?id=2382819181&userid=95906480
PlaylistRegex1: regexp.MustCompile("playlist\\?id=[0-9]+"),
ReqData: neteaseUtil.RequestData{
Headers: neteaseUtil.Headers{
{
"X-Real-IP",
"118.88.88.88",
},
},
},
IdRegex0: regexp.MustCompile("^[0-9]+"),
IdRegex1: regexp.MustCompile("^wy[0-9]+"),
}
}
func _newNetease() *Netease {
return &Netease{
PlaylistRegex0: regexp.MustCompile("^[0-9]+$"),
// https://music.163.com/playlist?id=2382819181&userid=95906480
PlaylistRegex1: regexp.MustCompile("playlist\\?id=[0-9]+"),
ReqData: neteaseUtil.RequestData{
Headers: neteaseUtil.Headers{
{
"X-Real-IP",
"118.88.88.88",
},
},
},
IdRegex0: regexp.MustCompile("^[0-9]+"),
IdRegex1: regexp.MustCompile("^wy[0-9]+"),
}
}
var NeteaseAPI *Netease
func init() {
NeteaseAPI = _newNetease()
Providers[NeteaseAPI.GetName()] = NeteaseAPI
}
// Netease private helper method
func _neteaseGetArtistNames(data neteaseTypes.SongDetailData) string {
artists := make([]string, 0)
for _, a := range data.Ar {
artists = append(artists, a.Name)
}
return strings.Join(artists, ",")
}
// MediaProvider implementation
func (n *Netease) GetName() string {
return "netease"
}
func (n *Netease) MatchMedia(keyword string) *model.Media {
if id := n.IdRegex0.FindString(keyword); id != "" {
return &model.Media{
Meta: model.Meta{
Name: n.GetName(),
Id: id,
},
}
}
if id := n.IdRegex1.FindString(keyword); id != "" {
return &model.Media{
Meta: model.Meta{
Name: n.GetName(),
Id: id[2:],
},
}
}
return nil
}
func (n *Netease) FormatPlaylistUrl(uri string) string {
var id string
id = n.PlaylistRegex0.FindString(uri)
if id != "" {
return id
}
id = n.PlaylistRegex1.FindString(uri)
if id != "" {
return id[12:]
}
return ""
}
func (n *Netease) GetPlaylist(playlist *model.Meta) ([]*model.Media, error) {
result, err := neteaseApi.GetPlaylistDetail(
n.ReqData, util.Atoi(playlist.Id))
if err != nil || result.Code != 200 {
return nil, ErrorExternalApi
}
cnt := len(result.Playlist.TrackIds)
if cnt == 0 {
return nil, ErrorExternalApi
}
ids := make([]int, len(result.Playlist.TrackIds))
for i := 0; i < cnt; i++ {
ids[i] = result.Playlist.TrackIds[i].Id
}
medias := make([]*model.Media, 0, cnt)
for index := 0; index < len(ids); index += 1000 {
result2, err := neteaseApi.GetSongDetail(
n.ReqData,
ids[index:util.IntMin(index+1000, len(ids))])
if err != nil || result2.Code != 200 {
break
}
cnt = len(result2.Songs)
if cnt == 0 {
break
}
for i := 0; i < cnt; i++ {
medias = append(medias, &model.Media{
Title: result2.Songs[i].Name,
Artist: _neteaseGetArtistNames(result2.Songs[i]),
Cover: model.Picture{Url: result2.Songs[i].Al.PicUrl},
Album: result2.Songs[i].Al.Name,
Url: "",
Header: nil,
User: nil,
Meta: model.Meta{
Name: n.GetName(),
Id: strconv.Itoa(result2.Songs[i].Id),
},
})
}
}
if len(medias) == 0 {
return nil, ErrorExternalApi
}
return medias, nil
}
func (n *Netease) Search(keyword string) ([]*model.Media, error) {
rawResult, err := neteaseApi.SearchSong(
n.ReqData,
neteaseApi.SearchSongConfig{
Keyword: keyword,
Limit: 30,
Offset: 0,
})
if err != nil || rawResult.Code != 200 {
return nil, ErrorExternalApi
}
medias := make([]*model.Media, 0)
for _, song := range rawResult.Result.Songs {
artists := make([]string, 0)
for _, a := range song.Artists {
artists = append(artists, a.Name)
}
medias = append(medias, &model.Media{
Title: song.Name,
Artist: strings.Join(artists, ","),
Cover: model.Picture{},
Album: song.Album.Name,
Url: "",
Header: nil,
Meta: model.Meta{
Name: n.GetName(),
Id: strconv.Itoa(song.Id),
},
})
}
return medias, nil
}
func (n *Netease) UpdateMedia(media *model.Media) error {
result, err := neteaseApi.GetSongDetail(
n.ReqData,
[]int{util.Atoi(media.Meta.(model.Meta).Id)})
if err != nil || result.Code != 200 {
return ErrorExternalApi
}
if len(result.Songs) == 0 {
return ErrorExternalApi
}
media.Title = result.Songs[0].Name
media.Cover.Url = result.Songs[0].Al.PicUrl
media.Album = result.Songs[0].Al.Name
media.Artist = _neteaseGetArtistNames(result.Songs[0])
return nil
}
func (n *Netease) UpdateMediaUrl(media *model.Media) error {
result, err := neteaseApi.GetSongURL(
n.ReqData,
neteaseApi.SongURLConfig{Ids: []int{util.Atoi(media.Meta.(model.Meta).Id)}})
if err != nil || result.Code != 200 {
return ErrorExternalApi
}
if len(result.Data) == 0 {
return ErrorExternalApi
}
if result.Data[0].Code != 200 {
return ErrorExternalApi
}
media.Url = result.Data[0].Url
return nil
}
func (n *Netease) UpdateMediaLyric(media *model.Media) error {
result, err := neteaseApi.GetSongLyric(n.ReqData, util.Atoi(media.Meta.(model.Meta).Id))
if err != nil || result.Code != 200 {
return ErrorExternalApi
}
media.Lyric = result.Lrc.Lyric
return nil
}

View File

@@ -1,62 +0,0 @@
package provider
import (
"fmt"
neteaseApi "github.com/XiaoMengXinX/Music163Api-Go/api"
"net/http"
)
// Netease other method
func (n *Netease) UpdateStatus() {
status, _ := neteaseApi.GetLoginStatus(n.ReqData)
n.loginStatus = status
}
// IsLogin check if current cookie is a login user
func (n *Netease) IsLogin() bool {
return n.loginStatus.Profile.UserId != 0
}
func (n *Netease) Nickname() string {
return n.loginStatus.Profile.Nickname
}
func (n *Netease) GetQrLoginKey() string {
unikey, err := neteaseApi.GetQrUnikey(n.ReqData)
if err != nil {
return ""
}
return unikey.Unikey
}
func (n *Netease) GetQrLoginUrl(key string) string {
return fmt.Sprintf("https://music.163.com/login?codekey=%s", key)
}
func (n *Netease) CheckQrLogin(key string) (bool, string) {
login, h, err := neteaseApi.CheckQrLogin(n.ReqData, key)
if err != nil {
return false, ""
}
// if login.Code == 800 || login.Code == 803. login success
if login.Code != 800 && login.Code != 803 {
return false, login.Message
}
cookies := make([]*http.Cookie, 0)
for _, c := range (&http.Response{Header: h}).Cookies() {
if c.Name == "MUSIC_U" || c.Name == "__csrf" {
cookies = append(cookies, c)
}
}
n.ReqData.Cookies = cookies
return true, login.Message
}
func (n *Netease) Logout() {
n.ReqData.Cookies = []*http.Cookie{
{Name: "MUSIC_U", Value: ""},
{Name: "__csrf", Value: ""},
}
return
}

View File

@@ -1,104 +0,0 @@
package provider
import (
"AynaLivePlayer/core/adapter"
"AynaLivePlayer/core/model"
"fmt"
"testing"
)
func TestNetease_Search(t *testing.T) {
var api adapter.MediaProvider = NeteaseAPI
result, err := api.Search("染 reol")
if err != nil {
return
}
fmt.Println(result)
media := result[0]
fmt.Println(media)
err = api.UpdateMediaUrl(media)
fmt.Println(err)
fmt.Println(media.Url)
}
func TestNetease_Search2(t *testing.T) {
var api adapter.MediaProvider = NeteaseAPI
result, err := api.Search("出山")
if err != nil {
return
}
t.Log(result)
media := result[0]
t.Log(media)
err = api.UpdateMediaUrl(media)
t.Log(err)
t.Log(media.Url)
}
func TestNetease_GetMusicMeta(t *testing.T) {
var api adapter.MediaProvider = NeteaseAPI
media := model.Media{
Meta: model.Meta{
Name: api.GetName(),
Id: "33516503",
},
}
err := api.UpdateMedia(&media)
fmt.Println(err)
if err != nil {
return
}
fmt.Println(media)
}
func TestNetease_GetMusic(t *testing.T) {
var api adapter.MediaProvider = NeteaseAPI
media := model.Media{
Meta: model.Meta{
Name: api.GetName(),
Id: "33516503",
},
}
err := api.UpdateMedia(&media)
if err != nil {
return
}
err = api.UpdateMediaUrl(&media)
if err != nil {
return
}
fmt.Println(media)
fmt.Println(media.Url)
}
func TestNetease_GetPlaylist(t *testing.T) {
var api adapter.MediaProvider = NeteaseAPI
playlist, err := api.GetPlaylist(&model.Meta{
Name: api.GetName(),
//Id: "2520739691",
Id: "2382819181",
})
if err != nil {
fmt.Println(err)
return
}
fmt.Println(len(playlist))
for _, media := range playlist {
fmt.Println(media.Title, media.Artist, media.Album)
}
}
func TestNetease_UpdateMediaLyric(t *testing.T) {
var api adapter.MediaProvider = NeteaseAPI
media := model.Media{
Meta: model.Meta{
Name: api.GetName(),
Id: "33516503",
},
}
err := api.UpdateMediaLyric(&media)
fmt.Println(err)
fmt.Println(media.Lyric)
}

View File

@@ -1,65 +0,0 @@
package provider
import (
"AynaLivePlayer/core/adapter"
model "AynaLivePlayer/core/model"
)
var RegisteredProviders = map[string]adapter.MediaProviderCtor{
"netease": NewNetease,
"local": NewLocalCtor,
"kuwo": NewKuwo,
"bilibili": NewBilibili,
"bilibili-video": NewBilibiliVideo,
}
var Providers map[string]adapter.MediaProvider = make(map[string]adapter.MediaProvider)
func GetPlaylist(meta *model.Meta) ([]*model.Media, error) {
if v, ok := Providers[meta.Name]; ok {
return v.GetPlaylist(meta)
}
return nil, ErrorNoSuchProvider
}
func FormatPlaylistUrl(pname, uri string) (string, error) {
if v, ok := Providers[pname]; ok {
return v.FormatPlaylistUrl(uri), nil
}
return "", ErrorNoSuchProvider
}
func MatchMedia(provider string, keyword string) *model.Media {
if v, ok := Providers[provider]; ok {
return v.MatchMedia(keyword)
}
return nil
}
func Search(provider string, keyword string) ([]*model.Media, error) {
if v, ok := Providers[provider]; ok {
return v.Search(keyword)
}
return nil, ErrorNoSuchProvider
}
func UpdateMedia(media *model.Media) error {
if v, ok := Providers[media.Meta.(model.Meta).Name]; ok {
return v.UpdateMedia(media)
}
return ErrorNoSuchProvider
}
func UpdateMediaUrl(media *model.Media) error {
if v, ok := Providers[media.Meta.(model.Meta).Name]; ok {
return v.UpdateMediaUrl(media)
}
return ErrorNoSuchProvider
}
func UpdateMediaLyric(media *model.Media) error {
if v, ok := Providers[media.Meta.(model.Meta).Name]; ok {
return v.UpdateMediaLyric(media)
}
return ErrorNoSuchProvider
}

View File

@@ -1,26 +0,0 @@
package provider
import (
"AynaLivePlayer/common/util"
"AynaLivePlayer/core/model"
"sort"
)
func MediaSort(keyword string, medias []*model.Media) {
mediaDist := make([]struct {
media *model.Media
dist int
}, len(medias))
for i, media := range medias {
mediaDist[i].media = media
mediaDist[i].dist = util.StrLen(util.LongestCommonString(keyword, media.Title)) +
util.StrLen(util.LongestCommonString(keyword, media.Artist))
}
sort.Slice(mediaDist, func(i, j int) bool {
return mediaDist[i].dist > mediaDist[j].dist
})
for i, media := range mediaDist {
medias[i] = media.media
}
return
}

View File

@@ -1,57 +0,0 @@
package main
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"strconv"
)
var w fyne.Window
func main() {
a := app.NewWithID("io.fyne.demo")
a.SetIcon(theme.FyneLogo())
w = a.NewWindow("Fyne Demo")
Regen(w)
w.Resize(fyne.NewSize(1080, 720))
w.ShowAndRun()
}
func Regen(w fyne.Window) {
tabs := container.NewDocTabs()
for _, datum := range generateData(100) {
tabs.Append(newItemTab(&datum))
}
w.SetContent(tabs)
}
func generateData(n int) (result []int) {
for i := 0; i < n; i++ {
result = append(result, i)
}
return
}
func newItemTab(i *int) *container.TabItem {
c := container.NewVBox(
BindIntWithEntry(i),
widget.NewButton("Regen", func() {
Regen(w)
}),
)
return container.NewTabItemWithIcon(strconv.Itoa(*i), theme.MenuIcon(), c)
}
func BindIntWithLabel(k *int) *widget.Label {
b := binding.BindInt(k)
return widget.NewLabelWithData(binding.IntToString(b))
}
func BindIntWithEntry(k *int) *widget.Entry {
b := binding.BindInt(k)
return widget.NewEntryWithData(binding.IntToString(b))
}

View File

@@ -1,5 +0,0 @@
[Details]
Icon = "../../theme/icons/fyne.png"
Name = "Fyne Demo"
ID = "io.fyne.demo"
Build = 6

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

File diff suppressed because one or more lines are too long

View File

@@ -1,43 +0,0 @@
package data
//go:generate fyne bundle -package data -o bundled.go assets
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/theme"
)
// ThemedResource is a resource wrapper that will return an appropriate resource
// for the currently selected theme.
type ThemedResource struct {
dark, light fyne.Resource
}
func isLight() bool {
r, g, b, _ := theme.ForegroundColor().RGBA()
return r < 0xaaaa && g < 0xaaaa && b < 0xaaaa
}
// Name returns the underlying resource name (used for caching)
func (res *ThemedResource) Name() string {
if isLight() {
return res.light.Name()
}
return res.dark.Name()
}
// Content returns the underlying content of the correct resource for the current theme
func (res *ThemedResource) Content() []byte {
if isLight() {
return res.light.Content()
}
return res.dark.Content()
}
// NewThemedResource creates a resource that adapts to the current theme setting.
func NewThemedResource(dark, light fyne.Resource) *ThemedResource {
return &ThemedResource{dark, light}
}
// FyneScene contains the full fyne logo with background design
var FyneScene = NewThemedResource(resourceFynescenedarkPng, resourceFynescenelightPng)

View File

@@ -1,203 +0,0 @@
// Package main provides various examples of Fyne API capabilities.
package main
import (
"fmt"
"log"
"net/url"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/cmd/fyne_demo/tutorials"
"fyne.io/fyne/v2/cmd/fyne_settings/settings"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
const preferenceCurrentTutorial = "currentTutorial"
var topWindow fyne.Window
func main() {
a := app.NewWithID("io.fyne.demo")
a.SetIcon(theme.FyneLogo())
logLifecycle(a)
w := a.NewWindow("Fyne Demo")
topWindow = w
w.SetMainMenu(makeMenu(a, w))
w.SetMaster()
content := container.NewMax()
title := widget.NewLabel("Component name")
intro := widget.NewLabel("An introduction would probably go\nhere, as well as a")
intro.Wrapping = fyne.TextWrapWord
setTutorial := func(t tutorials.Tutorial) {
if fyne.CurrentDevice().IsMobile() {
child := a.NewWindow(t.Title)
topWindow = child
child.SetContent(t.View(topWindow))
child.Show()
child.SetOnClosed(func() {
topWindow = w
})
return
}
title.SetText(t.Title)
intro.SetText(t.Intro)
content.Objects = []fyne.CanvasObject{t.View(w)}
content.Refresh()
}
tutorial := container.NewBorder(
container.NewVBox(title, widget.NewSeparator(), intro), nil, nil, nil, content)
if fyne.CurrentDevice().IsMobile() {
w.SetContent(makeNav(setTutorial, false))
} else {
split := container.NewHSplit(makeNav(setTutorial, true), tutorial)
split.Offset = 0.2
w.SetContent(split)
}
w.Resize(fyne.NewSize(640, 460))
w.ShowAndRun()
}
func logLifecycle(a fyne.App) {
a.Lifecycle().SetOnStarted(func() {
log.Println("Lifecycle: Started")
})
a.Lifecycle().SetOnStopped(func() {
log.Println("Lifecycle: Stopped")
})
a.Lifecycle().SetOnEnteredForeground(func() {
log.Println("Lifecycle: Entered Foreground")
})
a.Lifecycle().SetOnExitedForeground(func() {
log.Println("Lifecycle: Exited Foreground")
})
}
func makeMenu(a fyne.App, w fyne.Window) *fyne.MainMenu {
newItem := fyne.NewMenuItem("New", nil)
checkedItem := fyne.NewMenuItem("Checked", nil)
checkedItem.Checked = true
disabledItem := fyne.NewMenuItem("Disabled", nil)
disabledItem.Disabled = true
otherItem := fyne.NewMenuItem("Other", nil)
otherItem.ChildMenu = fyne.NewMenu("",
fyne.NewMenuItem("Project", func() { fmt.Println("Menu New->Other->Project") }),
fyne.NewMenuItem("Mail", func() { fmt.Println("Menu New->Other->Mail") }),
)
newItem.ChildMenu = fyne.NewMenu("",
fyne.NewMenuItem("File", func() { fmt.Println("Menu New->File") }),
fyne.NewMenuItem("Directory", func() { fmt.Println("Menu New->Directory") }),
otherItem,
)
settingsItem := fyne.NewMenuItem("Settings", func() {
w := a.NewWindow("Fyne Settings")
w.SetContent(settings.NewSettings().LoadAppearanceScreen(w))
w.Resize(fyne.NewSize(480, 480))
w.Show()
})
cutItem := fyne.NewMenuItem("Cut", func() {
shortcutFocused(&fyne.ShortcutCut{
Clipboard: w.Clipboard(),
}, w)
})
copyItem := fyne.NewMenuItem("Copy", func() {
shortcutFocused(&fyne.ShortcutCopy{
Clipboard: w.Clipboard(),
}, w)
})
pasteItem := fyne.NewMenuItem("Paste", func() {
shortcutFocused(&fyne.ShortcutPaste{
Clipboard: w.Clipboard(),
}, w)
})
findItem := fyne.NewMenuItem("Find", func() { fmt.Println("Menu Find") })
helpMenu := fyne.NewMenu("Help",
fyne.NewMenuItem("Documentation", func() {
u, _ := url.Parse("https://developer.fyne.io")
_ = a.OpenURL(u)
}),
fyne.NewMenuItem("Support", func() {
u, _ := url.Parse("https://fyne.io/support/")
_ = a.OpenURL(u)
}),
fyne.NewMenuItemSeparator(),
fyne.NewMenuItem("Sponsor", func() {
u, _ := url.Parse("https://fyne.io/sponsor/")
_ = a.OpenURL(u)
}))
// a quit item will be appended to our first (File) menu
file := fyne.NewMenu("File", newItem, checkedItem, disabledItem)
if !fyne.CurrentDevice().IsMobile() {
file.Items = append(file.Items, fyne.NewMenuItemSeparator(), settingsItem)
}
return fyne.NewMainMenu(
file,
fyne.NewMenu("Edit", cutItem, copyItem, pasteItem, fyne.NewMenuItemSeparator(), findItem),
helpMenu,
)
}
func makeNav(setTutorial func(tutorial tutorials.Tutorial), loadPrevious bool) fyne.CanvasObject {
a := fyne.CurrentApp()
tree := &widget.Tree{
ChildUIDs: func(uid string) []string {
return tutorials.TutorialIndex[uid]
},
IsBranch: func(uid string) bool {
children, ok := tutorials.TutorialIndex[uid]
return ok && len(children) > 0
},
CreateNode: func(branch bool) fyne.CanvasObject {
return widget.NewLabel("Collection Widgets")
},
UpdateNode: func(uid string, branch bool, obj fyne.CanvasObject) {
t, ok := tutorials.Tutorials[uid]
if !ok {
fyne.LogError("Missing tutorial panel: "+uid, nil)
return
}
obj.(*widget.Label).SetText(t.Title)
},
OnSelected: func(uid string) {
if t, ok := tutorials.Tutorials[uid]; ok {
a.Preferences().SetString(preferenceCurrentTutorial, uid)
setTutorial(t)
}
},
}
if loadPrevious {
currentPref := a.Preferences().StringWithFallback(preferenceCurrentTutorial, "welcome")
tree.Select(currentPref)
}
themes := fyne.NewContainerWithLayout(layout.NewGridLayout(2),
widget.NewButton("Dark", func() {
a.Settings().SetTheme(theme.DarkTheme())
}),
widget.NewButton("Light", func() {
a.Settings().SetTheme(theme.LightTheme())
}),
)
return container.NewBorder(nil, themes, nil, nil, tree)
}
func shortcutFocused(s fyne.Shortcut, w fyne.Window) {
if focused, ok := w.Canvas().Focused().(fyne.Shortcutable); ok {
focused.TypedShortcut(s)
}
}

View File

@@ -1,85 +0,0 @@
package tutorials
import (
"strconv"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/widget"
)
func scaleString(c fyne.Canvas) string {
return strconv.FormatFloat(float64(c.Scale()), 'f', 2, 32)
}
func texScaleString(c fyne.Canvas) string {
pixels, _ := c.PixelCoordinateForPosition(fyne.NewPos(1, 1))
texScale := float32(pixels) / c.Scale()
return strconv.FormatFloat(float64(texScale), 'f', 2, 32)
}
func prependTo(g *fyne.Container, s string) {
g.Objects = append([]fyne.CanvasObject{widget.NewLabel(s)}, g.Objects...)
g.Refresh()
}
func setScaleText(scale, tex *widget.Label, win fyne.Window) {
for scale.Visible() {
scale.SetText(scaleString(win.Canvas()))
tex.SetText(texScaleString(win.Canvas()))
time.Sleep(time.Second)
}
}
// advancedScreen loads a panel that shows details and settings that are a bit
// more detailed than normally needed.
func advancedScreen(win fyne.Window) fyne.CanvasObject {
scale := widget.NewLabel("")
tex := widget.NewLabel("")
screen := widget.NewCard("Screen info", "", widget.NewForm(
&widget.FormItem{Text: "Scale", Widget: scale},
&widget.FormItem{Text: "Texture Scale", Widget: tex},
))
go setScaleText(scale, tex, win)
label := widget.NewLabel("Just type...")
generic := container.NewVBox()
desk := container.NewVBox()
genericCard := widget.NewCard("", "Generic", container.NewVScroll(generic))
deskCard := widget.NewCard("", "Desktop", container.NewVScroll(desk))
win.Canvas().SetOnTypedRune(func(r rune) {
prependTo(generic, "Rune: "+string(r))
})
win.Canvas().SetOnTypedKey(func(ev *fyne.KeyEvent) {
prependTo(generic, "Key : "+string(ev.Name))
})
if deskCanvas, ok := win.Canvas().(desktop.Canvas); ok {
deskCanvas.SetOnKeyDown(func(ev *fyne.KeyEvent) {
prependTo(desk, "KeyDown: "+string(ev.Name))
})
deskCanvas.SetOnKeyUp(func(ev *fyne.KeyEvent) {
prependTo(desk, "KeyUp : "+string(ev.Name))
})
}
return container.NewHBox(
container.NewVBox(screen,
widget.NewButton("Custom Theme", func() {
fyne.CurrentApp().Settings().SetTheme(newCustomTheme())
}),
widget.NewButton("Fullscreen", func() {
win.SetFullScreen(!win.FullScreen())
}),
),
container.NewBorder(label, nil, nil, nil,
container.NewGridWithColumns(2, genericCard, deskCard),
),
)
}

View File

@@ -1,143 +0,0 @@
package tutorials
import (
"image/color"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
func makeAnimationScreen(_ fyne.Window) fyne.CanvasObject {
curves := makeAnimationCurves()
curves.Move(fyne.NewPos(0, 140+theme.Padding()))
return fyne.NewContainerWithoutLayout(makeAnimationCanvas(), curves)
}
func makeAnimationCanvas() fyne.CanvasObject {
rect := canvas.NewRectangle(color.Black)
rect.Resize(fyne.NewSize(410, 140))
a := canvas.NewColorRGBAAnimation(theme.PrimaryColorNamed(theme.ColorBlue), theme.PrimaryColorNamed(theme.ColorGreen),
time.Second*3, func(c color.Color) {
rect.FillColor = c
canvas.Refresh(rect)
})
a.RepeatCount = fyne.AnimationRepeatForever
a.AutoReverse = true
a.Start()
var a2 *fyne.Animation
i := widget.NewIcon(theme.CheckButtonCheckedIcon())
a2 = canvas.NewPositionAnimation(fyne.NewPos(0, 0), fyne.NewPos(350, 80), time.Second*3, func(p fyne.Position) {
i.Move(p)
width := 10 + (p.X / 7)
i.Resize(fyne.NewSize(width, width))
})
a2.RepeatCount = fyne.AnimationRepeatForever
a2.AutoReverse = true
a2.Curve = fyne.AnimationLinear
a2.Start()
running := true
var toggle *widget.Button
toggle = widget.NewButton("Stop", func() {
if running {
a.Stop()
a2.Stop()
toggle.SetText("Start")
} else {
a.Start()
a2.Start()
toggle.SetText("Stop")
}
running = !running
})
toggle.Resize(toggle.MinSize())
toggle.Move(fyne.NewPos(152, 54))
return fyne.NewContainerWithoutLayout(rect, i, toggle)
}
func makeAnimationCurves() fyne.CanvasObject {
label1, box1, a1 := makeAnimationCurveItem("EaseInOut", fyne.AnimationEaseInOut, 0)
label2, box2, a2 := makeAnimationCurveItem("EaseIn", fyne.AnimationEaseIn, 30+theme.Padding())
label3, box3, a3 := makeAnimationCurveItem("EaseOut", fyne.AnimationEaseOut, 60+theme.Padding()*2)
label4, box4, a4 := makeAnimationCurveItem("Linear", fyne.AnimationLinear, 90+theme.Padding()*3)
start := widget.NewButton("Compare", func() {
a1.Start()
a2.Start()
a3.Start()
a4.Start()
})
start.Resize(start.MinSize())
start.Move(fyne.NewPos(0, 120+theme.Padding()*4))
return fyne.NewContainerWithoutLayout(label1, label2, label3, label4, box1, box2, box3, box4, start)
}
func makeAnimationCurveItem(label string, curve fyne.AnimationCurve, yOff float32) (
text *widget.Label, box fyne.CanvasObject, anim *fyne.Animation) {
text = widget.NewLabel(label)
text.Alignment = fyne.TextAlignCenter
text.Resize(fyne.NewSize(380, 30))
text.Move(fyne.NewPos(0, yOff))
box = newThemedBox()
box.Resize(fyne.NewSize(30, 30))
box.Move(fyne.NewPos(0, yOff))
anim = canvas.NewPositionAnimation(
fyne.NewPos(0, yOff), fyne.NewPos(380, yOff), time.Second, func(p fyne.Position) {
box.Move(p)
box.Refresh()
})
anim.Curve = curve
anim.AutoReverse = true
anim.RepeatCount = 1
return
}
// themedBox is a simple box that change its background color according
// to the selected theme
type themedBox struct {
widget.BaseWidget
}
func newThemedBox() *themedBox {
b := &themedBox{}
b.ExtendBaseWidget(b)
return b
}
func (b *themedBox) CreateRenderer() fyne.WidgetRenderer {
b.ExtendBaseWidget(b)
bg := canvas.NewRectangle(theme.ForegroundColor())
return &themedBoxRenderer{bg: bg, objects: []fyne.CanvasObject{bg}}
}
type themedBoxRenderer struct {
bg *canvas.Rectangle
objects []fyne.CanvasObject
}
func (r *themedBoxRenderer) Destroy() {
}
func (r *themedBoxRenderer) Layout(size fyne.Size) {
r.bg.Resize(size)
}
func (r *themedBoxRenderer) MinSize() fyne.Size {
return r.bg.MinSize()
}
func (r *themedBoxRenderer) Objects() []fyne.CanvasObject {
return r.objects
}
func (r *themedBoxRenderer) Refresh() {
r.bg.FillColor = theme.ForegroundColor()
r.bg.Refresh()
}

View File

@@ -1,111 +0,0 @@
package tutorials
import (
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/widget"
)
func bindingScreen(_ fyne.Window) fyne.CanvasObject {
f := 0.2
data := binding.BindFloat(&f)
label := widget.NewLabelWithData(binding.FloatToStringWithFormat(data, "Float value: %0.2f"))
entry := widget.NewEntryWithData(binding.FloatToString(data))
floats := container.NewGridWithColumns(2, label, entry)
slide := widget.NewSliderWithData(0, 1, data)
slide.Step = 0.01
bar := widget.NewProgressBarWithData(data)
buttons := container.NewGridWithColumns(4,
widget.NewButton("0%", func() {
data.Set(0)
}),
widget.NewButton("30%", func() {
data.Set(0.3)
}),
widget.NewButton("70%", func() {
data.Set(0.7)
}),
widget.NewButton("100%", func() {
data.Set(1)
}))
boolData := binding.NewBool()
check := widget.NewCheckWithData("Check me!", boolData)
checkLabel := widget.NewLabelWithData(binding.BoolToString(boolData))
checkEntry := widget.NewEntryWithData(binding.BoolToString(boolData))
checks := container.NewGridWithColumns(3, check, checkLabel, checkEntry)
item := container.NewVBox(floats, slide, bar, buttons, widget.NewSeparator(), checks, widget.NewSeparator())
dataList := binding.BindFloatList(&[]float64{0.1, 0.2, 0.3})
button := widget.NewButton("Append", func() {
dataList.Append(float64(dataList.Length()+1) / 10)
})
list := widget.NewListWithData(dataList,
func() fyne.CanvasObject {
return container.NewBorder(nil, nil, nil, widget.NewButton("+", nil),
widget.NewLabel("item x.y"))
},
func(item binding.DataItem, obj fyne.CanvasObject) {
f := item.(binding.Float)
text := obj.(*fyne.Container).Objects[0].(*widget.Label)
text.Bind(binding.FloatToStringWithFormat(f, "item %0.1f"))
btn := obj.(*fyne.Container).Objects[1].(*widget.Button)
btn.OnTapped = func() {
val, _ := f.Get()
_ = f.Set(val + 1)
}
})
formStruct := struct {
Name, Email string
Subscribe bool
}{}
formData := binding.BindStruct(&formStruct)
form := newFormWithData(formData)
form.OnSubmit = func() {
fmt.Println("Struct:\n", formStruct)
}
listPanel := container.NewBorder(nil, button, nil, nil, list)
return container.NewBorder(item, nil, nil, nil, container.NewGridWithColumns(2, listPanel, form))
}
func newFormWithData(data binding.DataMap) *widget.Form {
keys := data.Keys()
items := make([]*widget.FormItem, len(keys))
for i, k := range keys {
data, err := data.GetItem(k)
if err != nil {
items[i] = widget.NewFormItem(k, widget.NewLabel(err.Error()))
}
items[i] = widget.NewFormItem(k, createBoundItem(data))
}
return widget.NewForm(items...)
}
func createBoundItem(v binding.DataItem) fyne.CanvasObject {
switch val := v.(type) {
case binding.Bool:
return widget.NewCheckWithData("", val)
case binding.Float:
s := widget.NewSliderWithData(0, 1, val)
s.Step = 0.01
return s
case binding.Int:
return widget.NewEntryWithData(binding.IntToString(val))
case binding.String:
return widget.NewEntryWithData(val)
default:
return widget.NewLabel("")
}
}

View File

@@ -1,49 +0,0 @@
package tutorials
import (
"image/color"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
)
func rgbGradient(x, y, w, h int) color.Color {
g := int(float32(x) / float32(w) * float32(255))
b := int(float32(y) / float32(h) * float32(255))
return color.NRGBA{uint8(255 - b), uint8(g), uint8(b), 0xff}
}
// canvasScreen loads a graphics example panel for the demo app
func canvasScreen(_ fyne.Window) fyne.CanvasObject {
gradient := canvas.NewHorizontalGradient(color.NRGBA{0x80, 0, 0, 0xff}, color.NRGBA{0, 0x80, 0, 0xff})
go func() {
for {
time.Sleep(time.Second)
gradient.Angle += 45
if gradient.Angle >= 360 {
gradient.Angle -= 360
}
canvas.Refresh(gradient)
}
}()
return fyne.NewContainerWithLayout(layout.NewGridWrapLayout(fyne.NewSize(90, 90)),
canvas.NewImageFromResource(theme.FyneLogo()),
&canvas.Rectangle{FillColor: color.NRGBA{0x80, 0, 0, 0xff},
StrokeColor: color.NRGBA{0xff, 0xff, 0xff, 0xff},
StrokeWidth: 1},
&canvas.Line{StrokeColor: color.NRGBA{0, 0, 0x80, 0xff}, StrokeWidth: 5},
&canvas.Circle{StrokeColor: color.NRGBA{0, 0, 0x80, 0xff},
FillColor: color.NRGBA{0x30, 0x30, 0x30, 0x60},
StrokeWidth: 2},
canvas.NewText("Text", color.NRGBA{0, 0x80, 0, 0xff}),
canvas.NewRasterWithPixels(rgbGradient),
gradient,
canvas.NewRadialGradient(color.NRGBA{0x80, 0, 0, 0xff}, color.NRGBA{0, 0x80, 0x80, 0xff}),
)
}

View File

@@ -1,119 +0,0 @@
package tutorials
import (
"fmt"
"strconv"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
// collectionScreen loads a tab panel for collection widgets
func collectionScreen(_ fyne.Window) fyne.CanvasObject {
content := container.NewVBox(
widget.NewLabelWithStyle("func Length() int", fyne.TextAlignLeading, fyne.TextStyle{Monospace: true}),
widget.NewLabelWithStyle("func CreateItem() fyne.CanvasObject", fyne.TextAlignLeading, fyne.TextStyle{Monospace: true}),
widget.NewLabelWithStyle("func UpdateItem(ListItemID, fyne.CanvasObject)", fyne.TextAlignLeading, fyne.TextStyle{Monospace: true}),
widget.NewLabelWithStyle("func OnSelected(ListItemID)", fyne.TextAlignLeading, fyne.TextStyle{Monospace: true}),
widget.NewLabelWithStyle("func OnUnselected(ListItemID)", fyne.TextAlignLeading, fyne.TextStyle{Monospace: true}))
return container.NewCenter(content)
}
func makeListTab(_ fyne.Window) fyne.CanvasObject {
data := make([]string, 1000)
for i := range data {
data[i] = "Test Item " + strconv.Itoa(i)
}
icon := widget.NewIcon(nil)
label := widget.NewLabel("Select An Item From The List")
hbox := container.NewHBox(icon, label)
list := widget.NewList(
func() int {
return len(data)
},
func() fyne.CanvasObject {
return container.NewHBox(widget.NewIcon(theme.DocumentIcon()), widget.NewLabel("Template Object"))
},
func(id widget.ListItemID, item fyne.CanvasObject) {
item.(*fyne.Container).Objects[1].(*widget.Label).SetText(data[id])
},
)
list.OnSelected = func(id widget.ListItemID) {
label.SetText(data[id])
icon.SetResource(theme.DocumentIcon())
}
list.OnUnselected = func(id widget.ListItemID) {
label.SetText("Select An Item From The List")
icon.SetResource(nil)
}
list.Select(125)
return container.NewHSplit(list, container.NewCenter(hbox))
}
func makeTableTab(_ fyne.Window) fyne.CanvasObject {
t := widget.NewTable(
func() (int, int) { return 500, 150 },
func() fyne.CanvasObject {
return widget.NewLabel("Cell 000, 000")
},
func(id widget.TableCellID, cell fyne.CanvasObject) {
label := cell.(*widget.Label)
switch id.Col {
case 0:
label.SetText(fmt.Sprintf("%d", id.Row+1))
case 1:
label.SetText("A longer cell")
default:
label.SetText(fmt.Sprintf("Cell %d, %d", id.Row+1, id.Col+1))
}
})
t.SetColumnWidth(0, 34)
t.SetColumnWidth(1, 102)
return t
}
func makeTreeTab(_ fyne.Window) fyne.CanvasObject {
data := map[string][]string{
"": {"A"},
"A": {"B", "D", "H", "J", "L", "O", "P", "S", "V"},
"B": {"C"},
"C": {"abc"},
"D": {"E"},
"E": {"F", "G"},
"F": {"adef"},
"G": {"adeg"},
"H": {"I"},
"I": {"ahi"},
"O": {"ao"},
"P": {"Q"},
"Q": {"R"},
"R": {"apqr"},
"S": {"T"},
"T": {"U"},
"U": {"astu"},
"V": {"W"},
"W": {"X"},
"X": {"Y"},
"Y": {"Z"},
"Z": {"avwxyz"},
}
tree := widget.NewTreeWithStrings(data)
tree.OnSelected = func(id string) {
fmt.Println("Tree node selected:", id)
}
tree.OnUnselected = func(id string) {
fmt.Println("Tree node unselected:", id)
}
tree.OpenBranch("A")
tree.OpenBranch("D")
tree.OpenBranch("E")
tree.OpenBranch("L")
tree.OpenBranch("M")
return tree
}

View File

@@ -1,154 +0,0 @@
package tutorials
import (
"fmt"
"image/color"
"strconv"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
// containerScreen loads a tab panel for containers
func containerScreen(_ fyne.Window) fyne.CanvasObject {
content := container.NewBorder(
widget.NewLabelWithStyle("Top", fyne.TextAlignCenter, fyne.TextStyle{}),
widget.NewLabelWithStyle("Bottom", fyne.TextAlignCenter, fyne.TextStyle{}),
widget.NewLabel("Left"),
widget.NewLabel("Right"),
widget.NewLabel("Border Container"))
return container.NewCenter(content)
}
func makeAppTabsTab(_ fyne.Window) fyne.CanvasObject {
tabs := container.NewAppTabs(
container.NewTabItem("Tab 1", widget.NewLabel("Content of tab 1")),
container.NewTabItem("Tab 2 bigger", widget.NewLabel("Content of tab 2")),
container.NewTabItem("Tab 3", widget.NewLabel("Content of tab 3")),
)
for i := 4; i <= 12; i++ {
tabs.Append(container.NewTabItem(fmt.Sprintf("Tab %d", i), widget.NewLabel(fmt.Sprintf("Content of tab %d", i))))
}
locations := makeTabLocationSelect(tabs.SetTabLocation)
return container.NewBorder(locations, nil, nil, nil, tabs)
}
func makeBorderLayout(_ fyne.Window) fyne.CanvasObject {
top := makeCell()
bottom := makeCell()
left := makeCell()
right := makeCell()
middle := widget.NewLabelWithStyle("BorderLayout", fyne.TextAlignCenter, fyne.TextStyle{})
return container.NewBorder(top, bottom, left, right, middle)
}
func makeBoxLayout(_ fyne.Window) fyne.CanvasObject {
top := makeCell()
bottom := makeCell()
middle := widget.NewLabel("BoxLayout")
center := makeCell()
right := makeCell()
col := container.NewVBox(top, middle, bottom)
return container.NewHBox(col, center, right)
}
func makeButtonList(count int) []fyne.CanvasObject {
var items []fyne.CanvasObject
for i := 1; i <= count; i++ {
index := i // capture
items = append(items, widget.NewButton("Button "+strconv.Itoa(index), func() {
fmt.Println("Tapped", index)
}))
}
return items
}
func makeCell() fyne.CanvasObject {
rect := canvas.NewRectangle(&color.NRGBA{128, 128, 128, 255})
rect.SetMinSize(fyne.NewSize(30, 30))
return rect
}
func makeCenterLayout(_ fyne.Window) fyne.CanvasObject {
middle := widget.NewButton("CenterLayout", func() {})
return container.NewCenter(middle)
}
func makeDocTabsTab(_ fyne.Window) fyne.CanvasObject {
tabs := container.NewDocTabs(
container.NewTabItem("Doc 1", widget.NewLabel("Content of document 1")),
container.NewTabItem("Doc 2 bigger", widget.NewLabel("Content of document 2")),
container.NewTabItem("Doc 3", widget.NewLabel("Content of document 3")),
)
i := 3
tabs.CreateTab = func() *container.TabItem {
i++
return container.NewTabItem(fmt.Sprintf("Doc %d", i), widget.NewLabel(fmt.Sprintf("Content of document %d", i)))
}
locations := makeTabLocationSelect(tabs.SetTabLocation)
return container.NewBorder(locations, nil, nil, nil, tabs)
}
func makeGridLayout(_ fyne.Window) fyne.CanvasObject {
box1 := makeCell()
box2 := widget.NewLabel("Grid")
box3 := makeCell()
box4 := makeCell()
return container.NewGridWithColumns(2,
box1, box2, box3, box4)
}
func makeScrollTab(_ fyne.Window) fyne.CanvasObject {
hlist := makeButtonList(20)
vlist := makeButtonList(50)
horiz := container.NewHScroll(container.NewHBox(hlist...))
vert := container.NewVScroll(container.NewVBox(vlist...))
return container.NewAdaptiveGrid(2,
container.NewBorder(horiz, nil, nil, nil, vert),
makeScrollBothTab())
}
func makeScrollBothTab() fyne.CanvasObject {
logo := canvas.NewImageFromResource(theme.FyneLogo())
logo.SetMinSize(fyne.NewSize(800, 800))
scroll := container.NewScroll(logo)
scroll.Resize(fyne.NewSize(400, 400))
return scroll
}
func makeSplitTab(_ fyne.Window) fyne.CanvasObject {
left := widget.NewMultiLineEntry()
left.Wrapping = fyne.TextWrapWord
left.SetText("Long text is looooooooooooooong")
right := container.NewVSplit(
widget.NewLabel("Label"),
widget.NewButton("Button", func() { fmt.Println("button tapped!") }),
)
return container.NewHSplit(container.NewVScroll(left), right)
}
func makeTabLocationSelect(callback func(container.TabLocation)) *widget.Select {
locations := widget.NewSelect([]string{"Top", "Bottom", "Leading", "Trailing"}, func(s string) {
callback(map[string]container.TabLocation{
"Top": container.TabLocationTop,
"Bottom": container.TabLocationBottom,
"Leading": container.TabLocationLeading,
"Trailing": container.TabLocationTrailing,
}[s])
})
locations.SetSelected("Top")
return locations
}

View File

@@ -1,150 +0,0 @@
package tutorials
import (
"fyne.io/fyne/v2"
)
// Tutorial defines the data structure for a tutorial
type Tutorial struct {
Title, Intro string
View func(w fyne.Window) fyne.CanvasObject
}
var (
// Tutorials defines the metadata for each tutorial
Tutorials = map[string]Tutorial{
"welcome": {"Welcome", "", welcomeScreen},
"canvas": {"Canvas",
"See the canvas capabilities.",
canvasScreen,
},
"animations": {"Animations",
"See how to animate components.",
makeAnimationScreen,
},
"icons": {"Theme Icons",
"Browse the embedded icons.",
iconScreen,
},
"containers": {"Containers",
"Containers group other widgets and canvas objects, organising according to their layout.\n" +
"Standard containers are illustrated in this section, but developers can also provide custom " +
"layouts using the fyne.NewContainerWithLayout() constructor.",
containerScreen,
},
"apptabs": {"AppTabs",
"A container to help divide up an application into functional areas.",
makeAppTabsTab,
},
"border": {"Border",
"A container that positions items around a central content.",
makeBorderLayout,
},
"box": {"Box",
"A container arranges items in horizontal or vertical list.",
makeBoxLayout,
},
"center": {"Center",
"A container to that centers child elements.",
makeCenterLayout,
},
"doctabs": {"DocTabs",
"A container to display a single document from a set of many.",
makeDocTabsTab,
},
"grid": {"Grid",
"A container that arranges all items in a grid.",
makeGridLayout,
},
"split": {"Split",
"A split container divides the container in two pieces that the user can resize.",
makeSplitTab,
},
"scroll": {"Scroll",
"A container that provides scrolling for it's content.",
makeScrollTab,
},
"widgets": {"Widgets",
"In this section you can see the features available in the toolkit widget set.\n" +
"Expand the tree on the left to browse the individual tutorial elements.",
widgetScreen,
},
"accordion": {"Accordion",
"Expand or collapse content panels.",
makeAccordionTab,
},
"button": {"Button",
"Simple widget for user tap handling.",
makeButtonTab,
},
"card": {"Card",
"Group content and widgets.",
makeCardTab,
},
"entry": {"Entry",
"Different ways to use the entry widget.",
makeEntryTab,
},
"form": {"Form",
"Gathering input widgets for data submission.",
makeFormTab,
},
"input": {"Input",
"A collection of widgets for user input.",
makeInputTab,
},
"text": {"Text",
"Text handling widgets.",
makeTextTab,
},
"toolbar": {"Toolbar",
"A row of shortcut icons for common tasks.",
makeToolbarTab,
},
"progress": {"Progress",
"Show duration or the need to wait for a task.",
makeProgressTab,
},
"collections": {"Collections",
"Collection widgets provide an efficient way to present lots of content.\n" +
"The List, Table, and Tree provide a cache and re-use mechanism that make it possible to scroll through thousands of elements.\n" +
"Use this for large data sets or for collections that can expand as users scroll.",
collectionScreen,
},
"list": {"List",
"A vertical arrangement of cached elements with the same styling.",
makeListTab,
},
"table": {"Table",
"A two dimensional cached collection of cells.",
makeTableTab,
},
"tree": {"Tree",
"A tree based arrangement of cached elements with the same styling.",
makeTreeTab,
},
"dialogs": {"Dialogs",
"Work with dialogs.",
dialogScreen,
},
"windows": {"Windows",
"Window function demo.",
windowScreen,
},
"binding": {"Data Binding",
"Connecting widgets to a data source.",
bindingScreen},
"advanced": {"Advanced",
"Debug and advanced information.",
advancedScreen,
},
}
// TutorialIndex defines how our tutorials should be laid out in the index tree
TutorialIndex = map[string][]string{
"": {"welcome", "canvas", "animations", "icons", "widgets", "collections", "containers", "dialogs", "windows", "binding", "advanced"},
"collections": {"list", "table", "tree"},
"containers": {"apptabs", "border", "box", "center", "doctabs", "grid", "scroll", "split"},
"widgets": {"accordion", "button", "card", "entry", "form", "input", "progress", "text", "toolbar"},
}
)

View File

@@ -1,185 +0,0 @@
package tutorials
import (
"errors"
"fmt"
"image/color"
"io/ioutil"
"log"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/data/validation"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/storage"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
func confirmCallback(response bool) {
fmt.Println("Responded with", response)
}
func colorPicked(c color.Color, w fyne.Window) {
log.Println("Color picked:", c)
rectangle := canvas.NewRectangle(c)
size := 2 * theme.IconInlineSize()
rectangle.SetMinSize(fyne.NewSize(size, size))
dialog.ShowCustom("Color Picked", "Ok", rectangle, w)
}
// dialogScreen loads demos of the dialogs we support
func dialogScreen(win fyne.Window) fyne.CanvasObject {
return container.NewVScroll(container.NewVBox(
widget.NewButton("Info", func() {
dialog.ShowInformation("Information", "You should know this thing...", win)
}),
widget.NewButton("Error", func() {
err := errors.New("a dummy error message")
dialog.ShowError(err, win)
}),
widget.NewButton("Confirm", func() {
cnf := dialog.NewConfirm("Confirmation", "Are you enjoying this demo?", confirmCallback, win)
cnf.SetDismissText("Nah")
cnf.SetConfirmText("Oh Yes!")
cnf.Show()
}),
widget.NewButton("File Open With Filter (.jpg or .png)", func() {
fd := dialog.NewFileOpen(func(reader fyne.URIReadCloser, err error) {
if err != nil {
dialog.ShowError(err, win)
return
}
if reader == nil {
log.Println("Cancelled")
return
}
imageOpened(reader)
}, win)
fd.SetFilter(storage.NewExtensionFileFilter([]string{".png", ".jpg", ".jpeg"}))
fd.Show()
}),
widget.NewButton("File Save", func() {
dialog.ShowFileSave(func(writer fyne.URIWriteCloser, err error) {
if err != nil {
dialog.ShowError(err, win)
return
}
if writer == nil {
log.Println("Cancelled")
return
}
fileSaved(writer, win)
}, win)
}),
widget.NewButton("Folder Open", func() {
dialog.ShowFolderOpen(func(list fyne.ListableURI, err error) {
if err != nil {
dialog.ShowError(err, win)
return
}
if list == nil {
log.Println("Cancelled")
return
}
children, err := list.List()
if err != nil {
dialog.ShowError(err, win)
return
}
out := fmt.Sprintf("Folder %s (%d children):\n%s", list.Name(), len(children), list.String())
dialog.ShowInformation("Folder Open", out, win)
}, win)
}),
widget.NewButton("Color Picker", func() {
picker := dialog.NewColorPicker("Pick a Color", "What is your favorite color?", func(c color.Color) {
colorPicked(c, win)
}, win)
picker.Show()
}),
widget.NewButton("Advanced Color Picker", func() {
picker := dialog.NewColorPicker("Pick a Color", "What is your favorite color?", func(c color.Color) {
colorPicked(c, win)
}, win)
picker.Advanced = true
picker.Show()
}),
widget.NewButton("Form Dialog (Login Form)", func() {
username := widget.NewEntry()
username.Validator = validation.NewRegexp(`^[A-Za-z0-9_-]+$`, "username can only contain letters, numbers, '_', and '-'")
password := widget.NewPasswordEntry()
password.Validator = validation.NewRegexp(`^[A-Za-z0-9_-]+$`, "password can only contain letters, numbers, '_', and '-'")
remember := false
items := []*widget.FormItem{
widget.NewFormItem("Username", username),
widget.NewFormItem("Password", password),
widget.NewFormItem("Remember me", widget.NewCheck("", func(checked bool) {
remember = checked
})),
}
dialog.ShowForm("Login...", "Log In", "Cancel", items, func(b bool) {
if !b {
return
}
var rememberText string
if remember {
rememberText = "and remember this login"
}
log.Println("Please Authenticate", username.Text, password.Text, rememberText)
}, win)
}),
))
}
func imageOpened(f fyne.URIReadCloser) {
if f == nil {
log.Println("Cancelled")
return
}
defer f.Close()
showImage(f)
}
func fileSaved(f fyne.URIWriteCloser, w fyne.Window) {
defer f.Close()
_, err := f.Write([]byte("Written by Fyne demo\n"))
if err != nil {
dialog.ShowError(err, w)
}
err = f.Close()
if err != nil {
dialog.ShowError(err, w)
}
log.Println("Saved to...", f.URI())
}
func loadImage(f fyne.URIReadCloser) *canvas.Image {
data, err := ioutil.ReadAll(f)
if err != nil {
fyne.LogError("Failed to load image data", err)
return nil
}
res := fyne.NewStaticResource(f.URI().Name(), data)
return canvas.NewImageFromResource(res)
}
func showImage(f fyne.URIReadCloser) {
img := loadImage(f)
if img == nil {
return
}
img.FillMode = canvas.ImageFillOriginal
w := fyne.CurrentApp().NewWindow(f.URI().Name())
w.SetContent(container.NewScroll(img))
w.Resize(fyne.NewSize(320, 240))
w.Show()
}

View File

@@ -1,195 +0,0 @@
package tutorials
import (
"image/color"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
type iconInfo struct {
name string
icon fyne.Resource
}
type browser struct {
current int
icons []iconInfo
name *widget.Select
icon *widget.Icon
}
func (b *browser) setIcon(index int) {
if index < 0 || index > len(b.icons)-1 {
return
}
b.current = index
b.name.SetSelected(b.icons[index].name)
b.icon.SetResource(b.icons[index].icon)
}
// iconScreen loads a panel that shows the various icons available in Fyne
func iconScreen(_ fyne.Window) fyne.CanvasObject {
b := &browser{}
b.icons = loadIcons()
prev := widget.NewButtonWithIcon("", theme.NavigateBackIcon(), func() {
b.setIcon(b.current - 1)
})
next := widget.NewButtonWithIcon("", theme.NavigateNextIcon(), func() {
b.setIcon(b.current + 1)
})
b.name = widget.NewSelect(iconList(b.icons), func(name string) {
for i, icon := range b.icons {
if icon.name == name {
if b.current != i {
b.setIcon(i)
}
break
}
}
})
b.name.SetSelected(b.icons[b.current].name)
buttons := container.NewHBox(prev, next)
bar := container.NewBorder(nil, nil, buttons, nil, b.name)
background := canvas.NewRasterWithPixels(checkerPattern)
background.SetMinSize(fyne.NewSize(280, 280))
b.icon = widget.NewIcon(b.icons[b.current].icon)
return fyne.NewContainerWithLayout(layout.NewBorderLayout(
bar, nil, nil, nil), bar, background, b.icon)
}
func checkerPattern(x, y, _, _ int) color.Color {
x /= 20
y /= 20
if x%2 == y%2 {
return theme.BackgroundColor()
}
return theme.ShadowColor()
}
func iconList(icons []iconInfo) []string {
ret := make([]string, len(icons))
for i, icon := range icons {
ret[i] = icon.name
}
return ret
}
func loadIcons() []iconInfo {
return []iconInfo{
{"CancelIcon", theme.CancelIcon()},
{"ConfirmIcon", theme.ConfirmIcon()},
{"DeleteIcon", theme.DeleteIcon()},
{"SearchIcon", theme.SearchIcon()},
{"SearchReplaceIcon", theme.SearchReplaceIcon()},
{"CheckButtonIcon", theme.CheckButtonIcon()},
{"CheckButtonCheckedIcon", theme.CheckButtonCheckedIcon()},
{"RadioButtonIcon", theme.RadioButtonIcon()},
{"RadioButtonCheckedIcon", theme.RadioButtonCheckedIcon()},
{"ColorAchromaticIcon", theme.ColorAchromaticIcon()},
{"ColorChromaticIcon", theme.ColorChromaticIcon()},
{"ColorPaletteIcon", theme.ColorPaletteIcon()},
{"ContentAddIcon", theme.ContentAddIcon()},
{"ContentRemoveIcon", theme.ContentRemoveIcon()},
{"ContentClearIcon", theme.ContentClearIcon()},
{"ContentCutIcon", theme.ContentCutIcon()},
{"ContentCopyIcon", theme.ContentCopyIcon()},
{"ContentPasteIcon", theme.ContentPasteIcon()},
{"ContentRedoIcon", theme.ContentRedoIcon()},
{"ContentUndoIcon", theme.ContentUndoIcon()},
{"InfoIcon", theme.InfoIcon()},
{"ErrorIcon", theme.ErrorIcon()},
{"QuestionIcon", theme.QuestionIcon()},
{"WarningIcon", theme.WarningIcon()},
{"DocumentIcon", theme.DocumentIcon()},
{"DocumentCreateIcon", theme.DocumentCreateIcon()},
{"DocumentPrintIcon", theme.DocumentPrintIcon()},
{"DocumentSaveIcon", theme.DocumentSaveIcon()},
{"FileIcon", theme.FileIcon()},
{"FileApplicationIcon", theme.FileApplicationIcon()},
{"FileAudioIcon", theme.FileAudioIcon()},
{"FileImageIcon", theme.FileImageIcon()},
{"FileTextIcon", theme.FileTextIcon()},
{"FileVideoIcon", theme.FileVideoIcon()},
{"FolderIcon", theme.FolderIcon()},
{"FolderNewIcon", theme.FolderNewIcon()},
{"FolderOpenIcon", theme.FolderOpenIcon()},
{"ComputerIcon", theme.ComputerIcon()},
{"HomeIcon", theme.HomeIcon()},
{"HelpIcon", theme.HelpIcon()},
{"HistoryIcon", theme.HistoryIcon()},
{"SettingsIcon", theme.SettingsIcon()},
{"StorageIcon", theme.StorageIcon()},
{"DownloadIcon", theme.DownloadIcon()},
{"UploadIcon", theme.UploadIcon()},
{"ViewFullScreenIcon", theme.ViewFullScreenIcon()},
{"ViewRestoreIcon", theme.ViewRestoreIcon()},
{"ViewRefreshIcon", theme.ViewRefreshIcon()},
{"VisibilityIcon", theme.VisibilityIcon()},
{"VisibilityOffIcon", theme.VisibilityOffIcon()},
{"ZoomFitIcon", theme.ZoomFitIcon()},
{"ZoomInIcon", theme.ZoomInIcon()},
{"ZoomOutIcon", theme.ZoomOutIcon()},
{"MoreHorizontalIcon", theme.MoreHorizontalIcon()},
{"MoreVerticalIcon", theme.MoreVerticalIcon()},
{"MoveDownIcon", theme.MoveDownIcon()},
{"MoveUpIcon", theme.MoveUpIcon()},
{"NavigateBackIcon", theme.NavigateBackIcon()},
{"NavigateNextIcon", theme.NavigateNextIcon()},
{"Menu", theme.MenuIcon()},
{"MenuExpand", theme.MenuExpandIcon()},
{"MenuDropDown", theme.MenuDropDownIcon()},
{"MenuDropUp", theme.MenuDropUpIcon()},
{"MailAttachmentIcon", theme.MailAttachmentIcon()},
{"MailComposeIcon", theme.MailComposeIcon()},
{"MailForwardIcon", theme.MailForwardIcon()},
{"MailReplyIcon", theme.MailReplyIcon()},
{"MailReplyAllIcon", theme.MailReplyAllIcon()},
{"MailSendIcon", theme.MailSendIcon()},
{"MediaFastForward", theme.MediaFastForwardIcon()},
{"MediaFastRewind", theme.MediaFastRewindIcon()},
{"MediaPause", theme.MediaPauseIcon()},
{"MediaPlay", theme.MediaPlayIcon()},
{"MediaStop", theme.MediaStopIcon()},
{"MediaRecord", theme.MediaRecordIcon()},
{"MediaReplay", theme.MediaReplayIcon()},
{"MediaSkipNext", theme.MediaSkipNextIcon()},
{"MediaSkipPrevious", theme.MediaSkipPreviousIcon()},
{"VolumeDown", theme.VolumeDownIcon()},
{"VolumeMute", theme.VolumeMuteIcon()},
{"VolumeUp", theme.VolumeUpIcon()},
{"AccountIcon", theme.AccountIcon()},
{"LoginIcon", theme.LoginIcon()},
{"LogoutIcon", theme.LogoutIcon()},
{"ListIcon", theme.ListIcon()},
{"GridIcon", theme.GridIcon()},
}
}

View File

@@ -1,72 +0,0 @@
package tutorials
import (
"image/color"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/theme"
)
var (
purple = &color.NRGBA{R: 128, G: 0, B: 128, A: 255}
orange = &color.NRGBA{R: 198, G: 123, B: 0, A: 255}
grey = &color.Gray{Y: 123}
)
// customTheme is a simple demonstration of a bespoke theme loaded by a Fyne app.
type customTheme struct {
}
func (customTheme) Color(c fyne.ThemeColorName, _ fyne.ThemeVariant) color.Color {
switch c {
case theme.ColorNameBackground:
return purple
case theme.ColorNameButton, theme.ColorNameDisabled:
return color.Black
case theme.ColorNamePlaceHolder, theme.ColorNameScrollBar:
return grey
case theme.ColorNamePrimary, theme.ColorNameHover, theme.ColorNameFocus:
return orange
case theme.ColorNameShadow:
return &color.RGBA{R: 0xcc, G: 0xcc, B: 0xcc, A: 0xcc}
default:
return color.White
}
}
func (customTheme) Font(style fyne.TextStyle) fyne.Resource {
return theme.DarkTheme().Font(style)
}
func (customTheme) Icon(n fyne.ThemeIconName) fyne.Resource {
return theme.DefaultTheme().Icon(n)
}
func (customTheme) Size(s fyne.ThemeSizeName) float32 {
switch s {
case theme.SizeNamePadding:
return 8
case theme.SizeNameInlineIcon:
return 20
case theme.SizeNameScrollBar:
return 10
case theme.SizeNameScrollBarSmall:
return 5
case theme.SizeNameText:
return 18
case theme.SizeNameHeadingText:
return 30
case theme.SizeNameSubHeadingText:
return 25
case theme.SizeNameCaptionText:
return 15
case theme.SizeNameInputBorder:
return 1
default:
return 0
}
}
func newCustomTheme() fyne.Theme {
return &customTheme{}
}

View File

@@ -1,42 +0,0 @@
package tutorials
import (
"net/url"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/cmd/fyne_demo/data"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
func parseURL(urlStr string) *url.URL {
link, err := url.Parse(urlStr)
if err != nil {
fyne.LogError("Could not parse URL", err)
}
return link
}
func welcomeScreen(_ fyne.Window) fyne.CanvasObject {
logo := canvas.NewImageFromResource(data.FyneScene)
logo.FillMode = canvas.ImageFillContain
if fyne.CurrentDevice().IsMobile() {
logo.SetMinSize(fyne.NewSize(171, 125))
} else {
logo.SetMinSize(fyne.NewSize(228, 167))
}
return container.NewCenter(container.NewVBox(
widget.NewLabelWithStyle("Welcome to the Fyne toolkit demo app", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
logo,
container.NewHBox(
widget.NewHyperlink("fyne.io", parseURL("https://fyne.io/")),
widget.NewLabel("-"),
widget.NewHyperlink("documentation", parseURL("https://developer.fyne.io/")),
widget.NewLabel("-"),
widget.NewHyperlink("sponsor", parseURL("https://fyne.io/sponsor/")),
),
))
}

View File

@@ -1,421 +0,0 @@
package tutorials
import (
"fmt"
"image/color"
"net/url"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/data/validation"
"fyne.io/fyne/v2/driver/mobile"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
const (
loremIpsum = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque quis consectetur nisi. Suspendisse id interdum felis.
Sed egestas eget tellus eu pharetra. Praesent pulvinar sed massa id placerat. Etiam sem libero, semper vitae consequat ut, volutpat id mi.
Mauris volutpat pellentesque convallis. Curabitur rutrum venenatis orci nec ornare. Maecenas quis pellentesque neque.
Aliquam consectetur dapibus nulla, id maximus odio ultrices ac. Sed luctus at felis sed faucibus. Cras leo augue, congue in velit ut, mattis rhoncus lectus.
Praesent viverra, mauris ut ullamcorper semper, leo urna auctor lectus, vitae vehicula mi leo quis lorem.
Nullam condimentum, massa at tempor feugiat, metus enim lobortis velit, eget suscipit eros ipsum quis tellus. Aenean fermentum diam vel felis dictum semper.
Duis nisl orci, tincidunt ut leo quis, luctus vehicula diam. Sed velit justo, congue id augue eu, euismod dapibus lacus. Proin sit amet imperdiet sapien.
Mauris erat urna, fermentum et quam rhoncus, fringilla consequat ante. Vivamus consectetur molestie odio, ac rutrum erat finibus a.
Suspendisse id maximus felis. Sed mauris odio, mattis eget mi eu, consequat tempus purus.`
)
var (
progress *widget.ProgressBar
fprogress *widget.ProgressBar
infProgress *widget.ProgressBarInfinite
endProgress chan interface{}
)
func makeAccordionTab(_ fyne.Window) fyne.CanvasObject {
link, err := url.Parse("https://fyne.io/")
if err != nil {
fyne.LogError("Could not parse URL", err)
}
ac := widget.NewAccordion(
widget.NewAccordionItem("A", widget.NewHyperlink("One", link)),
widget.NewAccordionItem("B", widget.NewLabel("Two")),
&widget.AccordionItem{
Title: "C",
Detail: widget.NewLabel("Three"),
},
)
ac.Append(widget.NewAccordionItem("D", &widget.Entry{Text: "Four"}))
return ac
}
func makeButtonTab(_ fyne.Window) fyne.CanvasObject {
disabled := widget.NewButton("Disabled", func() {})
disabled.Disable()
shareItem := fyne.NewMenuItem("Share via", nil)
shareItem.ChildMenu = fyne.NewMenu("",
fyne.NewMenuItem("Twitter", func() { fmt.Println("context menu Share->Twitter") }),
fyne.NewMenuItem("Reddit", func() { fmt.Println("context menu Share->Reddit") }),
)
menuLabel := newContextMenuButton("tap me for pop-up menu with submenus", fyne.NewMenu("",
fyne.NewMenuItem("Copy", func() { fmt.Println("context menu copy") }),
shareItem,
))
return container.NewVBox(
widget.NewButton("Button (text only)", func() { fmt.Println("tapped text button") }),
widget.NewButtonWithIcon("Button (text & leading icon)", theme.ConfirmIcon(), func() { fmt.Println("tapped text & leading icon button") }),
&widget.Button{
Alignment: widget.ButtonAlignLeading,
Text: "Button (leading-aligned, text only)",
OnTapped: func() { fmt.Println("tapped leading-aligned, text only button") },
},
&widget.Button{
Alignment: widget.ButtonAlignTrailing,
IconPlacement: widget.ButtonIconTrailingText,
Text: "Button (trailing-aligned, text & trailing icon)",
Icon: theme.ConfirmIcon(),
OnTapped: func() { fmt.Println("tapped trailing-aligned, text & trailing icon button") },
},
disabled,
layout.NewSpacer(),
layout.NewSpacer(),
menuLabel,
layout.NewSpacer(),
)
}
func makeCardTab(_ fyne.Window) fyne.CanvasObject {
card1 := widget.NewCard("Book a table", "Which time suits?",
widget.NewRadioGroup([]string{"6:30pm", "7:00pm", "7:45pm"}, func(string) {}))
card2 := widget.NewCard("With media", "No content, with image", nil)
card2.Image = canvas.NewImageFromResource(theme.FyneLogo())
card3 := widget.NewCard("Title 3", "Another card", widget.NewLabel("Content"))
return container.NewGridWithColumns(2, container.NewVBox(card1, card3),
container.NewVBox(card2))
}
func makeEntryTab(_ fyne.Window) fyne.CanvasObject {
entry := widget.NewEntry()
entry.SetPlaceHolder("Entry")
entryDisabled := widget.NewEntry()
entryDisabled.SetText("Entry (disabled)")
entryDisabled.Disable()
entryValidated := newNumEntry()
entryValidated.SetPlaceHolder("Must contain a number")
entryMultiLine := widget.NewMultiLineEntry()
entryMultiLine.SetPlaceHolder("MultiLine Entry")
return container.NewVBox(
entry,
entryDisabled,
entryValidated,
entryMultiLine)
}
func makeTextGrid() *widget.TextGrid {
grid := widget.NewTextGridFromString("TextGrid\n\tContent\nZebra")
grid.SetStyleRange(0, 4, 0, 7,
&widget.CustomTextGridStyle{BGColor: &color.NRGBA{R: 64, G: 64, B: 192, A: 128}})
grid.SetRowStyle(1, &widget.CustomTextGridStyle{BGColor: &color.NRGBA{R: 64, G: 192, B: 64, A: 128}})
white := &widget.CustomTextGridStyle{FGColor: color.White, BGColor: color.Black}
black := &widget.CustomTextGridStyle{FGColor: color.Black, BGColor: color.White}
grid.Rows[2].Cells[0].Style = white
grid.Rows[2].Cells[1].Style = black
grid.Rows[2].Cells[2].Style = white
grid.Rows[2].Cells[3].Style = black
grid.Rows[2].Cells[4].Style = white
grid.ShowLineNumbers = true
grid.ShowWhitespace = true
return grid
}
func makeTextTab(_ fyne.Window) fyne.CanvasObject {
label := widget.NewLabel("Label")
link, err := url.Parse("https://fyne.io/")
if err != nil {
fyne.LogError("Could not parse URL", err)
}
hyperlink := widget.NewHyperlink("Hyperlink", link)
entryLoremIpsum := widget.NewMultiLineEntry()
entryLoremIpsum.SetText(loremIpsum)
label.Alignment = fyne.TextAlignLeading
hyperlink.Alignment = fyne.TextAlignLeading
label.Wrapping = fyne.TextWrapWord
hyperlink.Wrapping = fyne.TextWrapWord
entryLoremIpsum.Wrapping = fyne.TextWrapWord
rich := widget.NewRichTextFromMarkdown(`
# RichText Heading
## A Sub Heading
---
* Item1 in _three_ segments
* Item2
* Item3
Normal **Bold** *Italic* [Link](https://fyne.io/) and some ` + "`Code`" + `.
This styled row should also wrap as expected, but only *when required*.
> An interesting quote here, most likely sharing some very interesting wisdom.`)
rich.Scroll = container.ScrollBoth
radioAlign := widget.NewRadioGroup([]string{"Text Alignment Leading", "Text Alignment Center", "Text Alignment Trailing"}, func(s string) {
var align fyne.TextAlign
switch s {
case "Text Alignment Leading":
align = fyne.TextAlignLeading
case "Text Alignment Center":
align = fyne.TextAlignCenter
case "Text Alignment Trailing":
align = fyne.TextAlignTrailing
}
label.Alignment = align
hyperlink.Alignment = align
for i := range rich.Segments {
if seg, ok := rich.Segments[i].(*widget.TextSegment); ok {
seg.Style.Alignment = align
}
if seg, ok := rich.Segments[i].(*widget.HyperlinkSegment); ok {
seg.Alignment = align
}
}
label.Refresh()
hyperlink.Refresh()
rich.Refresh()
})
radioAlign.SetSelected("Text Alignment Leading")
radioWrap := widget.NewRadioGroup([]string{"Text Wrapping Off", "Text Wrapping Truncate", "Text Wrapping Break", "Text Wrapping Word"}, func(s string) {
var wrap fyne.TextWrap
switch s {
case "Text Wrapping Off":
wrap = fyne.TextWrapOff
case "Text Wrapping Truncate":
wrap = fyne.TextTruncate
case "Text Wrapping Break":
wrap = fyne.TextWrapBreak
case "Text Wrapping Word":
wrap = fyne.TextWrapWord
}
label.Wrapping = wrap
hyperlink.Wrapping = wrap
entryLoremIpsum.Wrapping = wrap
rich.Wrapping = wrap
label.Refresh()
hyperlink.Refresh()
entryLoremIpsum.Refresh()
rich.Refresh()
})
radioWrap.SetSelected("Text Wrapping Word")
fixed := container.NewVBox(
container.NewHBox(
radioAlign,
layout.NewSpacer(),
radioWrap,
),
label,
hyperlink,
)
grid := makeTextGrid()
return container.NewBorder(fixed, grid, nil, nil,
container.NewGridWithRows(2, rich, entryLoremIpsum))
}
func makeInputTab(_ fyne.Window) fyne.CanvasObject {
selectEntry := widget.NewSelectEntry([]string{"Option A", "Option B", "Option C"})
selectEntry.PlaceHolder = "Type or select"
disabledCheck := widget.NewCheck("Disabled check", func(bool) {})
disabledCheck.Disable()
checkGroup := widget.NewCheckGroup([]string{"CheckGroup Item 1", "CheckGroup Item 2AAAAAAAAAAAAAA", "CheckGroup Item 3"}, func(s []string) { fmt.Println("selected", s) })
checkGroup.Horizontal = true
radio := widget.NewRadioGroup([]string{"Radio Item 1", "Radio Item 2"}, func(s string) { fmt.Println("selected", s) })
radio.Horizontal = true
disabledRadio := widget.NewRadioGroup([]string{"Disabled radio"}, func(string) {})
disabledRadio.Disable()
return container.NewVBox(
widget.NewSelect([]string{"Option 1", "Option 2", "Option 3"}, func(s string) { fmt.Println("selected", s) }),
selectEntry,
widget.NewCheck("Check", func(on bool) { fmt.Println("checked", on) }),
disabledCheck,
checkGroup,
radio,
disabledRadio,
widget.NewSlider(0, 100),
)
}
func makeProgressTab(_ fyne.Window) fyne.CanvasObject {
stopProgress()
progress = widget.NewProgressBar()
fprogress = widget.NewProgressBar()
fprogress.TextFormatter = func() string {
return fmt.Sprintf("%.2f out of %.2f", fprogress.Value, fprogress.Max)
}
infProgress = widget.NewProgressBarInfinite()
endProgress = make(chan interface{}, 1)
startProgress()
return container.NewVBox(
widget.NewLabel("Percent"), progress,
widget.NewLabel("Formatted"), fprogress,
widget.NewLabel("Infinite"), infProgress)
}
func makeFormTab(_ fyne.Window) fyne.CanvasObject {
name := widget.NewEntry()
name.SetPlaceHolder("John Smith")
email := widget.NewEntry()
email.SetPlaceHolder("test@example.com")
email.Validator = validation.NewRegexp(`\w{1,}@\w{1,}\.\w{1,4}`, "not a valid email")
password := widget.NewPasswordEntry()
password.SetPlaceHolder("Password")
disabled := widget.NewRadioGroup([]string{"Option 1", "Option 2"}, func(string) {})
disabled.Horizontal = true
disabled.Disable()
largeText := widget.NewMultiLineEntry()
form := &widget.Form{
Items: []*widget.FormItem{
{Text: "Name", Widget: name, HintText: "Your full name"},
{Text: "Email", Widget: email, HintText: "A valid email address"},
},
OnCancel: func() {
fmt.Println("Cancelled")
},
OnSubmit: func() {
fmt.Println("Form submitted")
fyne.CurrentApp().SendNotification(&fyne.Notification{
Title: "Form for: " + name.Text,
Content: largeText.Text,
})
},
}
form.Append("Password", password)
form.Append("Disabled", disabled)
form.Append("Message", largeText)
return form
}
func makeToolbarTab(_ fyne.Window) fyne.CanvasObject {
t := widget.NewToolbar(widget.NewToolbarAction(theme.MailComposeIcon(), func() { fmt.Println("New") }),
widget.NewToolbarSeparator(),
widget.NewToolbarSpacer(),
widget.NewToolbarAction(theme.ContentCutIcon(), func() { fmt.Println("Cut") }),
widget.NewToolbarAction(theme.ContentCopyIcon(), func() { fmt.Println("Copy") }),
widget.NewToolbarAction(theme.ContentPasteIcon(), func() { fmt.Println("Paste") }),
)
return container.NewBorder(t, nil, nil, nil)
}
func startProgress() {
progress.SetValue(0)
fprogress.SetValue(0)
select { // ignore stale end message
case <-endProgress:
default:
}
go func() {
end := endProgress
num := 0.0
for num < 1.0 {
time.Sleep(16 * time.Millisecond)
select {
case <-end:
return
default:
}
progress.SetValue(num)
fprogress.SetValue(num)
num += 0.002
}
progress.SetValue(1)
fprogress.SetValue(1)
// TODO make sure this resets when we hide etc...
stopProgress()
}()
infProgress.Start()
}
func stopProgress() {
if !infProgress.Running() {
return
}
infProgress.Stop()
endProgress <- struct{}{}
}
// widgetScreen shows a panel containing widget demos
func widgetScreen(_ fyne.Window) fyne.CanvasObject {
content := container.NewVBox(
widget.NewLabel("Labels"),
widget.NewButtonWithIcon("Icons", theme.HomeIcon(), func() {}),
widget.NewSlider(0, 1))
return container.NewCenter(content)
}
type contextMenuButton struct {
widget.Button
menu *fyne.Menu
}
func (b *contextMenuButton) Tapped(e *fyne.PointEvent) {
widget.ShowPopUpMenuAtPosition(b.menu, fyne.CurrentApp().Driver().CanvasForObject(b), e.AbsolutePosition)
}
func newContextMenuButton(label string, menu *fyne.Menu) *contextMenuButton {
b := &contextMenuButton{menu: menu}
b.Text = label
b.ExtendBaseWidget(b)
return b
}
type numEntry struct {
widget.Entry
}
func (n *numEntry) Keyboard() mobile.KeyboardType {
return mobile.NumberKeyboard
}
func newNumEntry() *numEntry {
e := &numEntry{}
e.ExtendBaseWidget(e)
e.Validator = validation.NewRegexp(`\d`, "Must contain a number")
return e
}

View File

@@ -1,71 +0,0 @@
package tutorials
import (
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/widget"
)
func windowScreen(_ fyne.Window) fyne.CanvasObject {
windowGroup := container.NewVBox(
widget.NewButton("New window", func() {
w := fyne.CurrentApp().NewWindow("Hello")
w.SetContent(widget.NewLabel("Hello World!"))
w.Show()
}),
widget.NewButton("Fixed size window", func() {
w := fyne.CurrentApp().NewWindow("Fixed")
w.SetContent(fyne.NewContainerWithLayout(layout.NewCenterLayout(), widget.NewLabel("Hello World!")))
w.Resize(fyne.NewSize(240, 180))
w.SetFixedSize(true)
w.Show()
}),
widget.NewButton("Toggle between fixed/not fixed window size", func() {
w := fyne.CurrentApp().NewWindow("Toggle fixed size")
w.SetContent(fyne.NewContainerWithLayout(layout.NewCenterLayout(), widget.NewCheck("Fixed size", func(toggle bool) {
if toggle {
w.Resize(fyne.NewSize(240, 180))
}
w.SetFixedSize(toggle)
})))
w.Show()
}),
widget.NewButton("Centered window", func() {
w := fyne.CurrentApp().NewWindow("Central")
w.SetContent(fyne.NewContainerWithLayout(layout.NewCenterLayout(), widget.NewLabel("Hello World!")))
w.CenterOnScreen()
w.Show()
}))
drv := fyne.CurrentApp().Driver()
if drv, ok := drv.(desktop.Driver); ok {
windowGroup.Objects = append(windowGroup.Objects,
widget.NewButton("Splash Window (only use on start)", func() {
w := drv.CreateSplashWindow()
w.SetContent(widget.NewLabelWithStyle("Hello World!\n\nMake a splash!",
fyne.TextAlignCenter, fyne.TextStyle{Bold: true}))
w.Show()
go func() {
time.Sleep(time.Second * 3)
w.Close()
}()
}))
}
otherGroup := widget.NewCard("Other", "",
widget.NewButton("Notification", func() {
fyne.CurrentApp().SendNotification(&fyne.Notification{
Title: "Fyne Demo",
Content: "Testing notifications...",
})
}))
return container.NewVBox(widget.NewCard("Windows", "", windowGroup), otherGroup)
}

96
app/main.go Normal file
View File

@@ -0,0 +1,96 @@
package main
import (
"AynaLivePlayer/core/events"
"AynaLivePlayer/core/model"
"AynaLivePlayer/global"
"AynaLivePlayer/gui"
"AynaLivePlayer/gui/gctx"
"AynaLivePlayer/internal"
"AynaLivePlayer/pkg/config"
"AynaLivePlayer/pkg/eventbus"
"AynaLivePlayer/pkg/i18n"
"AynaLivePlayer/pkg/logger"
loggerRepo "AynaLivePlayer/pkg/logger/repository"
"flag"
"os"
"os/signal"
"path/filepath"
)
var dev = flag.Bool("dev", false, "dev")
var headless = flag.Bool("headless", false, "headless")
type _LogConfig struct {
config.BaseConfig
Path string
Level logger.LogLevel
RedirectStderr bool
MaxSize int64
}
func (c *_LogConfig) Name() string {
return "Log"
}
var Log = &_LogConfig{
Path: "./log.txt",
Level: logger.LogLevelInfo,
RedirectStderr: false, // this should be true if it is in production mode.
MaxSize: 5,
}
func setupGlobal() {
//global.EventManager = event.NewManger(128, 16)
global.EventBus = eventbus.New(eventbus.WithMaxWorkerSize(len(events.EventsMapping)))
global.Logger = loggerRepo.NewZapColoredLogger(Log.Path, !*dev)
global.Logger.SetLogLevel(Log.Level)
}
func init() {
flag.Parse()
// if not dev, set working directory to executable directory
if !*dev {
exePath, _ := os.Executable()
exePath, _ = filepath.EvalSymlinks(exePath)
exeDir := filepath.Dir(exePath)
_ = os.Chdir(exeDir)
}
}
func main() {
config.LoadFromFile(config.ConfigPath)
config.LoadConfig(Log)
i18n.LoadLanguage(config.General.Language)
setupGlobal()
global.Logger.Info("================Program Start================")
global.Logger.Infof("================Current Version: %s================", model.Version(config.Version))
internal.Initialize()
if *headless || config.Experimental.Headless {
quit := make(chan os.Signal)
signal.Notify(quit, os.Interrupt)
_ = global.EventBus.Start()
<-quit
} else {
gui.Initialize()
_ = global.EventBus.Start()
gctx.Context.Window.ShowAndRun()
}
global.Logger.Info("closing internal server")
internal.Stop()
global.Logger.Infof("closing event manager")
_ = global.EventBus.Stop()
_ = global.EventBus.Wait()
if *dev {
global.Logger.Infof("saving translation")
i18n.SaveTranslation()
}
err := config.SaveToConfigFile(config.ConfigPath)
if err != nil {
global.Logger.Errorf("save config failed: %v", err)
} else {
global.Logger.Infof("save config success")
}
global.Logger.Info("================Program End================")
}

32
assets/config/diange.json Normal file
View File

@@ -0,0 +1,32 @@
{
"bilibili-video": {
"enable": true,
"command": "点b歌",
"priority": 3
},
"kugou": {
"enable": true,
"command": "点kg歌",
"priority": 5
},
"kugou-instr": {
"enable": true,
"command": "点伴奏",
"priority": 6
},
"kuwo": {
"enable": true,
"command": "点k歌",
"priority": 2
},
"local": {
"enable": true,
"command": "点local",
"priority": 4
},
"netease": {
"enable": true,
"command": "点w歌",
"priority": 1
}
}

View File

@@ -0,0 +1,12 @@
[
{
"live_room": {
"provider": "biliweb",
"room": "3819533"
},
"config": {
"auto_connect": false
},
"title": "web 3819533"
}
]

2230
assets/config/playlists.json Normal file

File diff suppressed because one or more lines are too long

View File

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

BIN
assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

BIN
assets/icon2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 KiB

Binary file not shown.

Binary file not shown.

View File

View File

@@ -0,0 +1,68 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#First released as C++ program by Hiroyuki Tsutsumi as part of the free software suite “Beer”
#I thought porting it to Python could be both a challenge and useful
from sys import argv, exit, getsizeof
from struct import pack_into, unpack_from
def ceil4(n):
"""returns the next integer which is a multiple of 4"""
return (n + 3) & ~3
if len(argv)!=2:
print("Usage: %s FontCollection.ttc" % argv)
exit(2)
filename = argv[1]
in_file = open(filename, "rb")
buf = in_file.read()
in_file.close()
if filename.lower().endswith(".ttc"):
filename = filename[:-4]
if buf[:4] != b"ttcf":
out_filename = "%s.ttf" % filename
out_file = open(out_filename, "wb")
out_file.write(buf)
#end, so we dont have to close the files or call exit() here
else:
ttf_count = unpack_from("!L", buf, 0x08)[0]
print("Anzahl enthaltener TTF-Dateien: %s" % ttf_count)
ttf_offset_array = unpack_from("!"+ttf_count*"L", buf, 0x0C)
for i in range(ttf_count):
print("Extrahiere TTF #%s:" % (i+1))
table_header_offset = ttf_offset_array[i]
print("\tHeader beginnt bei Byte %s" % table_header_offset)
table_count = unpack_from("!H", buf, table_header_offset+0x04)[0]
header_length = 0x0C + table_count * 0x10
print("\tHeaderlänge: %s Byte" % header_length)
table_length = 0
for j in range(table_count):
length = unpack_from("!L", buf, table_header_offset+0x0C+0x0C+j*0x10)[0]
table_length += ceil4(length)
total_length = header_length + table_length
new_buf = bytearray(total_length)
header = unpack_from(header_length*"c", buf, table_header_offset)
pack_into(header_length*"c", new_buf, 0, *header)
current_offset = header_length
for j in range(table_count):
offset = unpack_from("!L", buf, table_header_offset+0x0C+0x08+j*0x10)[0]
length = unpack_from("!L", buf, table_header_offset+0x0C+0x0C+j*0x10)[0]
pack_into("!L", new_buf, 0x0C+0x08+j*0x10, current_offset)
current_table = unpack_from(length*"c", buf, offset)
pack_into(length*"c", new_buf, current_offset, *current_table)
#table_checksum = sum(unpack_from("!"+("L"*length), new_buf, current_offset))
#pack_into("!L", new_buf, 0x0C+0x04+j*0x10, table_checksum)
current_offset += ceil4(length)
out_file = open("%s%d.ttf"%(filename, i), "wb")
out_file.write(new_buf)

View File

@@ -0,0 +1 @@
start AynaLivePlayer.exe --headless

View File

@@ -0,0 +1 @@
taskkill /F /IM AynaLivePlayer.exe

View File

@@ -4,6 +4,10 @@
"zh-CN"
],
"Messages": {
"default web protocol. enter room id to connect.": {
"en": "default web protocol. enter room id to connect.",
"zh-CN": "网页弹幕协议,请输入房间号"
},
"gui.config.basic.audio_device": {
"en": "Audio Device",
"zh-CN": "音频输出设备"
@@ -36,14 +40,6 @@
"en": "User Playlist",
"zh-CN": "用户歌单"
},
"gui.config.basic.skip_playlist": {
"en": "Skip Media From System Playlist",
"zh-CN": "跳过闲置歌单"
},
"gui.config.basic.skip_playlist.prompt": {
"en": "Skip",
"zh-CN": "跳过"
},
"gui.config.basic.skip_when_error": {
"en": "Skip this Media On Error",
"zh-CN": "跳过获取失败的歌曲"
@@ -56,6 +52,14 @@
"en": "Basic",
"zh-CN": "基础设置"
},
"gui.config.basic.use_system_playlist": {
"en": "Play system playlist when no music",
"zh-CN": "是否播放闲置歌单(实验性)"
},
"gui.config.basic.use_system_playlist.prompt": {
"en": "Yes",
"zh-CN": "是"
},
"gui.history.artist": {
"en": "Artist",
"zh-CN": "歌手"
@@ -72,6 +76,10 @@
"en": "User",
"zh-CN": "用户"
},
"gui.lyric.title": {
"en": "Lyric",
"zh-CN": "歌词"
},
"gui.player.button.lrc": {
"en": "lrc",
"zh-CN": "歌词"
@@ -168,6 +176,10 @@
"en": "Room ID",
"zh-CN": "房间号"
},
"gui.room.add.name": {
"en": "Display Name",
"zh-CN": "显示名"
},
"gui.room.add.prompt": {
"en": "enter room id",
"zh-CN": "填入房间号"
@@ -268,21 +280,57 @@
"en": "Search",
"zh-CN": "搜索"
},
"gui.tray.btn.show": {
"en": "Show",
"zh-CN": "打开"
},
"gui.update.already_latest_version": {
"en": "no update available",
"zh-CN": "没有可用更新"
},
"gui.update.new_version": {
"en": "New Version Available",
"zh-CN": "有新版本可用"
},
"open bilibili live protocol. enter client key to connect.": {
"en": "open bilibili live protocol. enter client key to connect.",
"zh-CN": "新版b站协议输入身份码"
},
"plugin.diange.admin": {
"en": "Admin",
"zh-CN": "管理员"
},
"plugin.diange.blacklist.btn.add": {
"en": "Add",
"zh-CN": "添加"
},
"plugin.diange.blacklist.description": {
"en": "Blacklist Configuration",
"zh-CN": "点歌黑名单设置"
},
"plugin.diange.blacklist.input.placeholder": {
"en": "enter word",
"zh-CN": "输入黑名单词"
},
"plugin.diange.blacklist.option.contains": {
"en": "Contains",
"zh-CN": "包含"
},
"plugin.diange.blacklist.option.exact": {
"en": "Exact Match",
"zh-CN": "相等"
},
"plugin.diange.blacklist.title": {
"en": "Blacklist",
"zh-CN": "点歌黑名单"
},
"plugin.diange.cooldown": {
"en": "Cooldown",
"zh-CN": "点歌冷却"
},
"plugin.diange.custom_cmd": {
"en": "Custom Command (Default one still works)",
"zh-CN": "自定义命令 (默认的依然可用)"
"en": "Custom Command",
"zh-CN": "自定义命令"
},
"plugin.diange.description": {
"en": "Basic Diange Configuration",
@@ -312,6 +360,26 @@
"en": "Max Queue",
"zh-CN": "最大点歌数"
},
"plugin.diange.skip_playlist": {
"en": "Skip Media From System Playlist",
"zh-CN": "跳过闲置歌单"
},
"plugin.diange.skip_playlist.prompt": {
"en": "Skip",
"zh-CN": "跳过"
},
"plugin.diange.source.command": {
"en": "command",
"zh-CN": "命令"
},
"plugin.diange.source.enable": {
"en": "enable",
"zh-CN": "启用"
},
"plugin.diange.source.priority": {
"en": "priority",
"zh-CN": "优先级"
},
"plugin.diange.source_cmd": {
"en": "Source Command",
"zh-CN": "来源点歌命令"
@@ -324,45 +392,41 @@
"en": "User",
"zh-CN": "普通用户"
},
"plugin.neteaselogin.current_user": {
"en": "Current User:",
"zh-CN": "当前用户:"
"plugin.diange.user_max": {
"en": "User Maximum queue",
"zh-CN": "单个用户最大点歌数"
},
"plugin.neteaselogin.current_user.notlogin": {
"en": "Not Login",
"zh-CN": "未登录"
"plugin.maxduration.description": {
"en": "Set the maximum duration of a song",
"zh-CN": "设置歌曲最长能播多久"
},
"plugin.neteaselogin.description": {
"en": "Netease User Login",
"zh-CN": "网易云登录"
"plugin.maxduration.enable": {
"en": "Enable",
"zh-CN": "开启"
},
"plugin.neteaselogin.logout": {
"en": "Logout",
"zh-CN": "登出"
"plugin.maxduration.maxduration": {
"en": "Max Duration (seconds)",
"zh-CN": "最大时长 (秒)"
},
"plugin.neteaselogin.qr.finish": {
"en": "Finish Scan",
"zh-CN": "完成扫描后按我"
"plugin.maxduration.skiponplay": {
"en": "Skip on play",
"zh-CN": "播放时跳过"
},
"plugin.neteaselogin.qr.new": {
"en": "Get a new qr code",
"zh-CN": "获取新二维码"
"plugin.maxduration.skiponreach": {
"en": "Skip when reach max duration",
"zh-CN": "播放到最大时长时跳过"
},
"plugin.neteaselogin.refresh": {
"en": "Refresh",
"zh-CN": "刷新状态"
},
"plugin.neteaselogin.title": {
"en": "Netease Login",
"zh-CN": "网易云登录"
"plugin.maxduration.title": {
"en": "Audio Duration Control",
"zh-CN": "歌曲时长控制"
},
"plugin.qiege.admin": {
"en": "Admin",
"zh-CN": "管理员"
},
"plugin.qiege.custom_cmd": {
"en": "Custom Command (Default one still works)",
"zh-CN": "自定义命令 (默认的依然可用)"
"en": "Custom Command",
"zh-CN": "自定义命令"
},
"plugin.qiege.description": {
"en": "Basic Qiege configuration",
@@ -384,6 +448,42 @@
"en": "User",
"zh-CN": "切自己"
},
"plugin.sourcelogin.current_user": {
"en": "Current Status:",
"zh-CN": "当前状态:"
},
"plugin.sourcelogin.current_user.loggedin": {
"en": "Logged In",
"zh-CN": "已登录"
},
"plugin.sourcelogin.current_user.notlogin": {
"en": "Not Login",
"zh-CN": "未登录"
},
"plugin.sourcelogin.description": {
"en": "Netease User Login",
"zh-CN": "来源登录"
},
"plugin.sourcelogin.logout": {
"en": "Logout",
"zh-CN": "登出"
},
"plugin.sourcelogin.qr.finish": {
"en": "Finish Scan",
"zh-CN": "完成扫描后按我"
},
"plugin.sourcelogin.qr.new": {
"en": "Get a new qr code",
"zh-CN": "获取新二维码"
},
"plugin.sourcelogin.refresh": {
"en": "Refresh",
"zh-CN": "刷新状态"
},
"plugin.sourcelogin.title": {
"en": "Netease Login",
"zh-CN": "来源登录"
},
"plugin.textinfo.checkbox": {
"en": "Enable",
"zh-CN": "开启"
@@ -400,53 +500,93 @@
"en": "Text Output",
"zh-CN": "文本输出"
},
"plugin.webinfo.autostart": {
"plugin.wshub.autostart": {
"en": "Auto start",
"zh-CN": "自动启用"
},
"plugin.webinfo.description": {
"en": "Web output configuration",
"zh-CN": "web输出设置"
"plugin.wshub.description": {
"en": "Websocket Hub Configuration",
"zh-CN": "Websocket服务器设置"
},
"plugin.webinfo.port": {
"plugin.wshub.local_host_only": {
"en": "only allow local host connection",
"zh-CN": "只允许本地连接"
},
"plugin.wshub.port": {
"en": "Port",
"zh-CN": "服务器端口"
},
"plugin.webinfo.server_control": {
"plugin.wshub.server_control": {
"en": "Control",
"zh-CN": "操作"
},
"plugin.webinfo.server_control.restart": {
"plugin.wshub.server_control.restart": {
"en": "Restart",
"zh-CN": "重启"
},
"plugin.webinfo.server_control.start": {
"plugin.wshub.server_control.start": {
"en": "Start",
"zh-CN": "启动"
},
"plugin.webinfo.server_control.stop": {
"plugin.wshub.server_control.stop": {
"en": "Stop",
"zh-CN": "停止"
},
"plugin.webinfo.server_preview": {
"en": "Server Preview",
"zh-CN": "效果预览"
"plugin.wshub.server_link": {
"en": "Websocket Server Link",
"zh-CN": "Websocket服务器链接"
},
"plugin.webinfo.server_status": {
"plugin.wshub.server_status": {
"en": "Server Status",
"zh-CN": "服务器状态"
},
"plugin.webinfo.server_status.running": {
"plugin.wshub.server_status.running": {
"en": "Running",
"zh-CN": "运行中"
},
"plugin.webinfo.server_status.stopped": {
"plugin.wshub.server_status.stopped": {
"en": "Stopped",
"zh-CN": "已停止"
},
"plugin.webinfo.title": {
"en": "Web Output",
"zh-CN": "Web输出"
"plugin.wshub.title": {
"en": "Websocket Hub",
"zh-CN": "Websocket服务器"
},
"plugin.wshub.webinfo_text": {
"en": "Obs browser output",
"zh-CN": "OBS网页输出: "
},
"plugin.yinliang.admin_permission": {
"en": "Admin only",
"zh-CN": "仅房管可操作"
},
"plugin.yinliang.description": {
"en": "Control volume via danmaku",
"zh-CN": "通过弹幕控制音量"
},
"plugin.yinliang.enabled": {
"en": "Enabled volume control",
"zh-CN": "启用弹幕音量控制"
},
"plugin.yinliang.max_volume": {
"en": "Maximum volume (%)",
"zh-CN": "最大音量限制 (%)"
},
"plugin.yinliang.title": {
"en": "Volume Control",
"zh-CN": "音量控制"
},
"plugin.yinliang.volume_down_cmd": {
"en": "Volume decrease command",
"zh-CN": "音量减少命令"
},
"plugin.yinliang.volume_step": {
"en": "Adjustment step (%)",
"zh-CN": "每次音量调整 (%)"
},
"plugin.yinliang.volume_up_cmd": {
"en": "Volume increase command",
"zh-CN": "音量增加命令"
}
}
}

View File

@@ -1,16 +0,0 @@
package config
type _GeneralConfig struct {
BaseConfig
Language string
AutoCheckUpdate bool
}
func (c *_GeneralConfig) Name() string {
return "General"
}
var General = &_GeneralConfig{
Language: "en",
AutoCheckUpdate: true,
}

View File

@@ -1,146 +0,0 @@
package event
import (
"sync"
)
type EventId string
type Event struct {
Id EventId
Cancelled bool
Data interface{}
Outdated bool
lock sync.RWMutex
}
type HandlerFunc func(event *Event)
type Handler struct {
EventId EventId
Name string
Handler HandlerFunc
SkipOutdated bool
}
type Manager struct {
handlers map[EventId]map[string]*Handler
prevEvent map[EventId]*Event
queue chan func()
stopSig chan int
queueSize int
workerSize int
lock sync.RWMutex
}
func NewManger(queueSize int, workerSize int) *Manager {
manager := &Manager{
handlers: make(map[EventId]map[string]*Handler),
prevEvent: make(map[EventId]*Event),
queue: make(chan func(), queueSize),
stopSig: make(chan int, workerSize),
queueSize: queueSize,
workerSize: workerSize,
}
for i := 0; i < workerSize; i++ {
go func() {
for {
select {
case <-manager.stopSig:
return
case f := <-manager.queue:
f()
}
}
}()
}
return manager
}
func (h *Manager) NewChildManager() *Manager {
return &Manager{
handlers: make(map[EventId]map[string]*Handler),
prevEvent: make(map[EventId]*Event),
queue: h.queue,
stopSig: h.stopSig,
queueSize: h.queueSize,
workerSize: h.workerSize,
}
}
func (h *Manager) Stop() {
for i := 0; i < h.workerSize; i++ {
h.stopSig <- 0
}
}
func (h *Manager) Register(handler *Handler) {
h.lock.Lock()
defer h.lock.Unlock()
m, ok := h.handlers[handler.EventId]
if !ok {
m = make(map[string]*Handler)
h.handlers[handler.EventId] = m
}
m[handler.Name] = handler
}
func (h *Manager) RegisterA(id EventId, name string, handler HandlerFunc) {
h.Register(&Handler{
EventId: id,
Name: name,
Handler: handler,
SkipOutdated: true,
})
}
func (h *Manager) UnregisterAll() {
h.lock.Lock()
defer h.lock.Unlock()
h.handlers = make(map[EventId]map[string]*Handler)
}
func (h *Manager) Unregister(name string) {
h.lock.Lock()
defer h.lock.Unlock()
for _, m := range h.handlers {
if _, ok := m[name]; ok {
delete(m, name)
}
}
}
func (h *Manager) Call(event *Event) {
h.lock.Lock()
handlers, ok := h.handlers[event.Id]
if e := h.prevEvent[event.Id]; e != nil {
e.lock.Lock()
e.Outdated = true
e.lock.Unlock()
}
h.prevEvent[event.Id] = event
h.lock.Unlock()
if !ok {
return
}
for _, eh := range handlers {
eventHandler := eh
h.queue <- func() {
event.lock.Lock()
if eventHandler.SkipOutdated && event.Outdated {
event.lock.Unlock()
return
}
eventHandler.Handler(event)
event.lock.Unlock()
}
}
}
func (h *Manager) CallA(id EventId, data interface{}) {
h.Call(&Event{
Id: id,
Data: data,
})
}

View File

@@ -1,35 +0,0 @@
package event
import (
"fmt"
"testing"
"time"
)
func TestEventSeq(t *testing.T) {
m := NewManger(128, 16)
m.RegisterA("ceshi", "asdf1", func(event *Event) {
fmt.Println("Num:", event.Data)
})
go func() {
for i := 0; i < 1000; i++ {
m.CallA("ceshi", fmt.Sprintf("a%d", i))
}
}()
for i := 0; i < 1000; i++ {
m.CallA("ceshi", i)
}
}
func TestEventWeired(t *testing.T) {
m := NewManger(128, 2)
m.RegisterA("playlist.update", "asdf1", func(event *Event) {
fmt.Printf("%d %p, outdated: %t\n", event.Data, event, event.Outdated)
})
for i := 0; i < 2; i++ {
fmt.Println("asdfsafasfasfasfasfasf")
m.CallA("playlist.update", i)
fmt.Println("asdfsafasfasfasfasfasf")
}
time.Sleep(1 * time.Second)
}

View File

@@ -1,10 +0,0 @@
package event
var MAX_QUEUE_SIZE = 128
var MAX_WORKER_SIZE = 16
var MainManager *Manager
func init() {
MainManager = NewManger(MAX_QUEUE_SIZE, MAX_WORKER_SIZE)
}

View File

@@ -1,26 +0,0 @@
package logger
//var Logger *logrus.Logger
//
//func init() {
// Logger = logrus.New()
// Logger.SetLevel(config.Log.Level)
// Logger.SetFormatter(&nested.Formatter{
// FieldsOrder: []string{"Module"},
// HideKeys: true,
// NoColors: true,
// })
// //_ = os.Truncate(config.Log.Path, 0)
// file, err := os.OpenFile(config.Log.Path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
// if err == nil {
// Logger.Out = io.MultiWriter(file, os.Stdout)
// } else {
// Logger.Info("Failed to log to file, using default stdout")
// }
// if config.Log.RedirectStderr {
// Logger.Info("panic/stderr redirect to log file")
// if _, err = paniclog.RedirectStderr(file); err != nil {
// Logger.Infof("Failed to redirect stderr to to file: %s", err)
// }
// }
//}

View File

@@ -1,7 +0,0 @@
package util
import "fmt"
func FormatTime(time int) string {
return fmt.Sprintf("%01d:%02d", time/60, time%60)
}

View File

@@ -1,9 +0,0 @@
package adapter
import "AynaLivePlayer/core/model"
type IApplication interface {
Version() model.VersionInfo
LatestVersion() model.VersionInfo
CheckUpdate() error
}

View File

@@ -1,31 +0,0 @@
package adapter
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/core/model"
)
type LiveClientCtor func(id string, ev *event.Manager, log ILogger) (LiveClient, error)
type LiveClient interface {
ClientName() string
RoomName() string
Connect() bool
Disconnect() bool
Status() bool
EventManager() *event.Manager
}
type ILiveRoom interface {
Client() LiveClient
Model() *model.LiveRoom // should return mutable model (not a copy)
Identifier() string
DisplayName() string
Status() bool
EventManager() *event.Manager
}
type LiveRoomExecutor interface {
Match(command string) bool
Execute(command string, args []string, danmu *model.DanmuMessage)
}

View File

@@ -1,23 +0,0 @@
package adapter
type LogLevel uint32
const (
LogLevelError LogLevel = iota
LogLevelWarn
LogLevelInfo
LogLevelDebug
)
type ILogger interface {
Debug(args ...interface{})
Debugf(format string, args ...interface{})
Info(args ...interface{})
Infof(format string, args ...interface{})
Warn(args ...interface{})
Warnf(format string, args ...interface{})
Error(args ...interface{})
Errorf(format string, args ...interface{})
WithModule(prefix string) ILogger
SetLogLevel(level LogLevel)
}

View File

@@ -1,40 +0,0 @@
package adapter
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/core/model"
)
type PlayerCtor func(ev *event.Manager, log ILogger) IPlayer
type IPlayer interface {
// Start the player
Start()
// Stop the player
Stop()
// Play play a media
Play(media *model.Media) error
// GetPlaying get playing media
// if player is idle, return nil
GetPlaying() *model.Media
// IsPaused return true if player is paused
IsPaused() bool
// Pause pause player
Pause() error
// Unpause unpause player
Unpause() error
// SetVolume set volume
SetVolume(volume float64) error
// IsIdle return true if player is playing anything
IsIdle() bool
// Seek to position, if absolute is true, position is absolute time, otherwise position is relative time
Seek(position float64, absolute bool) error
// SetWindowHandle set window handle for video output
SetWindowHandle(handle uintptr) error
// ObserveProperty observe player property change
ObserveProperty(property model.PlayerProperty, name string, handler event.HandlerFunc) error
// GetAudioDeviceList get audio device list
GetAudioDeviceList() ([]model.AudioDevice, error)
// SetAudioDevice set audio device
SetAudioDevice(device string) error
}

View File

@@ -1,22 +0,0 @@
package adapter
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/core/model"
)
type IPlaylist interface {
Identifier() string // must unique for each playlist
Model() *model.Playlist // mutable model (not a copy)
EventManager() *event.Manager
DisplayName() string
Size() int
Get(index int) *model.Media
Pop() *model.Media
Replace(medias []*model.Media)
Push(media *model.Media)
Insert(index int, media *model.Media)
Delete(index int) *model.Media
Move(src int, dst int)
Next() *model.Media
}

View File

@@ -1,17 +0,0 @@
package adapter
import "AynaLivePlayer/core/model"
type MediaProviderConfig map[string]string
type MediaProviderCtor func(config MediaProviderConfig) MediaProvider
type MediaProvider interface {
GetName() string
MatchMedia(keyword string) *model.Media
GetPlaylist(playlist *model.Meta) ([]*model.Media, error)
FormatPlaylistUrl(uri string) string
Search(keyword string) ([]*model.Media, error)
UpdateMedia(media *model.Media) error
UpdateMediaUrl(media *model.Media) error
UpdateMediaLyric(media *model.Media) error
}

View File

@@ -1,91 +0,0 @@
package adapter
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/core/model"
)
// IControlBridge is the interface for all controller and
// all system use cases.
type IControlBridge interface {
App() IApplication
LiveRooms() ILiveRoomController
PlayControl() IPlayController
Playlists() IPlaylistController
Provider() IProviderController
Plugin() IPluginController
LoadPlugins(plugins ...Plugin)
Logger() ILogger
CloseAndSave()
}
type ILiveRoomController interface {
Size() int
Get(index int) ILiveRoom
GetRoomStatus(index int) bool
Connect(index int) error
Disconnect(index int) error
AddRoom(clientName, roomId string) (*model.LiveRoom, error)
DeleteRoom(index int) error
AddDanmuCommand(executor LiveRoomExecutor)
GetAllClientNames() []string
}
type IProviderController interface {
GetPriority() []string
PrepareMedia(media *model.Media) error
MediaMatch(keyword string) *model.Media
Search(keyword string) ([]*model.Media, error)
SearchWithProvider(keyword string, provider string) ([]*model.Media, error)
PreparePlaylist(playlist IPlaylist) error
}
type IPluginController interface {
LoadPlugin(plugin Plugin)
LoadPlugins(plugins ...Plugin)
ClosePlugins()
}
type IPlaylistController interface {
Size() int
GetHistory() IPlaylist
AddToHistory(media *model.Media)
GetDefault() IPlaylist
GetCurrent() IPlaylist
Get(index int) IPlaylist
Add(pname string, uri string) (IPlaylist, error)
Remove(index int) (IPlaylist, error)
SetDefault(index int) error
PreparePlaylistByIndex(index int) error
}
type IPlayControlConfig struct {
SkipPlaylist bool
AutoNextWhenFail bool
}
type IPlayController interface {
EventManager() *event.Manager
GetPlaying() *model.Media
GetPlayer() IPlayer
PlayNext()
Play(media *model.Media) error
Add(keyword string, user interface{})
AddWithProvider(keyword string, provider string, user interface{})
Seek(position float64, absolute bool)
Toggle() bool
SetVolume(volume float64)
Destroy()
GetCurrentAudioDevice() string
GetAudioDevices() []model.AudioDevice
SetAudioDevice(device string)
GetLyric() ILyricLoader
Config() *IPlayControlConfig
}
type ILyricLoader interface {
EventManager() *event.Manager
Get() *model.Lyric
Reload(lyric string)
Update(time float64)
}

View File

@@ -1,54 +1,56 @@
package events
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/core/model"
)
//const (
// EventPlay string = "player.play"
// EventPlayed string = "player.played"
// EventPlaylistPreInsert string = "playlist.insert.pre"
// EventPlaylistInsert string = "playlist.insert.after"
// EventPlaylistUpdate string = "playlist.update"
// EventLyricUpdate string = "lyric.update"
// EventLyricReload string = "lyric.reload"
//)
const (
EventPlay event.EventId = "player.play"
EventPlayed event.EventId = "player.played"
EventPlaylistPreInsert event.EventId = "playlist.insert.pre"
EventPlaylistInsert event.EventId = "playlist.insert.after"
EventPlaylistUpdate event.EventId = "playlist.update"
EventLyricUpdate event.EventId = "lyric.update"
EventLyricReload event.EventId = "lyric.reload"
)
const ErrorUpdate = "update.error"
func EventPlayerPropertyUpdate(property model.PlayerProperty) event.EventId {
return event.EventId("player.property.update." + string(property))
type ErrorUpdateEvent struct {
Error error
}
type PlaylistInsertEvent struct {
Playlist *model.Playlist
Index int
Media *model.Media
}
type PlaylistUpdateEvent struct {
Playlist *model.Playlist // Playlist is a copy of the playlist
}
type PlayEvent struct {
Media *model.Media
}
type LyricUpdateEvent struct {
Lyrics *model.Lyric
Time float64
Lyric *model.LyricContext
}
type LyricReloadEvent struct {
Lyrics *model.Lyric
}
type PlayerPropertyUpdateEvent struct {
Property model.PlayerProperty
Value model.PlayerPropertyValue
}
type LiveRoomStatusUpdateEvent struct {
RoomTitle string
Status bool
}
//
//func EventPlayerPropertyUpdate(property model.PlayerProperty) string {
// return string("player.property.update." + string(property))
//}
//
//type PlaylistInsertEvent struct {
// Playlist *model.Playlist
// Index int
// Media *model.Media
//}
//
//type PlaylistUpdateEvent struct {
// Playlist *model.Playlist // Playlist is a copy of the playlist
//}
//
//type PlayEvent struct {
// Media *model.Media
//}
//
//type LyricUpdateEvent struct {
// Lyrics *model.Lyric
// Time float64
// Lyric *model.LyricContext
//}
//
//type LyricReloadEvent struct {
// Lyrics *model.Lyric
//}
//
//type PlayerPropertyUpdateEvent struct {
// Property model.PlayerProperty
// Value model.PlayerPropertyValue
//}
//
//type UpdateLiveRoomStatusData struct {
// RoomTitle string
// Status bool
//}

25
core/events/events.go Normal file
View File

@@ -0,0 +1,25 @@
package events
/*
# events package
events package contains all events used in application.
in theory. all interaction should use events package.
the events are dispatched using eventbus package.
Here are some major events
- cmd: call cmd
- reply: call reply
- update: information updating event. usually issued by internal controller and broadcast to all channel
naming convention
- cmd: 'cmd.event.id.'
- reply: 'reply.same.same.cmd.id'
- update: 'update.event.id'
*/

7
core/events/gui.go Normal file
View File

@@ -0,0 +1,7 @@
package events
const GUISetPlayerWindowOpenCmd = "cmd.gui.player_window.op"
type GUISetPlayerWindowOpenCmdEvent struct {
SetOpen bool
}

View File

@@ -1,16 +1,64 @@
package events
import (
"AynaLivePlayer/common/event"
"AynaLivePlayer/core/adapter"
"AynaLivePlayer/core/model"
liveroomsdk "github.com/AynaLivePlayer/liveroom-sdk"
)
const (
LiveRoomStatusChange event.EventId = "liveclient.status.change"
LiveRoomMessageReceive event.EventId = "liveclient.message.receive"
)
const CmdLiveRoomAdd = "cmd.liveroom.add"
type StatusChangeEvent struct {
Connected bool
Client adapter.LiveClient
type CmdLiveRoomAddData struct {
Title string
Provider string
RoomKey string
}
const CmdLiveRoomRemove = "cmd.liveroom.remove"
type CmdLiveRoomRemoveData struct {
Identifier string
}
const CmdLiveRoomConfigChange = "cmd.liveroom.config.change"
type CmdLiveRoomConfigChangeData struct {
Identifier string
Config model.LiveRoomConfig
}
const LiveRoomProviderUpdate = "update.liveroom.provider"
type LiveRoomProviderUpdateEvent struct {
Providers []model.LiveRoomProviderInfo
}
const UpdateLiveRoomRooms = "update.liveroom.rooms"
type UpdateLiveRoomRoomsData struct {
Rooms []model.LiveRoom
}
const UpdateLiveRoomStatus = "update.liveroom.status"
type UpdateLiveRoomStatusData struct {
Room model.LiveRoom
}
const CmdLiveRoomOperation = "cmd.liveroom.operation"
type CmdLiveRoomOperationData struct {
Identifier string
SetConnect bool // connect or disconnect
}
const ReplyLiveRoomOperation = "reply.liveroom.operation"
type ReplyLiveRoomOperationData struct {
Err error
}
const LiveRoomMessageReceive = "update.liveroom.message"
type LiveRoomMessageReceiveEvent struct {
Message *liveroomsdk.Message
}

78
core/events/mapping.go Normal file
View File

@@ -0,0 +1,78 @@
package events
import (
"AynaLivePlayer/core/model"
"encoding/json"
"errors"
"reflect"
)
var EventsMapping = map[string]any{
CmdLiveRoomAdd: CmdLiveRoomAddData{},
LiveRoomProviderUpdate: LiveRoomProviderUpdateEvent{},
CmdLiveRoomRemove: CmdLiveRoomRemoveData{},
UpdateLiveRoomRooms: UpdateLiveRoomRoomsData{},
UpdateLiveRoomStatus: UpdateLiveRoomStatusData{},
CmdLiveRoomConfigChange: CmdLiveRoomConfigChangeData{},
CmdLiveRoomOperation: CmdLiveRoomOperationData{},
PlayerVolumeChangeCmd: PlayerVolumeChangeCmdEvent{},
PlayerPlayCmd: PlayerPlayCmdEvent{},
PlayerPlayErrorUpdate: PlayerPlayErrorUpdateEvent{},
PlayerSeekCmd: PlayerSeekCmdEvent{},
PlayerToggleCmd: PlayerToggleCmdEvent{},
PlayerSetPauseCmd: PlayerSetPauseCmdEvent{},
PlayerPlayNextCmd: PlayerPlayNextCmdEvent{},
CmdGetCurrentLyric: CmdGetCurrentLyricData{},
UpdateCurrentLyric: UpdateCurrentLyricData{},
PlayerLyricPosUpdate: PlayerLyricPosUpdateEvent{},
PlayerPlayingUpdate: PlayerPlayingUpdateEvent{},
PlayerPropertyPauseUpdate: PlayerPropertyPauseUpdateEvent{},
PlayerPropertyPercentPosUpdate: PlayerPropertyPercentPosUpdateEvent{},
PlayerPropertyStateUpdate: PlayerPropertyStateUpdateEvent{},
PlayerPropertyTimePosUpdate: PlayerPropertyTimePosUpdateEvent{},
PlayerPropertyDurationUpdate: PlayerPropertyDurationUpdateEvent{},
PlayerPropertyVolumeUpdate: PlayerPropertyVolumeUpdateEvent{},
PlayerVideoPlayerSetWindowHandleCmd: PlayerVideoPlayerSetWindowHandleCmdEvent{},
PlayerSetAudioDeviceCmd: PlayerSetAudioDeviceCmdEvent{},
PlayerAudioDeviceUpdate: PlayerAudioDeviceUpdateEvent{},
PlaylistManagerSetSystemCmd: PlaylistManagerSetSystemCmdEvent{},
PlaylistManagerSystemUpdate: PlaylistManagerSystemUpdateEvent{},
PlaylistManagerRefreshCurrentCmd: PlaylistManagerRefreshCurrentCmdEvent{},
PlaylistManagerGetCurrentCmd: PlaylistManagerGetCurrentCmdEvent{},
PlaylistManagerCurrentUpdate: PlaylistManagerCurrentUpdateEvent{},
PlaylistManagerInfoUpdate: PlaylistManagerInfoUpdateEvent{},
PlaylistManagerAddPlaylistCmd: PlaylistManagerAddPlaylistCmdEvent{},
PlaylistManagerRemovePlaylistCmd: PlaylistManagerRemovePlaylistCmdEvent{},
MediaProviderUpdate: MediaProviderUpdateEvent{},
CmdMiaosicSearch: CmdMiaosicSearchData{},
ReplyMiaosicSearch: ReplyMiaosicSearchData{},
GUISetPlayerWindowOpenCmd: GUISetPlayerWindowOpenCmdEvent{},
}
func init() {
for _, v := range []model.PlaylistID{model.PlaylistIDSystem, model.PlaylistIDPlayer} {
EventsMapping[PlaylistDetailUpdate(v)] = PlaylistDetailUpdateEvent{}
EventsMapping[PlaylistMoveCmd(v)] = PlaylistMoveCmdEvent{}
EventsMapping[PlaylistSetIndexCmd(v)] = PlaylistSetIndexCmdEvent{}
EventsMapping[PlaylistDeleteCmd(v)] = PlaylistDeleteCmdEvent{}
EventsMapping[PlaylistInsertCmd(v)] = PlaylistInsertCmdEvent{}
EventsMapping[PlaylistInsertUpdate(v)] = PlaylistInsertUpdateEvent{}
EventsMapping[PlaylistNextCmd(v)] = PlaylistNextCmdEvent{}
EventsMapping[PlaylistNextUpdate(v)] = PlaylistNextUpdateEvent{}
EventsMapping[PlaylistModeChangeCmd(v)] = PlaylistModeChangeCmdEvent{}
EventsMapping[PlaylistModeChangeUpdate(v)] = PlaylistModeChangeUpdateEvent{}
}
}
func UnmarshalEventData(eventId string, data []byte) (any, error) {
val, ok := EventsMapping[eventId]
if !ok {
return nil, errors.New("event id not found")
}
newVal := reflect.New(reflect.TypeOf(val))
err := json.Unmarshal(data, newVal.Interface())
if err != nil {
return nil, err
}
return newVal.Elem().Interface(), nil
}

View File

@@ -0,0 +1,22 @@
package events
import (
"encoding/json"
"github.com/stretchr/testify/require"
"testing"
)
func TestUnmarshalEventData(t *testing.T) {
eventData := CmdLiveRoomAddData{
Title: "test",
Provider: "asdfasd",
RoomKey: "asdfasdf",
}
data, err := json.Marshal(eventData)
require.NoError(t, err)
val, err := UnmarshalEventData(CmdLiveRoomAdd, data)
require.NoError(t, err)
resultData, ok := val.(CmdLiveRoomAddData)
require.True(t, ok)
require.Equal(t, eventData, resultData)
}

57
core/events/miaosic.go Normal file
View File

@@ -0,0 +1,57 @@
package events
import "github.com/AynaLivePlayer/miaosic"
const CmdMiaosicGetMediaInfo = "cmd.miaosic.getMediaInfo"
type CmdMiaosicGetMediaInfoData struct {
Meta miaosic.MetaData `json:"meta"`
}
const ReplyMiaosicGetMediaInfo = "reply.miaosic.getMediaInfo"
type ReplyMiaosicGetMediaInfoData struct {
Info miaosic.MediaInfo `json:"info"`
Error error `json:"error"`
}
const CmdMiaosicGetMediaUrl = "cmd.miaosic.getMediaUrl"
type CmdMiaosicGetMediaUrlData struct {
Meta miaosic.MetaData `json:"meta"`
Quality miaosic.Quality `json:"quality"`
}
const ReplyMiaosicGetMediaUrl = "reply.miaosic.getMediaUrl"
type ReplyMiaosicGetMediaUrlData struct {
Urls []miaosic.MediaUrl `json:"urls"`
Error error `json:"error"`
}
const CmdMiaosicQrLogin = "cmd.miaosic.qrLogin"
type CmdMiaosicQrLoginData struct {
Provider string `json:"provider"`
}
const ReplyMiaosicQrLogin = "reply.miaosic.qrLogin"
type ReplyMiaosicQrLoginData struct {
Session miaosic.QrLoginSession `json:"session"`
Error error `json:"error"`
}
const CmdMiaosicQrLoginVerify = "cmd.miaosic.qrLoginVerify"
type CmdMiaosicQrLoginVerifyData struct {
Provider string `json:"provider"`
Session miaosic.QrLoginSession `json:"session"`
}
const ReplyMiaosicQrLoginVerify = "reply.miaosic.qrLoginVerify"
type ReplyMiaosicQrLoginVerifyData struct {
Result miaosic.QrLoginResult `json:"result"`
Error error `json:"error"`
}

View File

@@ -0,0 +1,49 @@
package events
import (
"AynaLivePlayer/core/model"
)
const PlayerVolumeChangeCmd = "cmd.player.op.change_volume"
type PlayerVolumeChangeCmdEvent struct {
Volume float64 // Volume from 0-100
}
const PlayerPlayCmd = "cmd.player.op.play"
type PlayerPlayCmdEvent struct {
Media model.Media
}
const PlayerPlayErrorUpdate = "update.player.play.error"
type PlayerPlayErrorUpdateEvent struct {
Error error
}
const PlayerSeekCmd = "cmd.player.op.seek"
type PlayerSeekCmdEvent struct {
Position float64
// Absolute is the seek mode.
// if absolute = true : position is the time in second
// if absolute = false: position is in percentage eg 0.1 0.2
Absolute bool
}
const PlayerToggleCmd = "cmd.player.op.toggle"
type PlayerToggleCmdEvent struct {
}
const PlayerSetPauseCmd = "cmd.player.op.pause"
type PlayerSetPauseCmdEvent struct {
Pause bool
}
const PlayerPlayNextCmd = "cmd.player.op.next"
type PlayerPlayNextCmdEvent struct {
}

View File

@@ -0,0 +1,23 @@
package events
import "github.com/AynaLivePlayer/miaosic"
const CmdGetCurrentLyric = "cmd.player.lyric.request"
type CmdGetCurrentLyricData struct {
}
const UpdateCurrentLyric = "update.player.lyric.reload"
type UpdateCurrentLyricData struct {
Lyrics miaosic.Lyrics
}
const PlayerLyricPosUpdate = "update.player.lyric.pos"
type PlayerLyricPosUpdateEvent struct {
Time float64
CurrentIndex int // -1 means no lyric
CurrentLine miaosic.LyricLine
Total int // total lyric count
}

View File

@@ -0,0 +1,46 @@
package events
import "AynaLivePlayer/core/model"
const PlayerPlayingUpdate = "update.player.playing"
type PlayerPlayingUpdateEvent struct {
Media model.Media
Removed bool // if no media is playing, removed is true
}
const PlayerPropertyPauseUpdate = "update.player.property.pause"
type PlayerPropertyPauseUpdateEvent struct {
Paused bool
}
const PlayerPropertyPercentPosUpdate = "update.player.property.percent_pos"
type PlayerPropertyPercentPosUpdateEvent struct {
PercentPos float64
}
const PlayerPropertyStateUpdate = "update.player.property.state"
type PlayerPropertyStateUpdateEvent struct {
State model.PlayerState
}
const PlayerPropertyTimePosUpdate = "update.player.property.time_pos"
type PlayerPropertyTimePosUpdateEvent struct {
TimePos float64 // Time in seconds
}
const PlayerPropertyDurationUpdate = "update.player.property.duration"
type PlayerPropertyDurationUpdateEvent struct {
Duration float64 // Duration in seconds
}
const PlayerPropertyVolumeUpdate = "update.player.property.volume"
type PlayerPropertyVolumeUpdateEvent struct {
Volume float64 // Volume from 0-100
}

View File

@@ -0,0 +1,22 @@
package events
import "AynaLivePlayer/core/model"
const PlayerVideoPlayerSetWindowHandleCmd = "cmd.player.videoplayer.set_window_handle"
type PlayerVideoPlayerSetWindowHandleCmdEvent struct {
Handle uintptr
}
const PlayerSetAudioDeviceCmd = "cmd.player.set_audio_device"
type PlayerSetAudioDeviceCmdEvent struct {
Device string
}
const PlayerAudioDeviceUpdate = "update.player.audio_device"
type PlayerAudioDeviceUpdateEvent struct {
Current string
Devices []model.AudioDevice
}

88
core/events/playlist.go Normal file
View File

@@ -0,0 +1,88 @@
package events
import (
"AynaLivePlayer/core/model"
)
func PlaylistDetailUpdate(id model.PlaylistID) string {
return string("update.playlist.detail." + id)
}
type PlaylistDetailUpdateEvent struct {
Medias []model.Media
}
func PlaylistMoveCmd(id model.PlaylistID) string {
return string("cmd.playlist.move." + id)
}
type PlaylistMoveCmdEvent struct {
From int
To int
}
func PlaylistSetIndexCmd(id model.PlaylistID) string {
return string("cmd.playlist.setindex." + id)
}
type PlaylistSetIndexCmdEvent struct {
Index int
}
func PlaylistDeleteCmd(id model.PlaylistID) string {
return string("cmd.playlist.delete." + id)
}
type PlaylistDeleteCmdEvent struct {
Index int
}
func PlaylistInsertCmd(id model.PlaylistID) string {
return string("cmd.playlist.insert." + id)
}
type PlaylistInsertCmdEvent struct {
Position int // position to insert, -1 means last one
Media model.Media
}
func PlaylistInsertUpdate(id model.PlaylistID) string {
return string("update.playlist.insert." + id)
}
type PlaylistInsertUpdateEvent struct {
Position int // position to insert, -1 means last one
Media model.Media
}
func PlaylistNextCmd(id model.PlaylistID) string {
return string("cmd.playlist.next." + id)
}
type PlaylistNextCmdEvent struct {
Remove bool // remove the media after next
}
func PlaylistNextUpdate(id model.PlaylistID) string {
return string("update.playlist.next." + id)
}
type PlaylistNextUpdateEvent struct {
Media model.Media
}
func PlaylistModeChangeCmd(id model.PlaylistID) string {
return string("cmd.playlist.mode." + id)
}
type PlaylistModeChangeCmdEvent struct {
Mode model.PlaylistMode
}
func PlaylistModeChangeUpdate(id model.PlaylistID) string {
return string("update.playlist.mode." + id)
}
type PlaylistModeChangeUpdateEvent struct {
Mode model.PlaylistMode
}

54
core/events/playlists.go Normal file
View File

@@ -0,0 +1,54 @@
package events
import (
"AynaLivePlayer/core/model"
)
const PlaylistManagerSetSystemCmd = "cmd.playlist.manager.set.system"
type PlaylistManagerSetSystemCmdEvent struct {
PlaylistID string
}
const PlaylistManagerSystemUpdate = "update.playlist.manager.system"
type PlaylistManagerSystemUpdateEvent struct {
Info model.PlaylistInfo
}
const PlaylistManagerRefreshCurrentCmd = "cmd.playlist.manager.refresh.current"
type PlaylistManagerRefreshCurrentCmdEvent struct {
PlaylistID string
}
const PlaylistManagerGetCurrentCmd = "cmd.playlist.manager.get.current"
type PlaylistManagerGetCurrentCmdEvent struct {
PlaylistID string
}
const PlaylistManagerCurrentUpdate = "update.playlist.manager.current"
type PlaylistManagerCurrentUpdateEvent struct {
Medias []model.Media
}
const PlaylistManagerInfoUpdate = "update.playlist.manager.info"
type PlaylistManagerInfoUpdateEvent struct {
Playlists []model.PlaylistInfo
}
const PlaylistManagerAddPlaylistCmd = "cmd.playlist.manager.add"
type PlaylistManagerAddPlaylistCmdEvent struct {
Provider string
URL string
}
const PlaylistManagerRemovePlaylistCmd = "cmd.playlist.manager.remove"
type PlaylistManagerRemovePlaylistCmdEvent struct {
PlaylistID string
}

7
core/events/provider.go Normal file
View File

@@ -0,0 +1,7 @@
package events
const MediaProviderUpdate = "update.media.provider.update"
type MediaProviderUpdateEvent struct {
Providers []string
}

18
core/events/search.go Normal file
View File

@@ -0,0 +1,18 @@
package events
import (
"AynaLivePlayer/core/model"
)
const CmdMiaosicSearch = "cmd.search"
type CmdMiaosicSearchData struct {
Keyword string
Provider string
}
const ReplyMiaosicSearch = "update.search_result"
type ReplyMiaosicSearchData struct {
Medias []model.Media
}

15
core/events/updater.go Normal file
View File

@@ -0,0 +1,15 @@
package events
import "AynaLivePlayer/core/model"
const CheckUpdateCmd = "cmd.update.check"
type CheckUpdateCmdEvent struct {
}
const CheckUpdateResultUpdate = "update.update.check"
type CheckUpdateResultUpdateEvent struct {
HasUpdate bool
Info model.VersionInfo
}

View File

@@ -1,40 +1,26 @@
package model
import (
"fmt"
)
import "github.com/AynaLivePlayer/liveroom-sdk"
type LiveRoomConfig struct {
AutoConnect bool `json:"auto_connect"`
}
type LiveRoom struct {
ClientName string
ID string
Title string
AutoConnect bool
AutoReconnect bool
LiveRoom liveroom.LiveRoom `json:"live_room"`
Config LiveRoomConfig `json:"config"`
Title string `json:"title"`
Status bool `json:"status"`
}
func (r *LiveRoom) String() string {
return fmt.Sprintf("<LiveRooms %s:%s>", r.ClientName, r.ID)
func (r *LiveRoom) DisplayName() string {
if r.Title != "" {
return r.Title
}
return r.LiveRoom.Identifier()
}
func (r *LiveRoom) Identifier() string {
return fmt.Sprintf("%s_%s", r.ClientName, r.ID)
}
type UserMedal struct {
Name string
Level int
RoomID string
}
type DanmuUser struct {
Uid string
Username string
Medal UserMedal
Admin bool
Privilege int
}
type DanmuMessage struct {
User DanmuUser
Message string
type LiveRoomProviderInfo struct {
Name string
Description string
}

View File

@@ -1,117 +0,0 @@
package model
import (
"github.com/spf13/cast"
"regexp"
"sort"
"strings"
)
var timeTagRegex = regexp.MustCompile("\\[[0-9]+:[0-9]+(\\.[0-9]+)?\\]")
type LyricLine struct {
Time float64 // in seconds
Lyric string
Translation string
}
type LyricContext struct {
Now *LyricLine
Index int
Total int
Prev []*LyricLine
Next []*LyricLine
}
type Lyric struct {
Lyrics []*LyricLine
}
func LoadLyric(lyric string) *Lyric {
tmp := make(map[float64]*LyricLine)
times := make([]float64, 0)
for _, line := range strings.Split(lyric, "\n") {
lrc := timeTagRegex.ReplaceAllString(line, "")
if len(lrc) > 0 && lrc[len(lrc)-1] == '\r' {
lrc = lrc[:len(lrc)-1]
}
for _, time := range timeTagRegex.FindAllString(line, -1) {
ts := strings.Split(time[1:len(time)-1], ":")
t := cast.ToFloat64(ts[0])*60 + cast.ToFloat64(ts[1])
times = append(times, t)
tmp[t] = &LyricLine{
Time: t,
Lyric: lrc,
}
}
}
sort.Float64s(times)
lrcs := make([]*LyricLine, len(times))
for index, time := range times {
lrcs[index] = tmp[time]
}
if len(lrcs) == 0 {
lrcs = append(lrcs, &LyricLine{Time: 0, Lyric: ""})
}
lrcs = append(lrcs, &LyricLine{
Time: 99999999999,
Lyric: "",
})
return &Lyric{Lyrics: lrcs}
}
func (l *Lyric) findIndexV1(time float64) int {
for i := 0; i < len(l.Lyrics)-1; i++ {
if l.Lyrics[i].Time <= time && time < l.Lyrics[i+1].Time {
return i
}
}
return -1
}
func (l *Lyric) findIndex(time float64) int {
start := 0
end := len(l.Lyrics) - 1
mid := (start + end) / 2
for start < end {
if l.Lyrics[mid].Time <= time && time < l.Lyrics[mid+1].Time {
return mid
}
if l.Lyrics[mid].Time > time {
end = mid
} else {
start = mid
}
mid = (start + end) / 2
}
return -1
}
func (l *Lyric) Find(time float64) *LyricLine {
idx := l.findIndex(time)
if idx == -1 {
return nil
}
return l.Lyrics[idx]
}
func (l *Lyric) FindContext(time float64, prev int, next int) *LyricContext {
prev = -prev
idx := l.findIndex(time)
if idx == -1 {
return nil
}
if (idx + prev) < 0 {
prev = -idx
}
if (idx + 1 + next) > len(l.Lyrics) {
next = len(l.Lyrics) - idx - 1
}
return &LyricContext{
Now: l.Lyrics[idx],
Index: idx,
Total: len(l.Lyrics),
Prev: l.Lyrics[idx+prev : idx],
Next: l.Lyrics[idx+1 : idx+1+next],
}
}

View File

@@ -1,31 +0,0 @@
package model
import (
"fmt"
"github.com/magiconair/properties/assert"
"testing"
)
var testLyric = "[ti:双截棍]\n[ar:周杰伦]\n[al:范特西]\n[00:03.85]双截棍\n[00:07.14]\n[00:30.13]岩烧店的烟味弥漫隔壁是国术馆\n[00:32.57]店里面的妈妈桑茶道有三段\n[00:34.61]教拳脚武术的老板练铁沙掌耍杨家枪\n[00:37.34]硬底子功夫最擅长还会金钟罩铁步衫\n[00:39.67]他们儿子我习惯从小就耳濡目染\n[00:41.96]什么刀枪跟棍棒我都耍的有模有样\n[00:44.22]什么兵器最喜欢双截棍柔中带刚\n[00:46.73]想要去河南嵩山学少林跟武当\n[00:49.24]干什么(客)干什么(客)呼吸吐纳心自在\n[00:51.28]干什么(客)干什么(客)气沉丹田手心开\n[00:53.44]干什么(客)干什么(客)日行千里系沙袋\n[00:56.13]飞檐走壁莫奇怪去去就来\n[00:58.35]一个马步向前一记左钩拳右钩拳\n[01:01.26]一句惹毛我的人有危险一再重演\n[01:04.02]一根我不抽的菸一放好多年它一直在身边\n[01:07.28]干什么(客)干什么(客)我打开任督二脉\n[01:10.27]干什么(客)干什么(客)东亚病夫的招牌\n[01:12.75]干什么(客)干什么(客)已被我一脚踢开\n[02:32.62][01:54.69][01:15.40]快使用双截棍哼哼哈兮\n[02:34.52][01:56.63][01:18.40]快使用双截棍哼哼哈兮\n[02:36.88][01:58.98][01:20.71]习武之人切记仁者无敌\n[02:39.45][02:01.66][01:23.27]是谁在练太极风生水起\n[02:41.97][02:03.93][01:25.74]快使用双截棍哼哼哈兮\n[02:44.42][02:06.11][01:27.75]快使用双截棍哼哼哈兮\n[02:47.01][02:08.54][01:30.13]如果我有轻功飞檐走壁\n[02:49.36][02:11.03][01:32.67]为人耿直不屈一身正气\n[02:53.81]快使用双截棍哼\n[02:56.30]我用手刀防御哼\n[02:58.52]漂亮的回旋踢\n[02:59.52]"
func TestLyric(t *testing.T) {
lryic := LoadLyric(testLyric)
for _, lrc := range lryic.Lyrics {
fmt.Println(lrc)
}
}
func TestLyricFind(t *testing.T) {
lryic := LoadLyric(testLyric)
fmt.Println(lryic.Find(90.4))
for _, l := range lryic.FindContext(90.4, -2, 2).Next {
fmt.Println(l)
}
}
func TestLyricFindV2(t *testing.T) {
lryic := LoadLyric(testLyric)
for i := 0.0; i < 170; i += 0.01 {
assert.Equal(t, lryic.Find(i), lryic.Find(i))
}
}

View File

@@ -1,46 +1,37 @@
package model
import (
"github.com/jinzhu/copier"
"github.com/AynaLivePlayer/liveroom-sdk"
"github.com/AynaLivePlayer/miaosic"
)
type Picture struct {
Url string
Data []byte
type User struct {
Name string
}
func (p Picture) Exists() bool {
return p.Url != "" || p.Data != nil
}
var PlaylistUser = User{Name: "Playlists"}
var SystemUser = User{Name: "System"}
type Media struct {
Title string
Artist string
Cover Picture
Album string
Lyric string
Url string
Header map[string]string
User interface{}
Meta interface{}
Info miaosic.MediaInfo
User interface{}
}
func (m *Media) ToUser() *User {
if u, ok := m.User.(*User); ok {
func (m *Media) IsLiveRoomUser() bool {
_, ok := m.User.(liveroom.User)
return ok
}
func (m *Media) ToUser() User {
if u, ok := m.User.(User); ok {
return u
}
return &User{Name: m.DanmuUser().Username}
return User{Name: m.DanmuUser().Username}
}
func (m *Media) DanmuUser() *DanmuUser {
if u, ok := m.User.(*DanmuUser); ok {
func (m *Media) DanmuUser() liveroom.User {
if u, ok := m.User.(liveroom.User); ok {
return u
}
return nil
}
func (m *Media) Copy() *Media {
newMedia := &Media{}
copier.Copy(newMedia, m)
return newMedia
return liveroom.User{}
}

Some files were not shown because too many files have changed in this diff Show More