Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Windows SMTC (System Media Transport Controls) integration #533

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build-appimage-compat.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
./appimage-build-compat.sh

- name: upload artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: Supersonic-compat.AppImage
path: Supersonic-x86_64.AppImage
2 changes: 1 addition & 1 deletion .github/workflows/build-appimage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
./appimage-build.sh

- name: upload artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: Supersonic.AppImage
path: Supersonic-x86_64.AppImage
2 changes: 1 addition & 1 deletion .github/workflows/build-macos-arm64.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
make zip_macos

- name: Upload package
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: Supersonic.zip
path: Supersonic.zip
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/build-macos-x64.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,13 @@ jobs:
mv Supersonic.zip Supersonic_HighSierra+.zip

- name: Upload Big Sur+ package
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: Supersonic_mac_x64_BigSur+.zip
path: Supersonic_BigSur+.zip

- name: Upload High Sierra+ package
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: Supersonic_mac_x64_HighSierra+.zip
path: Supersonic_HighSierra+.zip
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build-ubuntu-22.04.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
run: make package_linux

- name: Upload artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: Supersonic_ubuntu_x64.tar.xz
path: Supersonic.tar.xz
2 changes: 1 addition & 1 deletion .github/workflows/build-ubuntu-24.04.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
run: make package_linux

- name: Upload artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: Supersonic_ubuntu_x64.tar.xz
path: Supersonic.tar.xz
10 changes: 7 additions & 3 deletions .github/workflows/build-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,22 +53,26 @@ jobs:
rm Supersonic.exe &&
mv Supersonic-newbuild.exe Supersonic.exe

- name: Download smtc dll
run: >
wget https://github.com/supersonic-app/smtc-dll/releases/download/v0.1.0/SMTC.dll

- name: Generate zip bundle
run: zip Supersonic-windows.zip Supersonic.exe libmpv-2.dll
run: zip Supersonic-windows.zip Supersonic.exe libmpv-2.dll SMTC.dll

- name: Generate installer
uses: Minionguyjpro/[email protected]
with:
path: win_inno_installscript.iss

- name: Upload zip
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: Supersonic_windows_x64.zip
path: Supersonic-windows.zip

- name: Upload installer
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: Supersonic_windows_x64_installer.exe
path: Output/supersonic-installer.exe
92 changes: 81 additions & 11 deletions backend/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"reflect"
"runtime"
"slices"
"strings"
"time"

