├── .eslintrc.json
├── .github
├── FUNDING.yml
└── ISSUE_TEMPLATE
│ └── bug_report.md
├── .gitignore
├── .nvmrc
├── .vscode
└── settings.json
├── COMPATIBILITY.md
├── LICENSE
├── README.md
├── assets
├── config.json5
├── icons
│ ├── 128x128.png
│ ├── 16x16.png
│ ├── 256x256.png
│ ├── 32x32.png
│ ├── 512x512.png
│ ├── 64x64.png
│ ├── icon.ico
│ └── icon.svg
├── kwin-widget-rule.kwinrule
├── preview_browser.png
├── preview_lyrics.png
├── preview_obs.png
├── preview_widget.png
├── preview_widget_2.png
└── preview_widget_3.png
├── dist
└── www
│ ├── assets
│ ├── favicon
│ │ ├── android-chrome-192x192.png
│ │ ├── android-chrome-512x512.png
│ │ ├── apple-touch-icon.png
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ └── favicon.ico
│ ├── font
│ │ ├── Urbanist-Black.woff2
│ │ ├── Urbanist-BlackItalic.woff2
│ │ ├── Urbanist-Bold.woff2
│ │ ├── Urbanist-BoldItalic.woff2
│ │ ├── Urbanist-ExtraBold.woff2
│ │ ├── Urbanist-ExtraBoldItalic.woff2
│ │ ├── Urbanist-Italic.woff2
│ │ ├── Urbanist-Light.woff2
│ │ ├── Urbanist-LightItalic.woff2
│ │ ├── Urbanist-Regular.woff2
│ │ ├── Urbanist-SemiBold.woff2
│ │ ├── Urbanist-SemiBoldItalic.woff2
│ │ ├── Urbanist-Thin.woff2
│ │ └── Urbanist-ThinItalic.woff2
│ ├── images
│ │ ├── apps
│ │ │ ├── aimp.svg
│ │ │ ├── amarok.svg
│ │ │ ├── chrome.svg
│ │ │ ├── clementine.svg
│ │ │ ├── default.svg
│ │ │ ├── elisa.svg
│ │ │ ├── firefox.svg
│ │ │ ├── foobar2000.svg
│ │ │ ├── gnome-music.svg
│ │ │ ├── groove.svg
│ │ │ ├── librewolf.svg
│ │ │ ├── lollypop.svg
│ │ │ ├── mpv.svg
│ │ │ ├── msedge.svg
│ │ │ ├── musicbee.svg
│ │ │ ├── pithos.svg
│ │ │ ├── qmmp.svg
│ │ │ ├── rhythmbox.svg
│ │ │ ├── sonixd.svg
│ │ │ ├── spotify.svg
│ │ │ ├── strawberry.svg
│ │ │ ├── tauon.svg
│ │ │ ├── vivaldi.svg
│ │ │ └── vlc.svg
│ │ ├── glyph.svg
│ │ ├── no_song.png
│ │ └── no_song.svg
│ └── styles
│ │ ├── font
│ │ └── Urbanist.css
│ │ ├── style.css
│ │ └── ui
│ │ ├── Electron.css
│ │ ├── base
│ │ ├── General.css
│ │ └── WindowControls.css
│ │ └── sunamu
│ │ ├── AlbumArt.css
│ │ ├── Background.css
│ │ ├── Info.css
│ │ ├── Lyrics.css
│ │ ├── Metadata.css
│ │ ├── NowPlaying.css
│ │ ├── PlaybackControls.css
│ │ ├── PlayingIndicator.css
│ │ ├── Seekbar.css
│ │ ├── Sunamu.css
│ │ └── Variables.css
│ ├── index.htm
│ └── sunamu.webmanifest
├── electron-builder.yml
├── package.json
├── src
├── main
│ ├── appStatus.ts
│ ├── config.ts
│ ├── electron.ts
│ ├── index.ts
│ ├── instance.ts
│ ├── integrations
│ │ ├── discordrpc.ts
│ │ ├── lyrics.ts
│ │ ├── lyricsOffline.ts
│ │ └── tracklogger.ts
│ ├── logger.ts
│ ├── lyricproviders
│ │ ├── genius.ts
│ │ ├── local.ts
│ │ ├── lrc.ts
│ │ ├── metadata.ts
│ │ ├── musixmatch.ts
│ │ └── netease.ts
│ ├── platform
│ │ └── win32.ts
│ ├── playbackStatus.ts
│ ├── player
│ │ ├── index.ts
│ │ ├── mpris2.ts
│ │ └── winplayer.ts
│ ├── themes.ts
│ ├── thirdparty
│ │ ├── lastfm.ts
│ │ └── spotify.ts
│ ├── tsconfig.json
│ ├── util.ts
│ └── webserver.ts
├── types.ts
└── www
│ ├── index.ts
│ ├── lib
│ ├── appicon.ts
│ ├── buttons.ts
│ ├── config.ts
│ ├── event.ts
│ ├── lang
│ │ ├── de.ts
│ │ ├── en.ts
│ │ ├── es.ts
│ │ ├── fr.ts
│ │ ├── index.ts
│ │ ├── it.ts
│ │ ├── nl.ts
│ │ ├── zh-cn.ts
│ │ └── zh-tw.ts
│ ├── lyrics.ts
│ ├── nowplaying.ts
│ ├── npapi
│ │ ├── browser-npapi.ts
│ │ ├── electron-npapi.ts
│ │ └── tsconfig.electron.json
│ ├── screen.ts
│ ├── seekbar.ts
│ ├── showhide.ts
│ ├── songdata.ts
│ ├── thirdparty
│ │ └── socket.io.esm.min.js
│ └── util.ts
│ └── tsconfig.json
├── tsconfig.json
├── tsconfig.settings.json
└── yarn.lock
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "es2020": true,
4 | "node": true,
5 | "browser": true
6 | },
7 | "extends": "eslint:recommended",
8 | "parser": "@typescript-eslint/parser",
9 | "plugins": [
10 | "@typescript-eslint"
11 | ],
12 | "rules": {
13 | "arrow-body-style": [
14 | "error",
15 | "as-needed"
16 | ],
17 | "class-methods-use-this": 1,
18 | "eqeqeq": [
19 | "error",
20 | "smart"
21 | ],
22 | "dot-notation": [
23 | "error",
24 | {
25 | "allowKeywords": true
26 | }
27 | ],
28 | "func-call-spacing": [
29 | "error",
30 | "never"
31 | ],
32 | "func-names": [
33 | "error",
34 | "as-needed"
35 | ],
36 | "prefer-arrow-callback": [
37 | "error",
38 | {
39 | "allowNamedFunctions": true,
40 | "allowUnboundThis": true
41 | }
42 | ],
43 | "func-style": [
44 | "error",
45 | "declaration",
46 | {
47 | "allowArrowFunctions": true
48 | }
49 | ],
50 | "indent": [
51 | "error",
52 | "tab",
53 | {
54 | "SwitchCase": 1,
55 | "VariableDeclarator": 1,
56 | "MemberExpression": 1,
57 | "FunctionDeclaration": {
58 | "parameters": 1
59 | },
60 | "CallExpression": {
61 | "arguments": 1
62 | },
63 | "ArrayExpression": 1,
64 | "ObjectExpression": 1,
65 | "ImportDeclaration": 1,
66 | "flatTernaryExpressions": true,
67 | "offsetTernaryExpressions": true,
68 | "ignoreComments": false
69 | }
70 | ],
71 | "linebreak-style": [
72 | "error",
73 | "unix"
74 | ],
75 | "quotes": [
76 | "error",
77 | "double"
78 | ],
79 | "semi": [
80 | "error",
81 | "always"
82 | ],
83 | "curly": [
84 | "error",
85 | "multi-or-nest"
86 | ],
87 | "no-case-declarations": "off"
88 | }
89 | }
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | ko_fi: NyaomiPic
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 |
21 |
22 | **Describe the bug**
23 | A clear and concise description of what the bug is.
24 |
25 | **To Reproduce**
26 | Steps to reproduce the behavior:
27 | 1. Go to '...'
28 | 2. Click on '....'
29 | 3. Scroll down to '....'
30 | 4. See error
31 |
32 | **Expected behavior**
33 | A clear and concise description of what you expected to happen.
34 |
35 | **Screenshots**
36 | If applicable, add screenshots to help explain your problem.
37 |
38 | **Running platform (please complete the following information):**
39 | - OS: [e.g. Arch Linux]
40 | - Version: [e.g. 2.2.0 - or the commit hash]
41 | - Mode: [e.g. Electron / Browser]
42 |
43 | **Additional context**
44 | Add any other context about the problem here.
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | /dist/types*
3 | /dist/main/
4 | /dist/www/lib/
5 | /dist/www/index*
6 | !/dist/www/index.htm
7 | /lib/
8 | /targets/
9 | /yarn-*.log
10 | /.env
11 | /test.js
12 |
13 | # just in case someone runs makepkg directly
14 | /aur/sunamu*/*
15 | !/aur/sunamu*/PKGBUILD
16 | !/aur/sunamu*/.SRCINFO
17 | !/aur/sunamu*/sunamu.desktop
18 | !/aur/sunamu*/sunamu.sh
19 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 20
2 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.associations": {
3 | "*.json5": "json5"
4 | },
5 | "C_Cpp.errorSquiggles": "Enabled"
6 | }
--------------------------------------------------------------------------------
/COMPATIBILITY.md:
--------------------------------------------------------------------------------
1 | # Player compatibility
2 |
3 | ### Legend
4 | - Perfect: Works flawlessly
5 | - Working: Works, but additional work is needed OR caveats apply.
6 | - Detected: Sunamu detects it, but it doesn't work.
7 | - Not working: Sunamu cannot detect it.
8 |
9 | |Name|Status|Comments|
10 | |-|-|-|
11 | |Spotify|Perfect|They fixed their shit recently! Yay!|
12 | |VLC|Perfect||
13 | |Strawberry|Perfect||
14 | |elementary Music|Perfect|Reported by @KorbsStudio|
15 | |Rhythmbox|Perfect|Reported by @KorbsStudio|
16 | |Spot|Perfect|Reported by @KorbsStudio|
17 | |Clementine|Perfect|Reported by @KorbsStudio|
18 | |Tauon Music Box|Perfect|Reported by @KorbsStudio|
19 | |Pithos|Perfect|Reported by @KorbsStudio|
20 | |Sonixd|Perfect||
21 | |GNOME Music|Working|Cover arts are not shown "at first try". Possibly, the `PropertiesChanged` event is not handled well enough.|
22 | |QMMP|Working|Please enable the MPRIS plugin|
23 | |Spotifyd|Working|While the MPRIS2 implementation is kinda okay, they still need to raise the appropriate D-Bus `PropertiesChanged` event. See [their issue tracker](https://github.com/Spotifyd/spotifyd/issues/457).|
24 | |Spotify-Qt|Working|The developer has an issue ticket detailing some of the caveats [here](https://github.com/kraxarn/spotify-qt/issues/4).
25 | |MPV|Working|With the [mpv-mpris](https://github.com/hoyon/mpv-mpris) plugin|
26 | |Plasma Browser Integration|Working|It all depends on the website, really|
27 | |Lollypop|Detected|It doesn't implement the `PropertiesChanged` signal|
28 | |Amarok|Detected|Seems to not report the song information; Reported by @KorbsStudio|
29 | |Amberol|Detected|Non-compliant MPRIS2 implementation. Sunamu cannot support players not reporting `mpris:trackId`|
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Sunamu (スナム)
4 | Show your currently playing song in a stylish way!
5 |
6 | ## Screenshots
7 |
8 |
9 |

10 |

11 |

12 |

13 |

14 |

15 |
16 |
17 |
18 | ## OwO wats dis?
19 |
20 | Sunamu (pronounced as it is written) is a fancy music controller whose only purpose is to look as fancy as possible on secondary displays.
21 |
22 | _It effectively is the WAY TOO COMPLEX successor of MPRISLyrics, a project I made back when synchronized lyrics on Linux was a niche thing to have._
23 |
24 | ## Features
25 |
26 | - Display what you are playing in your TV, secondary display, or (heck) around the entire house!
27 | - Get the Spotify link for every song you listen to!*
28 | - Get lyrics for your songs!
29 | - Get a _GOOD_ Discord Rich Presence, finally!*
30 | - Bragging rights for your particular taste in music!**
31 |
32 | *This feature, or part of it, requires a Spotify Client ID and Client Secret.
33 |
34 | **No responsibility is taken from the Sunamu devs and contributors if you have bad taste in music
35 |
36 | ## Installation
37 |
38 | **Sunamu only works on Linux! Do not spam the issue section regarding Windows or macOS, as it cannot be ported to those computers!**
39 |
40 | ### Linux
41 |
42 | #### NOTE: Sunamu is very slow on development, mostly because there's hardly anything to implement that the developer can do. Please consider this when running a tagged release, as you can easily run a months-old, or even years-old, version. Please try compiling Sunamu from this repo instead!
43 |
44 | Get the latest release from the [Releases](https://github.com/NyaomiDEV/Sunamu/releases/latest) section.
45 |
46 | ## Configuration
47 |
48 | Sunamu's configuration file is located in:
49 | - Linux: Usually `~/.config/sunamu/config.json5` (`$XDG_CONFIG_HOME/sunamu/config.json5`);
50 | - Linux Flatpak: `~/.var/app/xyz.nyaomi.sunamu/config/sunamu/config.json5` (unsupported! but you can compile it yourself);
51 |
52 | You can use it to enable or disable features, and there are a LOT of them!
53 |
54 | Do you want to give it a read? [Here it is!](assets/config.json5)
55 |
56 | ## Usage
57 |
58 | Just launch it and preferably put it in fullscreen!
59 |
60 | ## Notable observed quirks
61 |
62 | Check and contribute to the compatibility table [here](COMPATIBILITY.md).
63 |
64 | ## License
65 |
66 | See the [LICENSE](LICENSE) file.
67 |
--------------------------------------------------------------------------------
/assets/icons/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NyaomiDEV/Sunamu/537d2986b5df0583c6c3064cacadce8efebb0a6b/assets/icons/128x128.png
--------------------------------------------------------------------------------
/assets/icons/16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NyaomiDEV/Sunamu/537d2986b5df0583c6c3064cacadce8efebb0a6b/assets/icons/16x16.png
--------------------------------------------------------------------------------
/assets/icons/256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NyaomiDEV/Sunamu/537d2986b5df0583c6c3064cacadce8efebb0a6b/assets/icons/256x256.png
--------------------------------------------------------------------------------
/assets/icons/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NyaomiDEV/Sunamu/537d2986b5df0583c6c3064cacadce8efebb0a6b/assets/icons/32x32.png
--------------------------------------------------------------------------------
/assets/icons/512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NyaomiDEV/Sunamu/537d2986b5df0583c6c3064cacadce8efebb0a6b/assets/icons/512x512.png
--------------------------------------------------------------------------------
/assets/icons/64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NyaomiDEV/Sunamu/537d2986b5df0583c6c3064cacadce8efebb0a6b/assets/icons/64x64.png
--------------------------------------------------------------------------------
/assets/icons/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NyaomiDEV/Sunamu/537d2986b5df0583c6c3064cacadce8efebb0a6b/assets/icons/icon.ico
--------------------------------------------------------------------------------
/assets/icons/icon.svg:
--------------------------------------------------------------------------------
1 |
36 |
--------------------------------------------------------------------------------
/assets/kwin-widget-rule.kwinrule:
--------------------------------------------------------------------------------
1 | [Sunamu Widget]
2 | Description=Sunamu Widget
3 | activity=00000000-0000-0000-0000-000000000000
4 | activityrule=2
5 | below=true
6 | belowrule=2
7 | desktops=
8 | desktopsrule=2
9 | ignoregeometry=true
10 | ignoregeometryrule=2
11 | position=0,0
12 | positionrule=2
13 | size=458,512
14 | sizerule=2
15 | skippager=true
16 | skippagerrule=2
17 | skipswitcher=true
18 | skipswitcherrule=2
19 | skiptaskbar=true
20 | skiptaskbarrule=2
21 | title=Sunamu Widget
22 | titlematch=1
23 | types=1
24 | windowrole=browser-window
25 | wmclass=sunamu
26 | wmclassmatch=1
27 |
--------------------------------------------------------------------------------
/assets/preview_browser.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NyaomiDEV/Sunamu/537d2986b5df0583c6c3064cacadce8efebb0a6b/assets/preview_browser.png
--------------------------------------------------------------------------------
/assets/preview_lyrics.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NyaomiDEV/Sunamu/537d2986b5df0583c6c3064cacadce8efebb0a6b/assets/preview_lyrics.png
--------------------------------------------------------------------------------
/assets/preview_obs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NyaomiDEV/Sunamu/537d2986b5df0583c6c3064cacadce8efebb0a6b/assets/preview_obs.png
--------------------------------------------------------------------------------
/assets/preview_widget.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NyaomiDEV/Sunamu/537d2986b5df0583c6c3064cacadce8efebb0a6b/assets/preview_widget.png
--------------------------------------------------------------------------------
/assets/preview_widget_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NyaomiDEV/Sunamu/537d2986b5df0583c6c3064cacadce8efebb0a6b/assets/preview_widget_2.png
--------------------------------------------------------------------------------
/assets/preview_widget_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NyaomiDEV/Sunamu/537d2986b5df0583c6c3064cacadce8efebb0a6b/assets/preview_widget_3.png
--------------------------------------------------------------------------------
/dist/www/assets/favicon/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NyaomiDEV/Sunamu/537d2986b5df0583c6c3064cacadce8efebb0a6b/dist/www/assets/favicon/android-chrome-192x192.png
--------------------------------------------------------------------------------
/dist/www/assets/favicon/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NyaomiDEV/Sunamu/537d2986b5df0583c6c3064cacadce8efebb0a6b/dist/www/assets/favicon/android-chrome-512x512.png
--------------------------------------------------------------------------------
/dist/www/assets/favicon/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NyaomiDEV/Sunamu/537d2986b5df0583c6c3064cacadce8efebb0a6b/dist/www/assets/favicon/apple-touch-icon.png
--------------------------------------------------------------------------------
/dist/www/assets/favicon/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NyaomiDEV/Sunamu/537d2986b5df0583c6c3064cacadce8efebb0a6b/dist/www/assets/favicon/favicon-16x16.png
--------------------------------------------------------------------------------
/dist/www/assets/favicon/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NyaomiDEV/Sunamu/537d2986b5df0583c6c3064cacadce8efebb0a6b/dist/www/assets/favicon/favicon-32x32.png
--------------------------------------------------------------------------------
/dist/www/assets/favicon/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NyaomiDEV/Sunamu/537d2986b5df0583c6c3064cacadce8efebb0a6b/dist/www/assets/favicon/favicon.ico
--------------------------------------------------------------------------------
/dist/www/assets/font/Urbanist-Black.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NyaomiDEV/Sunamu/537d2986b5df0583c6c3064cacadce8efebb0a6b/dist/www/assets/font/Urbanist-Black.woff2
--------------------------------------------------------------------------------
/dist/www/assets/font/Urbanist-BlackItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NyaomiDEV/Sunamu/537d2986b5df0583c6c3064cacadce8efebb0a6b/dist/www/assets/font/Urbanist-BlackItalic.woff2
--------------------------------------------------------------------------------
/dist/www/assets/font/Urbanist-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NyaomiDEV/Sunamu/537d2986b5df0583c6c3064cacadce8efebb0a6b/dist/www/assets/font/Urbanist-Bold.woff2
--------------------------------------------------------------------------------
/dist/www/assets/font/Urbanist-BoldItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NyaomiDEV/Sunamu/537d2986b5df0583c6c3064cacadce8efebb0a6b/dist/www/assets/font/Urbanist-BoldItalic.woff2
--------------------------------------------------------------------------------
/dist/www/assets/font/Urbanist-ExtraBold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NyaomiDEV/Sunamu/537d2986b5df0583c6c3064cacadce8efebb0a6b/dist/www/assets/font/Urbanist-ExtraBold.woff2
--------------------------------------------------------------------------------
/dist/www/assets/font/Urbanist-ExtraBoldItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NyaomiDEV/Sunamu/537d2986b5df0583c6c3064cacadce8efebb0a6b/dist/www/assets/font/Urbanist-ExtraBoldItalic.woff2
--------------------------------------------------------------------------------
/dist/www/assets/font/Urbanist-Italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NyaomiDEV/Sunamu/537d2986b5df0583c6c3064cacadce8efebb0a6b/dist/www/assets/font/Urbanist-Italic.woff2
--------------------------------------------------------------------------------
/dist/www/assets/font/Urbanist-Light.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NyaomiDEV/Sunamu/537d2986b5df0583c6c3064cacadce8efebb0a6b/dist/www/assets/font/Urbanist-Light.woff2
--------------------------------------------------------------------------------
/dist/www/assets/font/Urbanist-LightItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NyaomiDEV/Sunamu/537d2986b5df0583c6c3064cacadce8efebb0a6b/dist/www/assets/font/Urbanist-LightItalic.woff2
--------------------------------------------------------------------------------
/dist/www/assets/font/Urbanist-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NyaomiDEV/Sunamu/537d2986b5df0583c6c3064cacadce8efebb0a6b/dist/www/assets/font/Urbanist-Regular.woff2
--------------------------------------------------------------------------------
/dist/www/assets/font/Urbanist-SemiBold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NyaomiDEV/Sunamu/537d2986b5df0583c6c3064cacadce8efebb0a6b/dist/www/assets/font/Urbanist-SemiBold.woff2
--------------------------------------------------------------------------------
/dist/www/assets/font/Urbanist-SemiBoldItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NyaomiDEV/Sunamu/537d2986b5df0583c6c3064cacadce8efebb0a6b/dist/www/assets/font/Urbanist-SemiBoldItalic.woff2
--------------------------------------------------------------------------------
/dist/www/assets/font/Urbanist-Thin.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NyaomiDEV/Sunamu/537d2986b5df0583c6c3064cacadce8efebb0a6b/dist/www/assets/font/Urbanist-Thin.woff2
--------------------------------------------------------------------------------
/dist/www/assets/font/Urbanist-ThinItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NyaomiDEV/Sunamu/537d2986b5df0583c6c3064cacadce8efebb0a6b/dist/www/assets/font/Urbanist-ThinItalic.woff2
--------------------------------------------------------------------------------
/dist/www/assets/images/apps/aimp.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/dist/www/assets/images/apps/amarok.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/dist/www/assets/images/apps/chrome.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/dist/www/assets/images/apps/clementine.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/dist/www/assets/images/apps/default.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/dist/www/assets/images/apps/elisa.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/dist/www/assets/images/apps/firefox.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/dist/www/assets/images/apps/gnome-music.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/dist/www/assets/images/apps/groove.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/dist/www/assets/images/apps/librewolf.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/dist/www/assets/images/apps/lollypop.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/dist/www/assets/images/apps/mpv.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/dist/www/assets/images/apps/msedge.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/dist/www/assets/images/apps/musicbee.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/dist/www/assets/images/apps/pithos.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/dist/www/assets/images/apps/qmmp.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/dist/www/assets/images/apps/rhythmbox.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/dist/www/assets/images/apps/sonixd.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/dist/www/assets/images/apps/spotify.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/dist/www/assets/images/apps/strawberry.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/dist/www/assets/images/apps/tauon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/dist/www/assets/images/apps/vivaldi.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/dist/www/assets/images/apps/vlc.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/dist/www/assets/images/no_song.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NyaomiDEV/Sunamu/537d2986b5df0583c6c3064cacadce8efebb0a6b/dist/www/assets/images/no_song.png
--------------------------------------------------------------------------------
/dist/www/assets/images/no_song.svg:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/dist/www/assets/styles/font/Urbanist.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: "Urbanist";
3 | src: url("../../font/Urbanist-BoldItalic.woff2") format("woff2");
4 | font-weight: bold;
5 | font-style: italic;
6 | font-display: swap;
7 | }
8 |
9 | @font-face {
10 | font-family: "Urbanist";
11 | src: url("../../font/Urbanist-SemiBold.woff2") format("woff2");
12 | font-weight: 600;
13 | font-style: normal;
14 | font-display: swap;
15 | }
16 |
17 | @font-face {
18 | font-family: "Urbanist";
19 | src: url("../../font/Urbanist-ExtraBold.woff2") format("woff2");
20 | font-weight: bold;
21 | font-style: normal;
22 | font-display: swap;
23 | }
24 |
25 | @font-face {
26 | font-family: "Urbanist";
27 | src: url("../../font/Urbanist-Thin.woff2") format("woff2");
28 | font-weight: 100;
29 | font-style: normal;
30 | font-display: swap;
31 | }
32 |
33 | @font-face {
34 | font-family: "Urbanist";
35 | src: url("../../font/Urbanist-Bold.woff2") format("woff2");
36 | font-weight: bold;
37 | font-style: normal;
38 | font-display: swap;
39 | }
40 |
41 | @font-face {
42 | font-family: "Urbanist";
43 | src: url("../../font/Urbanist-BlackItalic.woff2") format("woff2");
44 | font-weight: 900;
45 | font-style: italic;
46 | font-display: swap;
47 | }
48 |
49 | @font-face {
50 | font-family: "Urbanist";
51 | src: url("../../font/Urbanist-ThinItalic.woff2") format("woff2");
52 | font-weight: 100;
53 | font-style: italic;
54 | font-display: swap;
55 | }
56 |
57 | @font-face {
58 | font-family: "Urbanist";
59 | src: url("../../font/Urbanist-LightItalic.woff2") format("woff2");
60 | font-weight: 300;
61 | font-style: italic;
62 | font-display: swap;
63 | }
64 |
65 | @font-face {
66 | font-family: "Urbanist";
67 | src: url("../../font/Urbanist-Light.woff2") format("woff2");
68 | font-weight: 300;
69 | font-style: normal;
70 | font-display: swap;
71 | }
72 |
73 | @font-face {
74 | font-family: "Urbanist";
75 | src: url("../../font/Urbanist-Italic.woff2") format("woff2");
76 | font-weight: normal;
77 | font-style: italic;
78 | font-display: swap;
79 | }
80 |
81 | @font-face {
82 | font-family: "Urbanist";
83 | src: url("../../font/Urbanist-Black.woff2") format("woff2");
84 | font-weight: 900;
85 | font-style: normal;
86 | font-display: swap;
87 | }
88 |
89 | @font-face {
90 | font-family: "Urbanist";
91 | src: url("../../font/Urbanist-Regular.woff2") format("woff2");
92 | font-weight: normal;
93 | font-style: normal;
94 | font-display: swap;
95 | }
96 |
97 | @font-face {
98 | font-family: "Urbanist";
99 | src: url("../../font/Urbanist-SemiBoldItalic.woff2") format("woff2");
100 | font-weight: 600;
101 | font-style: italic;
102 | font-display: swap;
103 | }
104 |
105 | @font-face {
106 | font-family: "Urbanist";
107 | src: url("../../font/Urbanist-ExtraBoldItalic.woff2") format("woff2");
108 | font-weight: bold;
109 | font-style: italic;
110 | font-display: swap;
111 | }
112 |
113 |
--------------------------------------------------------------------------------
/dist/www/assets/styles/style.css:
--------------------------------------------------------------------------------
1 | /* Fonts */
2 | @import url("font/Urbanist.css");
3 |
4 | @import url("ui/base/General.css");
5 | @import url("ui/base/WindowControls.css");
6 |
7 | /* Sunamu rules */
8 | @import url("ui/sunamu/Sunamu.css");
9 |
10 | /* Electron rules */
11 | @import url("ui/Electron.css");
12 |
--------------------------------------------------------------------------------
/dist/www/assets/styles/ui/Electron.css:
--------------------------------------------------------------------------------
1 | html.electron:not(.fullscreen) .background {
2 | -webkit-app-region: drag;
3 | }
4 |
5 | html.electron #minimize,
6 | html.electron #close {
7 | display: block;
8 | }
9 |
10 | html.electron .window-controls,
11 | html.electron #now-playing,
12 | html.electron #lyrics,
13 | html.electron #lyrics-copyright {
14 | -webkit-app-region: no-drag;
15 | }
16 |
17 | html.electron.widget-mode .background {
18 | -webkit-app-region: no-drag;
19 | }
20 |
21 | html.electron.widget-mode:not(.fullscreen) #album-art {
22 | -webkit-app-region: drag;
23 | }
24 |
--------------------------------------------------------------------------------
/dist/www/assets/styles/ui/base/General.css:
--------------------------------------------------------------------------------
1 | html {
2 | overflow: hidden;
3 | background-color: black;
4 | margin: 0;
5 | padding: 0;
6 | user-select: none;
7 | scrollbar-color: var(--color-fg) transparent;
8 | scrollbar-width: 4px;
9 | }
10 |
11 | html.idle:not(.force-idle) {
12 | cursor: none;
13 | }
14 |
15 | html.widget-mode {
16 | background-color: transparent;
17 | }
18 |
19 | body {
20 | margin: 0;
21 | padding: 0;
22 | }
23 |
24 | html.idle:not(.force-idle) body {
25 | cursor: none;
26 | }
27 |
28 | html.widget-mode body {
29 | transition: ease .2s filter;
30 | }
31 |
32 | html.widget-mode.not-playing.idle body,
33 | html.widget-mode.not-playing.hide-when-not-playing body {
34 | filter: opacity(0);
35 | }
36 |
37 | a:-webkit-any-link {
38 | text-decoration: none !important;
39 | }
40 |
41 | ::-webkit-scrollbar {
42 | width: 4px;
43 | }
44 |
45 | ::-webkit-scrollbar-track {
46 | background-color: transparent;
47 | }
48 |
49 | ::-webkit-scrollbar-thumb {
50 | background-color: var(--color-fg);
51 | border-radius: 2px;
52 | }
53 |
--------------------------------------------------------------------------------
/dist/www/assets/styles/ui/base/WindowControls.css:
--------------------------------------------------------------------------------
1 | .window-controls {
2 | position: fixed;
3 | top: 56px;
4 | right: 56px;
5 | z-index: 5;
6 | display: flex;
7 | gap: 8px;
8 | opacity: 1;
9 | transition: ease .2s;
10 | }
11 |
12 | html.idle .window-controls {
13 | opacity: 0;
14 | }
15 |
16 | .window-controls * {
17 | width: 24px;
18 | height: 24px;
19 | color: var(--color-fg);
20 | cursor: default;
21 | }
22 |
23 | #fullscreen,
24 | #minimize,
25 | #close {
26 | display: none;
27 | }
28 |
29 | html.supports-fullscreen #fullscreen {
30 | display: block;
31 | }
32 |
33 | html.widget-mode .window-controls,
34 | html.non-interactive .window-controls {
35 | display: none;
36 | }
37 |
38 | html.no-show-lyrics #fullscreen {
39 | display: none;
40 | }
41 |
42 | /* iPad */
43 | @media only screen and (max-width: 1024px) {
44 | .window-controls {
45 | top: 40px;
46 | right: 40px;
47 | }
48 | }
49 |
50 | /* iPhone */
51 | @media only screen and (max-width: 812px) {
52 | .window-controls {
53 | top: 24px;
54 | right: 24px;
55 | }
56 | }
--------------------------------------------------------------------------------
/dist/www/assets/styles/ui/sunamu/AlbumArt.css:
--------------------------------------------------------------------------------
1 | #album-art {
2 | flex-shrink: 0;
3 | display: inline-block;
4 | height: 232px;
5 | background-image: var(--cover-art-url);
6 | background-position: center;
7 | background-repeat: no-repeat;
8 | background-size: cover;
9 | border-radius: 16px;
10 | box-shadow:
11 | 0px 76px 72px rgba(0, 0, 0, 0.07),
12 | 0px 38.0371px 36.0352px rgba(0, 0, 0, 0.0532233),
13 | 0px 22.9118px 21.7059px rgba(0, 0, 0, 0.0456112),
14 | 0px 14.6831px 13.9103px rgba(0, 0, 0, 0.0399626),
15 | 0px 9.51638px 9.01552px rgba(0, 0, 0, 0.035),
16 | 0px 5.99045px 5.67517px rgba(0, 0, 0, 0.0300374),
17 | 0px 3.44189px 3.26074px rgba(0, 0, 0, 0.0243888),
18 | 0px 1.51486px 1.43513px rgba(0, 0, 0, 0.0167767);
19 | aspect-ratio: 1;
20 | transition: ease .5s background-image,
21 | ease .2s;
22 | }
23 |
24 | html.no-album-art #album-art {
25 | display: none;
26 | }
27 |
28 | html.idle #album-art {
29 | height: 88px;
30 | }
31 |
32 | /* iPad */
33 | @media only screen and (max-width: 1024px) {
34 | #album-art {
35 | height: 160px;
36 | border-radius: 12px;
37 | }
38 |
39 | html.idle #album-art {
40 | height: 72px;
41 | }
42 | }
43 |
44 | /* iPhone */
45 | @media only screen and (max-width: 812px) {
46 | #album-art {
47 | height: 96px;
48 | border-radius: 8px;
49 | }
50 |
51 | html.idle #album-art {
52 | height: 40px;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/dist/www/assets/styles/ui/sunamu/Background.css:
--------------------------------------------------------------------------------
1 | .background {
2 | position: fixed;
3 | top: -12.5vh;
4 | left: -12.5vw;
5 | width: 125vw;
6 | height: 125vh;
7 | z-index: -2;
8 |
9 | background-blend-mode: multiply;
10 | background-position: center;
11 | background-repeat: no-repeat;
12 | background-size: cover;
13 | filter: none;
14 | isolation: isolate;
15 |
16 | transition: ease 2s background-image,
17 | ease 2s background-color;
18 |
19 | }
20 |
21 | html:not(.colorblock):not(.widget-mode) .background::before,
22 | html:not(.colorblock):not(.widget-mode) .background::after {
23 | content: "";
24 |
25 | position: absolute;
26 | width: 200vmax;
27 | height: 200vmax;
28 | z-index: -1;
29 | aspect-ratio: 1;
30 |
31 | background-image: var(--cover-art-url);
32 | background-color: var(--color-bg);
33 | background-blend-mode: multiply;
34 | background-position: center;
35 | background-repeat: no-repeat;
36 | background-size: cover;
37 |
38 | filter: blur(180px) brightness(50%) saturate(150%);
39 | mix-blend-mode: exclusion;
40 |
41 | animation: bgAnim 60s ease-in-out infinite;
42 | transition: ease 2s background-image,
43 | ease 2s background-color;
44 | }
45 |
46 | html:not(.colorblock):not(.widget-mode) .background::before {
47 | top: 0;
48 | left: 0;
49 | }
50 |
51 | html:not(.colorblock):not(.widget-mode) .background::after {
52 | bottom: 0;
53 | right: 0;
54 |
55 | animation-direction: reverse;
56 | animation-delay: 10s;
57 | }
58 |
59 | html.no-bg-animation .background {
60 | background-color: var(--color-bg);
61 | background-image: var(--cover-art-url);
62 | filter: blur(64px) brightness(50%) saturate(150%);
63 | }
64 |
65 | html.colorblock .background {
66 | background-color: var(--color-bg);
67 | background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMDAiIGhlaWdodD0iMzAwIj48ZmlsdGVyIGlkPSJhIiB4PSIwIiB5PSIwIj48ZmVUdXJidWxlbmNlIHR5cGU9ImZyYWN0YWxOb2lzZSIgYmFzZUZyZXF1ZW5jeT0iLjc1IiBzdGl0Y2hUaWxlcz0ic3RpdGNoIi8+PGZlQ29sb3JNYXRyaXggdHlwZT0ic2F0dXJhdGUiIHZhbHVlcz0iMCIvPjwvZmlsdGVyPjxwYXRoIGZpbHRlcj0idXJsKCNhKSIgb3BhY2l0eT0iLjA1IiBkPSJNMCAwaDMwMHYzMDBIMHoiLz48L3N2Zz4=");
68 | background-blend-mode: overlay;
69 | background-repeat: repeat;
70 | background-size: contain;
71 | filter: none;
72 | }
73 |
74 | html.widget-mode .background {
75 | display: none;
76 | }
77 |
78 | @keyframes bgAnim {
79 | 0% {
80 | transform: rotate(0deg);
81 | }
82 |
83 | 100% {
84 | transform: rotate(360deg);
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/dist/www/assets/styles/ui/sunamu/Info.css:
--------------------------------------------------------------------------------
1 | .info-container {
2 | width: 100%;
3 | display: flex;
4 | gap: 24px;
5 | margin: 8px 0;
6 | transition: ease .2s;
7 | }
8 |
9 | html.idle .info-container {
10 | margin: 0;
11 | }
12 |
13 | html.no-info-container .info-container {
14 | display: none;
15 | }
16 |
17 | /* iPad */
18 | @media only screen and (max-width: 1024px) {
19 | .info-container {
20 | gap: 16px;
21 | }
22 | }
23 |
24 | /* iPhone */
25 | @media only screen and (max-width: 812px) {
26 | .info-container {
27 | gap: 8px;
28 | }
29 | }
--------------------------------------------------------------------------------
/dist/www/assets/styles/ui/sunamu/Metadata.css:
--------------------------------------------------------------------------------
1 | #metadata {
2 | height: 232px;
3 | width: 100%;
4 | color: var(--color-fg);
5 | display: flex;
6 | flex-direction: column;
7 | justify-content: center;
8 | overflow: hidden;
9 | transition: ease .2s;
10 | }
11 |
12 | html.idle #metadata {
13 | height: 88px;
14 | }
15 |
16 | html.idle:not(.no-lyrics) #metadata {
17 | width: 40%;
18 | }
19 |
20 | #metadata span {
21 | overflow-x: clip;
22 | overflow-y: visible;
23 | white-space: nowrap;
24 | text-overflow: ellipsis;
25 | transition: ease .05s;
26 | }
27 |
28 | #title {
29 | font-weight: 700;
30 | font-size: 64px;
31 | opacity: 1;
32 | }
33 |
34 | html.idle #title {
35 | font-size: 48px;
36 | }
37 |
38 | #artist {
39 | font-weight: 500;
40 | font-size: 48px;
41 | opacity: 0.7;
42 | }
43 |
44 | html.idle #artist {
45 | font-size: 24px;
46 | }
47 |
48 | #album {
49 | font-weight: 500;
50 | font-size: 36px;
51 | opacity: 0.7;
52 | }
53 |
54 | #play-count-info {
55 | font-weight: 500;
56 | font-size: 24px;
57 | opacity: 0.5;
58 | }
59 |
60 | html.idle :is(#album, #play-count-info) {
61 | font-size: 0;
62 | opacity: 0;
63 | }
64 |
65 | html.no-show-play-count-info #play-count-info {
66 | display: none;
67 | }
68 |
69 | #metadata .featuring {
70 | opacity: .8;
71 | }
72 |
73 | html.widget-mode #metadata > * {
74 | text-shadow: 0 2px 8px rgba(0, 0, 0, .75);
75 | }
76 |
77 | /* iPad */
78 | @media only screen and (max-width: 1024px) {
79 | #metadata {
80 | height: 160px;
81 | }
82 |
83 | html.idle #metadata {
84 | height: 72px;
85 | }
86 |
87 | #title {
88 | font-size: 48px;
89 | }
90 |
91 | #artist {
92 | font-size: 36px;
93 | }
94 |
95 | #album {
96 | font-size: 24px;
97 | }
98 |
99 | #play-count-info {
100 | font-size: 16px;
101 | }
102 |
103 | html.idle #title {
104 | font-size: 36px;
105 | }
106 |
107 | html.idle #artist {
108 | font-size: 24px;
109 | }
110 | }
111 |
112 | /* iPhone */
113 | @media only screen and (max-width: 812px) {
114 | #metadata {
115 | height: 96px;
116 | }
117 |
118 | html.idle #metadata {
119 | height: 40px;
120 | }
121 |
122 | #title {
123 | font-size: 24px;
124 | }
125 |
126 | #artist {
127 | font-size: 20px;
128 | }
129 |
130 | #album {
131 | font-size: 16px;
132 | }
133 |
134 | #play-count-info {
135 | font-size: 12px;
136 | }
137 |
138 | html.idle #title {
139 | font-size: 20px;
140 | }
141 |
142 | html.idle #artist {
143 | font-size: 16px;
144 | }
145 | }
--------------------------------------------------------------------------------
/dist/www/assets/styles/ui/sunamu/NowPlaying.css:
--------------------------------------------------------------------------------
1 | #now-playing {
2 | box-sizing: border-box;
3 | position: absolute;
4 | bottom: 0;
5 | left: 0;
6 | z-index: 1;
7 | display: flex;
8 | flex-direction: column;
9 | align-items: flex-start;
10 | justify-content: flex-end;
11 | width: 100%;
12 | padding: 56px;
13 | }
14 |
15 | #now-playing * {
16 | z-index: 1;
17 | }
18 |
19 | #now-playing::before {
20 | content: "";
21 | content-visibility: none;
22 | position: absolute;
23 | bottom: 0;
24 | left: 0;
25 | z-index: 0;
26 | width: 100%;
27 | height: 100%;
28 | opacity: 0;
29 | background-color: var(--color-bg);
30 | --mask-image: linear-gradient(
31 | 180deg,
32 | rgba(0,0,0,0) 0%,
33 | rgba(0,0,0,1) 75%
34 | );
35 | mask-image: var(--mask-image);
36 | -webkit-mask-image: var(--mask-image);
37 | transition: ease .2s;
38 | }
39 |
40 | html:not(.idle):not(.widget-mode) #now-playing::before {
41 | opacity: 0.75;
42 | }
43 |
44 | /* iPad */
45 | @media only screen and (max-width: 1024px) {
46 | #now-playing {
47 | padding: 40px;
48 | }
49 | }
50 |
51 | /* iPhone */
52 | @media only screen and (max-width: 812px) {
53 | #now-playing {
54 | padding: 24px;
55 | }
56 | }
--------------------------------------------------------------------------------
/dist/www/assets/styles/ui/sunamu/PlaybackControls.css:
--------------------------------------------------------------------------------
1 | html:is(.no-controls, .non-interactive, .not-playing) #playback-controls {
2 | display: none;
3 | }
4 |
5 | html.no-show-lyrics #lyrics-btn {
6 | display: none;
7 | }
8 |
9 | html.no-extra-buttons :is(#spotify, #lastfm) {
10 | display: none;
11 | }
12 |
13 | #playback-controls {
14 | display: flex;
15 | flex-direction: row;
16 | align-items: center;
17 | justify-content: center;
18 | width: 100%;
19 | height: 40px;
20 | margin: 8px 0;
21 | gap: 16px;
22 | transition: ease .2s;
23 | }
24 |
25 | html.idle #playback-controls {
26 | opacity: 0;
27 | margin: 0;
28 | height: 0;
29 | }
30 |
31 | #playback-controls svg {
32 | width: 24px;
33 | height: 24px;
34 | aspect-ratio: 1;
35 | padding: 4px;
36 | border-radius: 24px;
37 | color: var(--color-fg);
38 | background-color: transparent;
39 | transition: ease .2s;
40 | pointer-events: all;
41 | }
42 |
43 | #playback-controls #playpause {
44 | padding: 8px;
45 | }
46 |
47 | #playback-controls svg:hover {
48 | color: var(--color-bg);
49 | background-color: var(--color-fg);
50 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
51 | }
52 |
53 | #playback-controls svg:active {
54 | color: var(--color-bg);
55 | background-color: var(--color-fg);
56 | filter: saturate(2);
57 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
58 | }
59 |
60 | #playback-controls svg.active {
61 | filter: saturate(2);
62 | }
63 |
64 | #playback-controls svg.active:hover {
65 | filter: saturate(3);
66 | }
67 |
68 | #playback-controls svg.disabled {
69 | opacity: 0.5;
70 | filter: saturate(.5);
71 | pointer-events: none;
72 | }
73 |
--------------------------------------------------------------------------------
/dist/www/assets/styles/ui/sunamu/PlayingIndicator.css:
--------------------------------------------------------------------------------
1 | html:is(.no-playing-indicator, .widget-mode) .playing-indicator {
2 | display: none;
3 | }
4 |
5 | html.no-player-icon #player-icon {
6 | display: none;
7 | }
8 |
9 | .playing-indicator {
10 | position: absolute;
11 | top: 56px;
12 | left: 56px;
13 | display: flex;
14 | gap: 8px;
15 | align-items: center;
16 | justify-items: center;
17 | transition: ease .2s;
18 | }
19 |
20 | html:is(.idle, .not-playing) .playing-indicator {
21 | opacity: 0;
22 | }
23 |
24 | .playing-indicator .text {
25 | display: flex;
26 | flex-direction: column;
27 | align-items: flex-start;
28 | }
29 |
30 | #player-icon {
31 | width: 72px;
32 | aspect-ratio: 1;
33 | background-color: var(--color-fg);
34 | opacity: 1;
35 | mask-size: cover;
36 | -webkit-mask-size: cover;
37 | mask-image: var(--app-icon);
38 | -webkit-mask-image: var(--app-icon);
39 | }
40 |
41 | #player-playing {
42 | color: var(--color-fg);
43 | opacity: 0.5;
44 | text-transform: uppercase;
45 | font-weight: 500;
46 | font-size: 24px;
47 | }
48 |
49 | #player-name {
50 | color: var(--color-fg);
51 | font-weight: bold;
52 | font-size: 36px;
53 | }
54 |
55 | /* iPad */
56 | @media only screen and (max-width: 1024px) {
57 | .playing-indicator {
58 | top: 40px;
59 | left: 40px;
60 | }
61 |
62 | #player-icon {
63 | width: 48px;
64 | }
65 |
66 | #player-playing {
67 | font-size: 16px;
68 | }
69 |
70 | #player-name {
71 | font-size: 24px;
72 | }
73 | }
74 |
75 | /* iPhone */
76 | @media only screen and (max-width: 812px) {
77 | .playing-indicator {
78 | top: 24px;
79 | left: 24px;
80 | }
81 |
82 | #player-icon {
83 | width: 24px;
84 | }
85 |
86 | #player-playing {
87 | font-size: 12px;
88 | }
89 |
90 | #player-name {
91 | font-size: 16px;
92 | }
93 | }
94 |
95 | @media only screen and (orientation: portrait) {
96 | .playing-indicator {
97 | width: 100vw;
98 | left: 0;
99 | justify-content: center;
100 | }
101 |
102 | .playing-indicator .text {
103 | align-items: center;
104 | }
105 |
106 | #player-icon {
107 | display: none;
108 | }
109 | }
--------------------------------------------------------------------------------
/dist/www/assets/styles/ui/sunamu/Seekbar.css:
--------------------------------------------------------------------------------
1 | html:is(.no-progress, .not-playing, .not-reporting-position) .seekbar-container {
2 | display: none;
3 | }
4 |
5 | html.non-interactive #seekbar {
6 | pointer-events: none;
7 | }
8 |
9 | .seekbar-container {
10 | width: 100%;
11 | height: 12px;
12 | display: flex;
13 | align-items: center;
14 | justify-content: center;
15 | margin: 8px 0;
16 | gap: 8px;
17 | transition: ease .2s;
18 | }
19 |
20 | html.idle .seekbar-container {
21 | height: 0;
22 | opacity: 0;
23 | margin: 0;
24 | }
25 |
26 | #time-elapsed, #time-total {
27 | font-size: 16px;
28 | width: 48px;
29 | color: var(--color-fg);
30 | text-align: center;
31 | }
32 |
33 | #seekbar {
34 | position: relative;
35 | width: 100%;
36 | height: 8px;
37 | pointer-events: none;
38 | }
39 |
40 | #seekbar-bg {
41 | position: absolute;
42 | width: 100%;
43 | height: 100%;
44 | z-index: 0;
45 | border-radius: 4px;
46 | background-color: var(--color-fg);
47 | opacity: 0.5;
48 | box-shadow: 0 0px 0px rgba(0, 0, 0, 0.3), 0 5px 16px rgba(0, 0, 0, 0.2);
49 | }
50 |
51 | #seekbar.draggable #seekbar-bg {
52 | pointer-events: auto;
53 | }
54 |
55 | #seekbar-fg {
56 | position: absolute;
57 | border-radius: 4px;
58 | background-color: var(--color-fg);
59 | opacity: 0.5;
60 | height: 100%;
61 | transition: ease .2s;
62 | min-width: 4px;
63 | }
64 |
65 | #seekbar-ball {
66 | position: absolute;
67 | top: -4px;
68 | background-color: var(--color-fg);
69 | opacity: 0;
70 | width: 0;
71 | height: 0;
72 | border-radius: 8px;
73 | width: 16px;
74 | height: 16px;
75 | transform: translateX(-8px) scale(0);
76 | transition: ease .2s;
77 | }
78 |
79 | #seekbar:hover #seekbar-fg,
80 | #seekbar:hover #seekbar-ball {
81 | opacity: 1;
82 | }
83 |
84 | #seekbar:hover #seekbar-ball {
85 | transform: translateX(-8px) scale(1);
86 | }
87 |
88 | #seekbar-fg.dragging {
89 | transition: none;
90 | }
91 |
92 | /* iPad */
93 | @media only screen and (max-width: 1024px) {
94 | #time-elapsed, #time-total {
95 | font-size: 12px;
96 | width: 36px;
97 | }
98 | }
99 |
100 | /* iPhone */
101 | @media only screen and (max-width: 812px) {
102 | #time-elapsed, #time-total {
103 | font-size: 12px;
104 | width: 36px;
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/dist/www/assets/styles/ui/sunamu/Sunamu.css:
--------------------------------------------------------------------------------
1 | @import url("Variables.css");
2 | @import url("Background.css");
3 | @import url("AlbumArt.css");
4 | @import url("Info.css");
5 | @import url("Metadata.css");
6 | @import url("Seekbar.css");
7 | @import url("Lyrics.css");
8 | @import url("PlayingIndicator.css");
9 | @import url("PlaybackControls.css");
10 | @import url("NowPlaying.css");
11 |
12 | h1, h2, h3, h4, h5, h6, span, p {
13 | font-family: var(--font-family);
14 | }
15 |
--------------------------------------------------------------------------------
/dist/www/assets/styles/ui/sunamu/Variables.css:
--------------------------------------------------------------------------------
1 | :root {
2 | /* Traverse to the root and back because FUCKING WEBKIT I HATE YOU APPLE */
3 | --cover-art-url: url("../../../../assets/images/no_song.svg");
4 | --app-icon: url("../../../../assets/images/apps/default.svg");
5 |
6 | --font-family: "Urbanist";
7 |
8 | --color-muted: rgb(2, 117, 151);
9 | --color-vibrant: rgb(4, 196, 252);
10 |
11 | --color-light-muted: rgb(3, 148, 150);
12 | --color-light-vibrant: rgb(123, 251, 252);
13 |
14 | --color-dark-muted: rgb(2, 117, 151);
15 | --color-dark-vibrant: rgb(2, 102, 131);
16 |
17 | --color-fg: var(--color-light-vibrant);
18 | --color-bg: var(--color-dark-vibrant);
19 | }
20 |
21 | html:is(.no-colors, .no-palette, .not-playing) {
22 | --color-fg: white;
23 | --color-bg: rgba(0, 0, 0, 0.5);
24 | }
25 |
26 | html.inverted-default-colors:is(.no-colors, .no-palette, .not-playing) {
27 | --color-fg: black;
28 | --color-bg: rgba(255, 255, 255, 0.5);
29 | }
30 |
31 | html.colorblock:is(.not-playing) {
32 | --color-bg: var(--color-dark-vibrant);
33 | }
34 |
--------------------------------------------------------------------------------
/dist/www/index.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
26 |
29 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
74 |
75 |
76 |
77 |
80 |
83 |
86 |
89 |
92 |
95 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
--------------------------------------------------------------------------------
/dist/www/sunamu.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "background_color": "#009ccc",
3 | "description": "Beautiful Now Playing widget for the desktop, the web and streaming software",
4 | "display": "fullscreen",
5 | "icons": [
6 | {
7 | "src": "/assets/favicon/android-chrome-192x192.png",
8 | "sizes": "192x192",
9 | "type": "image/png"
10 | },
11 | {
12 | "src": "/assets/favicon/android-chrome-512x512.png",
13 | "sizes": "512x512",
14 | "type": "image/png"
15 | }
16 | ],
17 | "name": "Sunamu",
18 | "short_name": "Sunamu",
19 | "start_url": "/index.htm"
20 | }
--------------------------------------------------------------------------------
/electron-builder.yml:
--------------------------------------------------------------------------------
1 | appId: xyz.nyaomi.sunamu
2 | productName: Sunamu
3 | copyright: Copyright © 2021-2023 Naomi Calabretta
4 | directories:
5 | output: targets
6 | files:
7 | - "**/*"
8 | - "!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}"
9 | - "!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}"
10 | - "!**/node_modules/*.d.ts"
11 | - "!**/node_modules/.bin"
12 | - "!**/*.{iml,o,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,xproj}"
13 | - "!.editorconfig"
14 | - "!**/._*"
15 | - "!**/{.DS_Store,.git,.hg,.svn,CVS,RCS,SCCS,.gitignore,.gitattributes}"
16 | - "!**/{__pycache__,thumbs.db,.flowconfig,.idea,.vs,.nyc_output}"
17 | - "!**/{appveyor.yml,.travis.yml,circle.yml}"
18 | - "!**/{npm-debug.log,yarn.lock,.yarn-integrity,.yarn-metadata.json}"
19 | - "!src/*"
20 | - "!.eslintrc.json"
21 | - "!test.js"
22 | - "!README.md"
23 | - "!tsconfig.json"
24 | - "!tsconfig.settings.json"
25 | - "!assets/{preview*.png,kwin-widget-rule.kwinrule}"
26 | - "!aur/*"
27 | - "!.github/*"
28 | - "!.vscode/*"
29 | - "!.env"
30 | - "!.nvmrc"
31 | asar: true
32 | asarUnpack:
33 | - "./node_modules/sharp/vendor/**/*"
34 | - "./node_modules/sharp/build/**/*"
35 | - "./node_modules/register-scheme/build/**/*"
36 | # Linux specific
37 | - "./node_modules/abstract-socket/build/**/*"
38 | # Windows specific
39 | - "./node_modules/windowtoolbox/build/**/*"
40 | - "./node_modules/winplayer-node/build/**/*"
41 | linux:
42 | target:
43 | - AppImage
44 | maintainer: Naomi Calabretta
45 | vendor: Naomi Calabretta
46 | executableName: Sunamu
47 | icon: assets/icons
48 | synopsis: Beautiful app to show which music you are playing
49 | description: Show your currently playing song in a stylish way!
50 | category: Multimedia
51 | desktop: Sunamu
52 | publish:
53 | - github
54 | deb:
55 | depends:
56 | - libgtk-3-0
57 | - libnotify4
58 | - libxss1
59 | - libxtst6
60 | - libnss3
61 | - libatspi2.0-0
62 | - libuuid1
63 | - libvips
64 | rpm:
65 | depends:
66 | - /bin/sh
67 | - gtk3
68 | - libnotify
69 | - libXScrnSaver
70 | - libXtst
71 | - nss
72 | - at-spi2-core
73 | - libuuid
74 | - vips
75 | fpm: ["--rpm-rpmbuild-define=_build_id_links none"]
76 | pacman:
77 | depends:
78 | - gtk3
79 | - nss
80 | - libvips
81 | - c-ares
82 | - libxslt
83 | - libevent
84 | - minizip
85 | - re2
86 | - snappy
87 | flatpak:
88 | license: "LICENSE"
89 | runtime: "org.freedesktop.Platform"
90 | runtimeVersion: "22.08"
91 | sdk: "org.freedesktop.Sdk"
92 | base: "org.electronjs.Electron2.BaseApp"
93 | baseVersion: "22.08"
94 | branch: "master"
95 | useWaylandFlags: false # Configurable in Sunamu Config
96 | finishArgs:
97 | # Wayland/X11 Rendering
98 | - "--socket=wayland"
99 | - "--socket=x11"
100 | - "--share=ipc"
101 | # OpenGL
102 | - "--device=dri"
103 | # Directory access
104 | # Discord IPC
105 | - "--filesystem=xdg-run/discord-ipc-0"
106 | - "--filesystem=xdg-run/discord-ipc-1"
107 | - "--filesystem=xdg-run/discord-ipc-2"
108 | - "--filesystem=xdg-run/discord-ipc-3"
109 | - "--filesystem=xdg-run/discord-ipc-4"
110 | - "--filesystem=xdg-run/discord-ipc-5"
111 | - "--filesystem=xdg-run/discord-ipc-6"
112 | - "--filesystem=xdg-run/discord-ipc-7"
113 | - "--filesystem=xdg-run/discord-ipc-8"
114 | - "--filesystem=xdg-run/discord-ipc-9"
115 | # Discord IPC but Flatpak
116 | - "--filesystem=xdg-run/app/com.discordapp.Discord/discord-ipc-0"
117 | - "--filesystem=xdg-run/app/com.discordapp.Discord/discord-ipc-1"
118 | - "--filesystem=xdg-run/app/com.discordapp.Discord/discord-ipc-2"
119 | - "--filesystem=xdg-run/app/com.discordapp.Discord/discord-ipc-3"
120 | - "--filesystem=xdg-run/app/com.discordapp.Discord/discord-ipc-4"
121 | - "--filesystem=xdg-run/app/com.discordapp.Discord/discord-ipc-5"
122 | - "--filesystem=xdg-run/app/com.discordapp.Discord/discord-ipc-6"
123 | - "--filesystem=xdg-run/app/com.discordapp.Discord/discord-ipc-7"
124 | - "--filesystem=xdg-run/app/com.discordapp.Discord/discord-ipc-8"
125 | - "--filesystem=xdg-run/app/com.discordapp.Discord/discord-ipc-9"
126 | # Cover art locations for Clementine, Strawberry, etc.
127 | - "--filesystem=/tmp:ro"
128 | # Cover art locations for VLC media player, Lollypop, etc.
129 | - "--filesystem=xdg-cache:ro"
130 | # Cover art locations for Clementine, Strawberry, etc. on Flatpak
131 | - "--filesystem=xdg-run/app:ro"
132 | # Cover art locations for GNOME Music on Flatpak, and possibly more
133 | - "--filesystem=~/.var/app:ro"
134 | # Restore RW for sunamu's Flatpak folders
135 | - "--filesystem=xdg-run/app/xyz.nyaomi.sunamu"
136 | - "--filesystem=~/.var/app/xyz.nyaomi.sunamu"
137 | # Allow communication with network
138 | - "--share=network"
139 | # Talk to media players
140 | - "--talk-name=org.mpris.MediaPlayer2.*"
141 | win:
142 | target:
143 | - nsis
144 | icon: assets/icons/icon.ico
145 | legalTrademarks: Copyright © 2021-2023 Naomi Calabretta
146 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sunamu",
3 | "description": "Beautiful Now Playing widget for the desktop, the web and streaming software",
4 | "version": "2.2.0",
5 | "main": "dist/main/index.js",
6 | "license": "MPL-2.0",
7 | "author": {
8 | "email": "me@nyaomi.xyz",
9 | "name": "Naomi Calabretta"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "https://github.com/NyaomiDEV/Sunamu.git"
14 | },
15 | "scripts": {
16 | "lint": "eslint src/ --ext .ts",
17 | "compile": "tsc -b",
18 | "clean": "rm dist/types*; rm -rf dist/main; rm -rf dist/www/lib; rm -rf dist/www/index.{js,d.ts,d.ts.map}; rm -rf targets",
19 | "install-deps": "electron-builder install-app-deps; yarn run build:prepare:sharp",
20 | "install-deps:node": "npm rebuild; yarn run build:prepare:sharp",
21 | "test": "yarn run compile && electron . --sunamu-debug",
22 | "test:node": "yarn run compile && node . --sunamu-debug",
23 | "dev": "yarn run test",
24 | "dev:node": "yarn run test:node",
25 | "start": "yarn run run",
26 | "start:node": "yarn run run:node",
27 | "run": "yarn run compile && electron .",
28 | "run:node": "yarn run compile && node .",
29 | "build:prepare:sharp": "cd node_modules/sharp && rm -rf vendor && rm -rf build; SHARP_IGNORE_GLOBAL_LIBVIPS=1 yarn run install",
30 | "build": "yarn run clean && yarn run compile && electron-builder"
31 | },
32 | "devDependencies": {
33 | "@types/jsdom": "^21.1.6",
34 | "@types/mime": "^3.0.4",
35 | "@types/node": "^20.11.4",
36 | "@types/node-static": "^0.7.11",
37 | "@types/obs-studio": "^2.17.2",
38 | "@typescript-eslint/eslint-plugin": "^6.19.0",
39 | "@typescript-eslint/parser": "^6.19.0",
40 | "electron": "^28.1.3",
41 | "electron-builder": "^24.9.1",
42 | "eslint": "^8.56.0",
43 | "socket.io-client": "^4.7.4",
44 | "typescript": "^5.3.3"
45 | },
46 | "dependencies": {
47 | "@xhayper/discord-rpc": "^1.1.2",
48 | "axios": "^1.6.5",
49 | "electron-window-state": "^5.0.3",
50 | "golden-fleece": "^1.0.9",
51 | "jsdom": "^23.2.0",
52 | "json5": "^2.2.3",
53 | "mime": "^3.0.0",
54 | "node-static": "^0.7.11",
55 | "node-vibrant": "^3.2.1-alpha.1",
56 | "sharp": "^0.33.2",
57 | "socket.io": "^4.7.4",
58 | "yargs": "^17.7.2"
59 | },
60 | "optionalDependencies": {
61 | "mpris-for-dummies": "NyaomiDEV/mpris-for-dummies",
62 | "windowtoolbox": "NyaomiDEV/windowtoolbox",
63 | "winplayer-rs": "NyaomiDEV/WinPlayer-Node"
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/main/appStatus.ts:
--------------------------------------------------------------------------------
1 | import yargs from "yargs";
2 | import { hideBin } from "yargs/helpers";
3 | import { get as getConfig } from "./config";
4 |
5 | export const argv = yargs(hideBin(process.argv)).argv;
6 |
7 | export const widgetModeElectron = checkFunctionality(getConfig("scenes").electron.widgetMode, "widget-electron");
8 | export const widgetMode = checkFunctionality(getConfig("scenes").default.widgetMode, "widget");
9 | export const debugMode = checkFunctionality(getConfig("debugMode"), "sunamu-debug");
10 | export const devTools = checkFunctionality(getConfig("devToolsAtStartup"), "sunamu-devtools");
11 | export const consolidateConfig = checkFunctionality(false, "consolidate-config");
12 | export const lyricsActive = checkFunctionality(getConfig("lyricsActive"), "lyrics");
13 |
14 | export const useElectron = checkFunctionality(getConfig("useElectron"), "electron");
15 | export const useWebserver = checkFunctionality(getConfig("useWebserver"), "webserver");
16 |
17 | export function checkFunctionality(configBoolean: boolean, name: string): boolean {
18 | return checkSwitch(argv[name.toLowerCase()]) ?? checkSwitch(process.env[name.replace(/-/g, "_").toUpperCase()]) ?? configBoolean;
19 | }
20 |
21 | export function checkSwitch(str?: string | boolean): boolean | undefined {
22 | if(typeof str === "boolean") return str;
23 |
24 | if (!str)
25 | return undefined;
26 |
27 | switch (str?.toLowerCase().trim()) {
28 | case "true":
29 | case "yes":
30 | case "1":
31 | return true;
32 | case "false":
33 | case "no":
34 | case "0":
35 | case null:
36 | case undefined:
37 | return false;
38 | }
39 |
40 | return Boolean(str);
41 | }
42 |
--------------------------------------------------------------------------------
/src/main/config.ts:
--------------------------------------------------------------------------------
1 | import { Config } from "../types";
2 | import { evaluate, patch } from "golden-fleece";
3 | import { copyFileSync, mkdirSync, readFileSync, watchFile, writeFileSync } from "fs";
4 | import { resolve } from "path";
5 | import { getAppData } from "./util";
6 | import EventEmitter from "events";
7 |
8 | const configPath = resolve(getAppData(), "sunamu", "config.json5");
9 | const defaultConfigPath = resolve(__dirname, "..", "..", "assets", "config.json5");
10 |
11 | const defaultConfig: Config = evaluate(readFileSync(defaultConfigPath, "utf8"));
12 | let config: Config = getUserConfig();
13 |
14 | const emitter = new EventEmitter();
15 | export { emitter as default };
16 |
17 | if (compare(config, defaultConfig, true))
18 | save();
19 |
20 | watchFile(configPath, () => {
21 | const _config = getUserConfig();
22 |
23 | if (compare(config, _config, false)){
24 | config = _config;
25 | emitter.emit("configChanged");
26 | }
27 |
28 | });
29 |
30 | function getUserConfig() {
31 | try {
32 | return evaluate(readFileSync(configPath, "utf8"));
33 | } catch (_) {
34 | save(false, defaultConfig);
35 | return defaultConfig;
36 | }
37 | }
38 |
39 | function compare(a: any, b: any, update: boolean = false): boolean {
40 | let changed = false;
41 | for (const key in b) {
42 | if (typeof b[key] !== typeof a[key] || Array.isArray(b[key]) !== Array.isArray(a[key])) {
43 | if(update)
44 | a[key] = b[key];
45 | changed = true;
46 | continue;
47 | }
48 |
49 | if (typeof b[key] === "object" && !Array.isArray(b[key]))
50 | changed = compare(a[key], b[key], update) || changed;
51 | }
52 |
53 | return changed;
54 | }
55 |
56 | export function save(backup: boolean = true, configToSave = config) {
57 | mkdirSync(resolve(configPath, ".."), { recursive: true });
58 | if(backup){
59 | const now = new Date();
60 | const date = now.getFullYear().toString().padStart(4, "0") +
61 | now.getMonth().toString().padStart(2, "0") +
62 | now.getDay().toString().padStart(2, "0") + "-" +
63 | now.getHours().toString().padStart(2, "0") +
64 | now.getMinutes().toString().padStart(2, "0") +
65 | now.getSeconds().toString().padStart(2, "0");
66 | copyFileSync(configPath, configPath + ".backup-" + date);
67 | }
68 | try{
69 | writeFileSync(configPath, patch(readFileSync(configPath, "utf8"), configToSave));
70 | }catch(_){
71 | writeFileSync(configPath, patch(readFileSync(defaultConfigPath, "utf8"), configToSave));
72 | }
73 | }
74 |
75 | export function consolidateToDefaultConfig(){
76 | return writeFileSync(configPath, patch(readFileSync(defaultConfigPath, "utf8"), config));
77 | }
78 |
79 | export function get(name: string): T {
80 | return config[name];
81 | }
82 |
83 | export function getAll(): Config {
84 | return Object.assign({}, config);
85 | }
86 |
87 | export function set(name: string, value: T){
88 | config[name] = value;
89 | save(false);
90 | }
91 |
--------------------------------------------------------------------------------
/src/main/index.ts:
--------------------------------------------------------------------------------
1 | import getPlayer, { Player } from "./player";
2 | import playbackStatus, { updateInfo } from "./playbackStatus";
3 | import { consolidateConfig, lyricsActive, useElectron, useWebserver } from "./appStatus";
4 | import { updatePresence } from "./integrations/discordrpc";
5 | import { logTrack } from "./integrations/tracklogger";
6 | import Instance from "./instance";
7 | import { manageLyricsCache } from "./integrations/lyricsOffline";
8 | import { consolidateToDefaultConfig } from "./config";
9 | import { logToDebug } from "./logger";
10 | export { logToDebug as debug };
11 |
12 | process.title = "sunamu";
13 |
14 | let player: Player;
15 |
16 | async function main(): Promise {
17 | const instance = new Instance();
18 | const haveLock = await instance.requestLock();
19 |
20 | if(!haveLock){
21 | console.error("Another instance is running!");
22 | return process.exit(1); // for some reason I can't just process.exit because thanks Node on Windows
23 | }
24 |
25 | if(consolidateConfig)
26 | consolidateToDefaultConfig();
27 |
28 | let _useWebserver = useWebserver;
29 | player = await getPlayer();
30 | player.init(updateInfo);
31 |
32 | if(useElectron){
33 | logToDebug("Loading Electron");
34 | try{
35 | const Electron = await import("./electron");
36 | await Electron.default();
37 | }catch(_e){
38 | console.error("Electron is not available. Perhaps you are using vanilla Node?\nForcing Webserver!");
39 | _useWebserver = true;
40 | }
41 | }
42 |
43 | if(_useWebserver){
44 | logToDebug("Loading Webserver");
45 | const WebServer = await import("./webserver");
46 | await WebServer.default();
47 | }
48 |
49 | playbackStatus.on("songdata", async (_songdata?, metadataChanged?) => {
50 | updatePresence();
51 | if(metadataChanged)
52 | logTrack();
53 | });
54 |
55 | // Lyrics cache management
56 | if(lyricsActive)
57 | await manageLyricsCache();
58 | }
59 |
60 | main();
--------------------------------------------------------------------------------
/src/main/instance.ts:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from "events";
2 | import { unlink } from "fs/promises";
3 | import { connect, createServer } from "net";
4 | import { tmpdir } from "os";
5 | import * as path from "path";
6 |
7 | import yargs from "yargs";
8 | import { hideBin } from "yargs/helpers";
9 |
10 | export default class Instance extends EventEmitter {
11 |
12 | private server = createServer();
13 |
14 | requestLock(): Promise {
15 | let lockPath;
16 | switch(process.platform){
17 | case "win32":
18 | lockPath = path.join("\\\\?\\pipe", tmpdir(), "SunamuInstance.lock");
19 | break;
20 | default:
21 | lockPath = path.resolve(tmpdir(), "SunamuInstance.lock");
22 | break;
23 | }
24 |
25 | return new Promise(resolve => {
26 | this.server.on("error", (e: { code: string }) => {
27 | if (e.code === "EADDRINUSE") {
28 | this.server.close();
29 | const sock = connect({ path: lockPath }, () => {
30 | sock.write(JSON.stringify(yargs(hideBin(process.argv)).argv), () => {
31 | sock.destroy();
32 | resolve(false);
33 | });
34 | });
35 | sock.on("error", (e: { code: string }) => {
36 | if (e.code === "ECONNREFUSED")
37 | unlink(lockPath).then(() => resolve(this.requestLock()));
38 | });
39 | }
40 | });
41 |
42 | this.server.on("listening", () => resolve(true));
43 |
44 | this.server.on("connection", _sock => {
45 | _sock.on("data", buf => {
46 | this.emit("second-instance", JSON.parse(buf.toString()));
47 | });
48 | });
49 |
50 | this.server.listen(lockPath);
51 | });
52 | }
53 |
54 | releaseLock(): Promise {
55 | return new Promise(resolve => {
56 | this.server.close(e => resolve(e ? false : true));
57 | });
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/main/integrations/discordrpc.ts:
--------------------------------------------------------------------------------
1 | import { Client, SetActivity } from "@xhayper/discord-rpc";
2 | import { DiscordPresenceConfig } from "../../types";
3 | import { debug } from "../";
4 | import { checkFunctionality } from "../appStatus";
5 | import configEmitter, { get as getConfig } from "../config";
6 | import { songdata } from "../playbackStatus";
7 | import { secondsToTime } from "../util";
8 |
9 | export let discordPresenceConfig: DiscordPresenceConfig = getPresenceConfig();
10 |
11 | const clientId = "908012408008736779";
12 | let rpc = new Client({clientId});
13 |
14 | rpc.on("connected", () => {
15 | debug("Discord RPC is connected");
16 | });
17 |
18 | rpc.on("disconnected", async () => {
19 | debug("Discord RPC was disconnected");
20 | });
21 |
22 | configEmitter.on("configChanged", async () => {
23 | discordPresenceConfig = getPresenceConfig();
24 | });
25 |
26 | function getPresenceConfig() {
27 | const settings: DiscordPresenceConfig = Object.assign({}, getConfig("discordRpc"));
28 | settings.enabled = checkFunctionality(settings.enabled, "discord-rpc");
29 | return settings;
30 | }
31 |
32 | const connect = (() => {
33 | let isConnecting = false;
34 |
35 | async function _connect(){
36 | if (rpc.isConnected) return;
37 | if (isConnecting) return;
38 | isConnecting = true;
39 |
40 | let error: boolean;
41 | do {
42 | error = false;
43 | try {
44 | debug("Discord RPC logging in");
45 | await rpc.connect();
46 | } catch (_e) {
47 | debug(_e);
48 | error = true;
49 | debug("Discord RPC errored out while logging in, waiting 5 seconds before retrying");
50 | await new Promise(resolve => setTimeout(resolve, 5000));
51 | }
52 | } while (error);
53 | isConnecting = false;
54 | }
55 |
56 | return _connect;
57 | })();
58 |
59 | export async function updatePresence() {
60 | if (!discordPresenceConfig.enabled){
61 | if(rpc.isConnected) rpc.destroy();
62 | return;
63 | }
64 |
65 | const presence = await getPresence();
66 |
67 | await connect();
68 | if (!rpc.isConnected) return; // failed
69 |
70 | if(!presence) {
71 | rpc.user?.clearActivity();
72 | return;
73 | }
74 |
75 | return rpc.user?.setActivity(presence);
76 | }
77 |
78 | async function getPresence() {
79 | if (!songdata || !songdata.metadata.id || discordPresenceConfig.blacklist.includes(songdata.appName))
80 | return;
81 |
82 | const activity: SetActivity = { // everything must be two characters long at least
83 | details: songdata.metadata.title ? `"${songdata.metadata.title}"` : "Unknown track",
84 | state: songdata.metadata.artist ? `By ${songdata.metadata.artist}` : undefined,
85 | largeImageText: songdata.metadata.album ? `"${songdata.metadata.album}"` : undefined,
86 | smallImageKey: songdata.status.toLowerCase(),
87 | smallImageText: `${songdata.status}` + (songdata.metadata.length > 0 ? ` (${secondsToTime(songdata.metadata.length)})` : ""),
88 | instance: false
89 | };
90 |
91 | if (songdata.status === "Playing" && songdata.elapsed.howMuch) {
92 | const now = Date.now();
93 | const start = Math.round(now - (songdata.elapsed.howMuch * 1000));
94 | const end = Math.round(start + (songdata.metadata.length * 1000));
95 | activity.startTimestamp = start;
96 | activity.endTimestamp = end;
97 | }
98 |
99 | if (songdata.spotify) {
100 | if (!activity.largeImageKey){
101 | const images = songdata.spotify.album?.images;
102 | if(images?.length)
103 | activity.largeImageKey = images[images?.length-1]?.url;
104 | }
105 |
106 | activity.buttons = [
107 | {
108 | label: "Listen on Spotify",
109 | url: songdata.spotify.external_urls.spotify
110 | }
111 | ];
112 | }
113 |
114 | if (songdata.lastfm) {
115 | if(!activity.largeImageKey){
116 | const images = songdata.lastfm.album?.image;
117 | if (images?.length)
118 | activity.largeImageKey = images[images?.length - 1]?.["#text"];
119 | }
120 |
121 | activity.buttons = [
122 | {
123 | label: "View on Last.fm",
124 | url: songdata.lastfm.url
125 | }
126 | ];
127 | }
128 |
129 | if (!activity.largeImageKey)
130 | activity.largeImageKey = "app_large";
131 |
132 | return activity;
133 | }
134 |
--------------------------------------------------------------------------------
/src/main/integrations/lyrics.ts:
--------------------------------------------------------------------------------
1 | import { debug } from "../";
2 | import { get as getLyrics, save as saveLyrics } from "./lyricsOffline";
3 |
4 | import * as Musixmatch from "../lyricproviders/musixmatch";
5 | import * as NetEase from "../lyricproviders/netease";
6 | import * as Genius from "../lyricproviders/genius";
7 | import * as Metadata from "../lyricproviders/metadata";
8 | import * as Local from "../lyricproviders/local";
9 | import type { Lyrics, Metadata as MetadataType } from "../../types";
10 | import { getAll as getConfig } from "../config";
11 |
12 | const providerList: any[] = [
13 | Musixmatch,
14 | NetEase,
15 | Genius,
16 | Metadata,
17 | Local
18 | ];
19 |
20 | for(let i = 0; i < providerList.length; i++){
21 | if(!providerList[i].supportedPlatforms.includes(process.platform))
22 | providerList.splice(i, 1);
23 | }
24 |
25 | export async function getAllLyrics(metadata: MetadataType): Promise {
26 | if (!metadata.artist || !metadata.title) // there can't be lyrics without at least those two fields
27 | return [];
28 |
29 | const configProviders = getConfig().lyricsProviders;
30 | const providerPromises = Object.keys(configProviders).map(x => configProviders[x] ? providerList.find(y => y.name === x)?.query(metadata) : undefined).filter(Boolean);
31 |
32 | return (await Promise.all(providerPromises)).filter(x => x?.lines.length);
33 | }
34 |
35 | export async function saveCustomLyrics(metadata: MetadataType, lyrics: Lyrics){
36 | const id = computeLyricsID(metadata);
37 | await saveLyrics(id, lyrics);
38 | debug("Saved custom lyrics from " + lyrics.provider);
39 | }
40 |
41 | export async function queryLyricsAutomatically(metadata: MetadataType): Promise {
42 | let lyrics: Lyrics | undefined;
43 | const id = computeLyricsID(metadata);
44 |
45 | const cached = await getLyrics(id);
46 |
47 | // This should only be executed inside the electron (main) process
48 | if (!cached || !cached.lines!.length || !cached?.synchronized) {
49 | if (!cached) debug(`Cache miss for ${metadata.artist} - ${metadata.title}`);
50 | else if (!cached?.synchronized) debug(`Cache hit but unsynced lyrics. Trying to fetch synchronized lyrics for ${metadata.artist} - ${metadata.title}`);
51 |
52 | lyrics = (await getAllLyrics(metadata)).find(x => !cached?.synchronized ? x.synchronized : true);
53 |
54 | if (lyrics){
55 | await saveLyrics(id, lyrics);
56 | debug("Fetched from " + lyrics.provider);
57 | }else
58 | debug("Unable to fetch lyrics");
59 | }
60 |
61 | if(cached && !lyrics)
62 | lyrics = cached;
63 |
64 | return lyrics || { unavailable: true };
65 | }
66 |
67 | function computeLyricsID(metadata: MetadataType){
68 | return `${metadata.artist}:${metadata.album}:${metadata.title}`;
69 | }
70 |
--------------------------------------------------------------------------------
/src/main/integrations/lyricsOffline.ts:
--------------------------------------------------------------------------------
1 | import { mkdir, readdir, readFile, rm, stat, writeFile } from "fs/promises";
2 | import { extname, resolve } from "path";
3 | import { createHash } from "crypto";
4 | import JSON5 from "json5";
5 | import { Lyrics } from "../../types";
6 | import { getAppData, gzipCompress, gzipDecompress, humanDimensionToBytes } from "../util";
7 | import { get as getConfig } from "../config";
8 | import { debug } from "..";
9 |
10 | import type { Stats } from "fs";
11 |
12 | const lyrPath = resolve(getAppData(), "sunamu", "Lyrics Cache");
13 |
14 | function md5(data){
15 | return createHash("md5").update(data).digest("hex");
16 | }
17 |
18 | export async function get(id: string): Promise{
19 | const cachePath = resolve(lyrPath, md5(id) + ".gz");
20 | try {
21 | return JSON5.parse((await gzipDecompress(await readFile(cachePath))).toString());
22 | }catch (_) {
23 | return undefined;
24 | }
25 | }
26 |
27 | export async function save(id: string, data: any): Promise{
28 | const cachePath = resolve(lyrPath, md5(id) + ".gz");
29 | // mkdir
30 | try {
31 | mkdir(lyrPath, {recursive:true});
32 | }catch(_){
33 | return false;
34 | }
35 |
36 | // save
37 | try {
38 | await writeFile(cachePath, await gzipCompress(JSON5.stringify(data)));
39 | return true;
40 | } catch (_) {
41 | return false;
42 | }
43 | }
44 |
45 | export async function remove(id: string): Promise{
46 | const cachePath = resolve(lyrPath, md5(id) + ".gz");
47 | // save
48 | try {
49 | await rm(cachePath);
50 | return true;
51 | } catch (_) {
52 | return false;
53 | }
54 | }
55 |
56 | async function convertUncompressed(): Promise{
57 | try{
58 | const lyrics = await readdir(lyrPath);
59 | for (const file of lyrics) {
60 | if (extname(file) === ".gz") continue;
61 |
62 | const path = resolve(lyrPath, file);
63 | await writeFile(path + ".gz", await gzipCompress(await readFile(path)));
64 | await rm(path);
65 | }
66 | } catch(_){
67 | debug("Cannot convert uncompressed lyrics; probably the cache path does not exist.");
68 | }
69 | }
70 |
71 | async function statCachePath(): Promise