"github.com/dweymouth/supersonic/backend/ipc"
Expand Down Expand Up @@ -45,17 +46,19 @@ type App struct {
LocalPlayer *mpv.Player
UpdateChecker UpdateChecker
MPRISHandler *MPRISHandler
WinSMTC *SMTC
ipcServer ipc.IPCServer

// UI callbacks to be set in main
OnReactivate func()
OnExit func()

appName string
appVersionTag string
configDir string
cacheDir string
portableMode bool
appName string
displayAppName string
appVersionTag string
configDir string
cacheDir string
portableMode bool

isFirstLaunch bool // set by config file reader
bgrndCtx context.Context
Expand Down Expand Up @@ -95,12 +98,13 @@ func StartupApp(appName, displayAppName, appVersion, appVersionTag, latestReleas
}

a := &App{
logFile: logFile,
appName: appName,
appVersionTag: appVersionTag,
configDir: confDir,
cacheDir: cacheDir,
portableMode: portableMode,
logFile: logFile,
appName: appName,
displayAppName: displayAppName,
appVersionTag: appVersionTag,
configDir: confDir,
cacheDir: cacheDir,
portableMode: portableMode,
}
a.bgrndCtx, a.cancel = context.WithCancel(context.Background())
a.readConfig()
Expand Down Expand Up @@ -165,11 +169,14 @@ func StartupApp(appName, displayAppName, appVersion, appVersionTag, latestReleas
}

// OS media center integrations
// Linux MPRIS
a.setupMPRIS(displayAppName)
// MacOS MPNowPlayingInfoCenter
InitMPMediaHandler(a.PlaybackManager, func(id string) (string, error) {
a.ImageManager.GetCoverThumbnail(id) // ensure image is cached locally
return a.ImageManager.GetCoverArtUrl(id)
})
// Windows SMTC is initialized from main once we have a window HWND.

a.startConfigWriter(a.bgrndCtx)

Expand Down Expand Up @@ -331,6 +338,66 @@ func (a *App) setupMPRIS(mprisAppName string) {
a.MPRISHandler.Start()
}

func (a *App) SetupWindowsSMTC(hwnd uintptr) {
smtc, err := InitSMTCForWindow(hwnd)
if err != nil {
log.Printf("error initializing SMTC: %d", err)
return
}
a.WinSMTC = smtc
smtc.UpdateMetadata(a.displayAppName, "")

smtc.OnButtonPressed(func(btn SMTCButton) {
switch btn {
case SMTCButtonPlay:
a.PlaybackManager.Continue()
case SMTCButtonPause:
a.PlaybackManager.Pause()
case SMTCButtonNext:
a.PlaybackManager.SeekNext()
case SMTCButtonPrevious:
a.PlaybackManager.SeekBackOrPrevious()
case SMTCButtonStop:
a.PlaybackManager.Stop()
}
})
smtc.OnSeek(func(millis int) {
a.PlaybackManager.SeekSeconds(float64(millis) / 1000)
})

a.PlaybackManager.OnSongChange(func(nowPlaying mediaprovider.MediaItem, _ *mediaprovider.Track) {
if nowPlaying == nil {
smtc.UpdateMetadata("Supersonic", "")
return
}
meta := nowPlaying.Metadata()
smtc.UpdateMetadata(meta.Name, strings.Join(meta.Artists, ", "))
smtc.UpdatePosition(0, meta.Duration*1000)
go func() {
a.ImageManager.GetCoverThumbnail(meta.CoverArtID) // ensure image is cached locally
if path, err := a.ImageManager.GetCoverArtPath(meta.CoverArtID); err == nil {
smtc.SetThumbnail(path)
}
}()
})
a.PlaybackManager.OnSeek(func() {
dur := a.PlaybackManager.NowPlaying().Metadata().Duration
smtc.UpdatePosition(int(a.PlaybackManager.CurrentPlayer().GetStatus().TimePos*1000), dur*1000)
})
a.PlaybackManager.OnPlaying(func() {
smtc.SetEnabled(true)
smtc.UpdatePlaybackState(SMTCPlaybackStatePlaying)
})
a.PlaybackManager.OnPaused(func() {
smtc.SetEnabled(true)
smtc.UpdatePlaybackState(SMTCPlaybackStatePaused)
})
a.PlaybackManager.OnStopped(func() {
smtc.SetEnabled(false)
smtc.UpdatePlaybackState(SMTCPlaybackStateStopped)
})
}

func (a *App) LoginToDefaultServer(string) error {
serverCfg := a.ServerManager.GetDefaultServer()
if serverCfg == nil {
Expand Down Expand Up @@ -370,6 +437,9 @@ func (a *App) Shutdown() {
a.ipcServer.Shutdown(a.bgrndCtx)
}
a.MPRISHandler.Shutdown()
if a.WinSMTC != nil {
a.WinSMTC.Shutdown()
}
a.PlaybackManager.DisableCallbacks()
a.PlaybackManager.Stop() // will trigger scrobble check
a.cancel()
Expand Down
9 changes: 9 additions & 0 deletions backend/imagemanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,15 @@ func (i *ImageManager) GetCoverArtUrl(coverID string) (string, error) {
return "", errors.New("cover not found")
}

// GetCoverArtURL returns the file path for the locally cached cover thumbnail, if it exists.
func (i *ImageManager) GetCoverArtPath(coverID string) (string, error) {
path := i.filePathForCover(coverID)
if _, err := os.Stat(path); err == nil {
return path, nil
}
return "", errors.New("cover not found")
}

// GetCachedArtistImage returns the artist image for the given artistID from the on-disc cache, if it exists.
func (i *ImageManager) GetCachedArtistImage(artistID string) (image.Image, bool) {
return i.loadLocalImage(i.filePathForArtistImage(artistID))
Expand Down
Loading
Loading