Compare commits

..

No commits in common. "7d4bc6dd898a43dd71a4ec91618654e48b59840e" and "c35416c9283ce4e3dda3e22077d3c9651205ca70" have entirely different histories.

24 changed files with 317 additions and 774 deletions

View File

@ -10,7 +10,7 @@ on:
jobs:
build-linux:
runs-on: ubuntu-22.04
runs-on: ubuntu-20.04
steps:
- name: Checkout code
@ -46,7 +46,7 @@ jobs:
run: cmake -B build -S . -DCMAKE_BUILD_TYPE=Release
- name: Build the project
run: cmake --build build --config Release --target PlayerLink --parallel $(nproc)
run: cmake --build build --config Release
- name: Download linuxdeploy
run: |
@ -66,63 +66,6 @@ jobs:
name: PlayerLink-AppImage
path: ./*.AppImage
build-linux-arm64:
runs-on: ubuntu-22.04-arm
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
submodules: recursive
fetch-depth: 0
- name: Set up CMake
uses: lukka/get-cmake@latest
with:
cmakeVersion: '3.22.0'
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
build-essential \
libssl-dev \
libx11-dev \
libxext-dev \
libxrandr-dev \
libxinerama-dev \
libxcursor-dev \
zlib1g-dev \
libglu1-mesa-dev \
libgtk-3-dev \
libwayland-dev \
fuse
sudo modprobe fuse
- name: Configure with CMake
run: cmake -B build -S . -DCMAKE_BUILD_TYPE=Release
- name: Build the project
run: cmake --build build --config Release --target PlayerLink --parallel $(nproc)
- name: Download linuxdeploy
run: |
wget "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-aarch64.AppImage"
wget "https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh"
chmod +x linuxdeploy-aarch64.AppImage linuxdeploy-plugin-gtk.sh
sudo mv linuxdeploy-aarch64.AppImage /usr/local/bin/linuxdeploy
- name: Create AppImage
run: |
cp -r build/PlayerLink linux/AppDir/usr/bin/
linuxdeploy --appdir=linux/AppDir --plugin gtk --output appimage
- name: Upload AppImage artifact
uses: actions/upload-artifact@v4
with:
name: PlayerLink-AppImage-ARM64
path: ./*.AppImage
build-windows:
runs-on: windows-latest
@ -142,47 +85,12 @@ jobs:
run: cmake -B build -S . -DCMAKE_BUILD_TYPE=Release
- name: Build the project
run: cmake --build build --config Release --target PlayerLink
run: cmake --build build --config Release
- name: Upload Windows artifact
uses: actions/upload-artifact@v4
with:
name: PlayerLink-Windows-AMD64
path: build/Release/*
build-windows-arm64:
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
submodules: recursive
fetch-depth: 0
- name: Setup right windows sdk
uses: GuillaumeFalourd/setup-windows10-sdk-action@v2.4
with:
sdk-version: 26100
- name: Set up CMake
uses: lukka/get-cmake@latest
with:
cmakeVersion: '3.22.0'
- name: Configure with CMake for ARM64
run: cmake -B build -S . -DCMAKE_SYSTEM_VERSION="10.0.26100.0" -DCMAKE_BUILD_TYPE=Release -A ARM64
- name: Build the project for ARM64
run: cmake --build build --config Release --target PlayerLink
- name: Rename ARM64 executable
run: Rename-Item build\Release\PlayerLink.exe PlayerLink-arm64.exe
- name: Upload Windows ARM64 artifact
uses: actions/upload-artifact@v4
with:
name: PlayerLink-Windows-ARM64
name: PlayerLink-Windows-Executable
path: build/Release/*
build-macos:
@ -204,7 +112,7 @@ jobs:
run: cmake -B build -S . -DCMAKE_BUILD_TYPE=Release
- name: Build the project
run: cmake --build build --config Release --target PlayerLink --parallel $(sysctl -n hw.physicalcpu)
run: cmake --build build --config Release
- name: Create DMG package
run: |
@ -220,7 +128,7 @@ jobs:
create-release:
if: startsWith(github.ref, 'refs/tags/')
needs: [build-linux, build-linux-arm64, build-windows, build-windows-arm64, build-macos]
needs: [build-linux, build-windows, build-macos]
runs-on: ubuntu-latest
steps:
- name: Download artifacts
@ -240,7 +148,6 @@ jobs:
release_name: Release ${{ github.ref_name }}
draft: false
prerelease: false
- name: Upload AppImage
uses: actions/upload-release-asset@v1
env:
@ -251,16 +158,6 @@ jobs:
asset_name: PlayerLink-x86_64.AppImage
asset_content_type: application/octet-stream
- name: Upload ARM64 AppImage
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./release-assets/PlayerLink-aarch64.AppImage
asset_name: PlayerLink-aarch64.AppImage
asset_content_type: application/octet-stream
- name: Upload Windows Executable
uses: actions/upload-release-asset@v1
env:
@ -271,16 +168,6 @@ jobs:
asset_name: PlayerLink.exe
asset_content_type: application/octet-stream
- name: Upload Windows ARM64 Executable
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./release-assets/PlayerLink-arm64.exe
asset_name: PlayerLink-arm64.exe
asset_content_type: application/octet-stream
- name: Upload macOS DMG
uses: actions/upload-release-asset@v1
env:

View File

@ -1,11 +1,11 @@
cmake_minimum_required (VERSION 3.12)
cmake_minimum_required (VERSION 3.8)
include("cmake/create_resources.cmake")
file(GLOB_RECURSE SOURCES "src/*.cpp")
#enable objective c support on mac os, needed for wxwidgets and compile for both intel macs and apple sillicon macs
if(APPLE)
list(APPEND SOURCES "src/backends/darwin.mm" ${CMAKE_SOURCE_DIR}/osx/icon.icns ${CMAKE_SOURCE_DIR}/osx/MediaRemote.js)
list(APPEND SOURCES "src/backends/darwin.mm" ${CMAKE_SOURCE_DIR}/osx/icon.icns)
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "" FORCE)
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING "" FORCE)
project ("PlayerLink" LANGUAGES C CXX OBJCXX)
@ -40,7 +40,6 @@ elseif(APPLE)
endif()
set_target_properties(PlayerLink PROPERTIES MACOSX_BUNDLE TRUE)
set_source_files_properties(${CMAKE_SOURCE_DIR}/osx/icon.icns PROPERTIES MACOSX_PACKAGE_LOCATION "Resources")
set_source_files_properties(${CMAKE_SOURCE_DIR}/osx/MediaRemote.js PROPERTIES MACOSX_PACKAGE_LOCATION "Resources")
set_target_properties(PlayerLink PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_SOURCE_DIR}/osx/Info.plist)
elseif(UNIX AND NOT APPLE)
list(APPEND LIBRARIES dbus)

View File

@ -80,13 +80,13 @@ An example on how to add custom apps to the config can be found [here](./setting
4. Build the project :)
```bash
# for a release build
cmake --build build --config Release --target PlayerLink
cmake --build build --config Release
# for a debug build
cmake --build build --target PlayerLink
cmake --build build
```
## Contributing
This repository is open for contributions. You can view the current roadmap [here](https://github.com/EinTim23/PlayerLink/projects) or implement your own features and then open a pull request. Please keep your code as consistent and clean as possible.
## Credits
This project was heavily inspired by [Alexandra Göttlicher's MusicRPC](https://github.com/kaethchen/MusicRPC) and her project may provide a better experience when being on Mac OS only.
This project was heavily inspired by [Alexandra Aurora's MusicRPC](https://github.com/AlexandraAurora/MusicRPC) and her project may provide a better experience when being on Mac OS only.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 692 KiB

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 285 KiB

View File

@ -1,33 +0,0 @@
ObjC.import('Foundation');
try {
const frameworkPath = '/System/Library/PrivateFrameworks/MediaRemote.framework';
const framework = $.NSBundle.bundleWithPath($(frameworkPath));
framework.load
const MRNowPlayingRequest = $.NSClassFromString('MRNowPlayingRequest');
const playerPath = MRNowPlayingRequest.localNowPlayingPlayerPath;
const bundleID = ObjC.unwrap(playerPath.client.bundleIdentifier);
const nowPlayingItem = MRNowPlayingRequest.localNowPlayingItem;
const info = nowPlayingItem.nowPlayingInfo;
const title = info.valueForKey('kMRMediaRemoteNowPlayingInfoTitle');
const album = info.valueForKey('kMRMediaRemoteNowPlayingInfoAlbum');
const artist = info.valueForKey('kMRMediaRemoteNowPlayingInfoArtist');
const duration = info.valueForKey('kMRMediaRemoteNowPlayingInfoDuration');
const playbackStatus = info.valueForKey('kMRMediaRemoteNowPlayingInfoPlaybackRate');
const elapsed = info.valueForKey('kMRMediaRemoteNowPlayingInfoElapsedTime');
JSON.stringify({
title: ObjC.unwrap(title),
album: ObjC.unwrap(album),
artist: ObjC.unwrap(artist),
duration: ObjC.unwrap(duration),
playbackStatus: ObjC.unwrap(playbackStatus),
elapsed: ObjC.unwrap(elapsed),
player: ObjC.unwrap(bundleID)
});
} catch (error) {
JSON.stringify({ player: 'none', error: error.toString() });
}

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 817 KiB

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

View File

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
<path d="M21.731 2.269a2.625 2.625 0 0 0-3.712 0l-1.157 1.157 3.712 3.712 1.157-1.157a2.625 2.625 0 0 0 0-3.712ZM19.513 8.199l-3.712-3.712-12.15 12.15a5.25 5.25 0 0 0-1.32 2.214l-.8 2.685a.75.75 0 0 0 .933.933l2.685-.8a5.25 5.25 0 0 0 2.214-1.32L19.513 8.2Z" />
</svg>

Before

Width:  |  Height:  |  Size: 367 B

View File

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
<path fill-rule="evenodd" d="M14 3a2 2 0 1 0-4 0v7H3a2 2 0 1 0 0 4h7v7a2 2 0 1 0 4 0v-7h7a2 2 0 1 0 0-4h-7V3z" clip-rule="evenodd" />
</svg>

Before

Width:  |  Height:  |  Size: 239 B

View File

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
<path fill-rule="evenodd" d="M16.5 4.478v.227a48.816 48.816 0 0 1 3.878.512.75.75 0 1 1-.256 1.478l-.209-.035-1.005 13.07a3 3 0 0 1-2.991 2.77H8.084a3 3 0 0 1-2.991-2.77L4.087 6.66l-.209.035a.75.75 0 0 1-.256-1.478A48.567 48.567 0 0 1 7.5 4.705v-.227c0-1.564 1.213-2.9 2.816-2.951a52.662 52.662 0 0 1 3.369 0c1.603.051 2.815 1.387 2.815 2.951Zm-6.136-1.452a51.196 51.196 0 0 1 3.273 0C14.39 3.05 15 3.684 15 4.478v.113a49.488 49.488 0 0 0-6 0v-.113c0-.794.609-1.428 1.364-1.452Zm-.355 5.945a.75.75 0 1 0-1.5.058l.347 9a.75.75 0 1 0 1.499-.058l-.346-9Zm5.48.058a.75.75 0 1 0-1.498-.058l-.347 9a.75.75 0 0 0 1.5.058l.345-9Z" clip-rule="evenodd" />
</svg>

Before

Width:  |  Height:  |  Size: 751 B

View File

@ -5,34 +5,13 @@
"client_id": "1245257414715113573",
"enabled": true,
"name": "Spotify",
"type": 2,
"process_names": [
"org.mpris.MediaPlayer2.spotify",
"com.spotify.client",
"Spotify.exe"
],
"search_endpoint": "https://open.spotify.com/search/"
},
{
"client_id": "1337188104829665340",
"enabled": true,
"name": "Apple Music",
"type": 2,
"process_names": [
"com.apple.Music",
"AppleInc.AppleMusicWin_nzyj5cx40ttqa!APP",
"AppleMusic.exe"
],
"search_endpoint": "https://music.apple.com/search?term="
}
],
"lastfm": {
"api_key": "",
"api_secret": "",
"enabled": false,
"password": "",
"username": ""
},
"odesli": true,
"autostart": false
}

View File

@ -1,11 +1,9 @@
#include <Foundation/NSObjCRuntime.h>
#ifdef __APPLE__
#include <AppKit/AppKit.h>
#include <Cocoa/Cocoa.h>
#include <Foundation/Foundation.h>
#include <dispatch/dispatch.h>
#include <filesystem>
#include <nlohmann-json/single_include/nlohmann/json.hpp>
#include <fstream>
#include "../MediaRemote.hpp"
@ -18,120 +16,65 @@ void hideDockIcon(bool shouldHide) {
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
}
NSString *getFilePathFromBundle(NSString *fileName, NSString *fileType) {
NSString *filePath = [[NSBundle mainBundle] pathForResource:fileName ofType:fileType];
return filePath;
}
NSString *executeCommand(NSString *command, NSArray *arguments) {
NSTask *task = [[NSTask alloc] init];
task.launchPath = command;
task.arguments = arguments;
NSPipe *pipe = [NSPipe pipe];
task.standardOutput = pipe;
task.standardError = [NSPipe pipe];
[task launch];
NSData *data = [[pipe fileHandleForReading] readDataToEndOfFile];
NSString *output = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
[task waitUntilExit];
return output;
}
std::shared_ptr<MediaInfo> backend::getMediaInformation() {
// apple decided to prevent apps not signed by them to use media remote, so we use an apple script instead. But that script only works on Sonoma or newer and the other one is arguably better, so keep the old method as well
if (@available(macOS 15.0, *)) {
static NSString *script = getFilePathFromBundle(@"MediaRemote", @"js");
NSString *output = executeCommand(@"/usr/bin/osascript", @[ @"-l", @"JavaScript", script ]);
nlohmann::json j = nlohmann::json::parse([output UTF8String]);
__block NSString *appName = nil;
__block NSDictionary *playingInfo = nil;
std::string appName = j["player"].get<std::string>();
if (appName == "none")
return nullptr;
dispatch_group_t group = dispatch_group_create();
bool paused = j["playbackStatus"].get<int>() == 0;
dispatch_group_enter(group);
MRMediaRemoteGetNowPlayingApplicationPID(dispatch_get_main_queue(), ^(pid_t pid) {
if (pid > 0) {
NSRunningApplication *app = [NSRunningApplication runningApplicationWithProcessIdentifier:pid];
if (app)
appName = [[app.bundleIdentifier copy] retain];
}
dispatch_group_leave(group);
});
std::string songTitle = j["title"].get<std::string>();
dispatch_group_enter(group);
MRMediaRemoteGetNowPlayingInfo(dispatch_get_main_queue(), ^(CFDictionaryRef result) {
if (result)
playingInfo = [[(__bridge NSDictionary *)result copy] retain];
dispatch_group_leave(group);
});
std::string songAlbum = j["album"].get<std::string>();
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
dispatch_release(group);
if (appName == nil || playingInfo == nil)
return nullptr;
std::string songArtist = j["artist"].get<std::string>();
bool paused = [playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoPlaybackRate] intValue] == 0;
int64_t elapsedTimeMs = 0;
int64_t durationMs = 0;
try {
double durationNumber = j["duration"].get<double>();
durationMs = static_cast<int64_t>(durationNumber * 1000);
NSString *title = playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoTitle];
std::string songTitle = title ? [title UTF8String] : "";
double elapsedTimeNumber = j["elapsed"].get<double>();
elapsedTimeMs = static_cast<int64_t>(elapsedTimeNumber * 1000);
} catch (...) {
}
NSString *album = playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoAlbum];
std::string songAlbum = album ? [album UTF8String] : "";
return std::make_shared<MediaInfo>(paused, songTitle, songArtist, songAlbum, appName, "", durationMs,
elapsedTimeMs);
} else {
__block NSString *appName = nil;
__block NSDictionary *playingInfo = nil;
NSString *artist = playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoArtist];
std::string songArtist = artist ? [artist UTF8String] : "";
dispatch_group_t group = dispatch_group_create();
NSData *artworkData = playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoArtworkData];
dispatch_group_enter(group);
MRMediaRemoteGetNowPlayingApplicationPID(dispatch_get_main_queue(), ^(pid_t pid) {
if (pid > 0) {
NSRunningApplication *app = [NSRunningApplication runningApplicationWithProcessIdentifier:pid];
if (app)
appName = [[app.bundleIdentifier copy] retain];
}
dispatch_group_leave(group);
});
std::string thumbnailData;
if (artworkData)
thumbnailData = std::string((const char *)[artworkData bytes], [artworkData length]);
dispatch_group_enter(group);
MRMediaRemoteGetNowPlayingInfo(dispatch_get_main_queue(), ^(CFDictionaryRef result) {
if (result)
playingInfo = [[(__bridge NSDictionary *)result copy] retain];
dispatch_group_leave(group);
});
NSNumber *elapsedTimeNumber = playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoElapsedTime];
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
dispatch_release(group);
if (appName == nil || playingInfo == nil)
return nullptr;
NSNumber *durationNumber = playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoDuration];
bool paused = [playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoPlaybackRate] intValue] == 0;
int64_t elapsedTimeMs = elapsedTimeNumber ? static_cast<int64_t>([elapsedTimeNumber doubleValue] * 1000) : 0;
NSString *title = playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoTitle];
std::string songTitle = title ? [title UTF8String] : "";
int64_t durationMs = durationNumber ? static_cast<int64_t>([durationNumber doubleValue] * 1000) : 0;
NSString *album = playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoAlbum];
std::string songAlbum = album ? [album UTF8String] : "";
std::string appNameString = appName.UTF8String;
NSString *artist = playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoArtist];
std::string songArtist = artist ? [artist UTF8String] : "";
NSData *artworkData = playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoArtworkData];
std::string thumbnailData;
if (artworkData)
thumbnailData = std::string((const char *)[artworkData bytes], [artworkData length]);
NSNumber *elapsedTimeNumber = playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoElapsedTime];
NSNumber *durationNumber = playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoDuration];
int64_t elapsedTimeMs = elapsedTimeNumber ? static_cast<int64_t>([elapsedTimeNumber doubleValue] * 1000) : 0;
int64_t durationMs = durationNumber ? static_cast<int64_t>([durationNumber doubleValue] * 1000) : 0;
std::string appNameString = appName.UTF8String;
[appName release];
[playingInfo release];
return std::make_shared<MediaInfo>(paused, songTitle, songArtist, songAlbum, appNameString, thumbnailData,
durationMs, elapsedTimeMs);
}
[appName release];
[playingInfo release];
return std::make_shared<MediaInfo>(paused, songTitle, songArtist, songAlbum, appNameString, thumbnailData,
durationMs, elapsedTimeMs);
}
std::filesystem::path backend::getConfigDirectory() {
@ -145,14 +88,14 @@ bool backend::toggleAutostart(bool enabled) {
launchAgentPath = launchAgentPath / "Library" / "LaunchAgents";
std::filesystem::create_directories(launchAgentPath);
launchAgentPath = launchAgentPath / "PlayerLink.plist";
if (!enabled && std::filesystem::exists(launchAgentPath)) {
std::filesystem::remove(launchAgentPath);
return true;
}
NSString *binaryPath = [[[NSProcessInfo processInfo] arguments][0] stringByStandardizingPath];
// I would also like to use std::format here, but well I also want to support older mac os versions.
//I would also like to use std::format here, but well I also want to support older mac os versions.
std::string formattedPlist =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" "
"\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n <dict>\n\n "

View File

@ -35,6 +35,59 @@ std::string toStdString(winrt::hstring& in) {
return result;
}
std::string getAppModelIdOfProcess(HANDLE hProc) {
UINT32 length = 0;
LONG rc = GetApplicationUserModelId(hProc, &length, NULL);
if (rc != ERROR_INSUFFICIENT_BUFFER)
return "";
PWSTR fullName = (PWSTR)malloc(length * sizeof(*fullName));
if (!fullName)
return "";
rc = GetApplicationUserModelId(hProc, &length, fullName);
if (rc != ERROR_SUCCESS) {
free(fullName);
return "";
}
winrt::hstring wideName = fullName;
std::string name = toStdString(wideName);
free(fullName);
return name;
}
std::string getProcessNameFromAppModelId(std::string appModelId) {
DWORD processes[1024];
DWORD cbNeeded;
if (!EnumProcesses(processes, sizeof(processes), &cbNeeded))
return "";
unsigned int processCount = cbNeeded / sizeof(DWORD);
for (DWORD i = 0; i < processCount; i++) {
DWORD processID = processes[i];
HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, processID);
if (hProcess) {
std::string modelid = getAppModelIdOfProcess(hProcess);
if (modelid != appModelId) {
CloseHandle(hProcess);
continue;
}
char exeName[MAX_PATH]{};
DWORD size = MAX_PATH;
QueryFullProcessImageNameA(hProcess, 0, exeName, &size);
std::filesystem::path exePath = exeName;
CloseHandle(hProcess);
return exePath.filename().string();
}
}
return "";
}
bool CreateShortcut(std::string source, std::string target) {
CoInitialize(nullptr);
WCHAR src[MAX_PATH];
@ -92,55 +145,57 @@ std::shared_ptr<MediaInfo> backend::getMediaInformation() {
return nullptr;
auto playbackInfo = currentSession.GetPlaybackInfo();
try {
auto mediaProperties = currentSession.TryGetMediaPropertiesAsync().get();
auto timelineInformation = currentSession.GetTimelineProperties();
if (!mediaProperties)
return nullptr;
auto endTime = std::chrono::duration_cast<std::chrono::milliseconds>(timelineInformation.EndTime()).count();
auto elapsedTime =
std::chrono::duration_cast<std::chrono::milliseconds>(timelineInformation.Position()).count();
auto thumbnail = mediaProperties.Thumbnail();
std::string thumbnailData = "";
if (thumbnail) {
auto stream = thumbnail.OpenReadAsync().get();
size_t size = static_cast<size_t>(stream.Size());
DataReader reader(stream);
reader.LoadAsync(static_cast<uint32_t>(size)).get();
std::vector<uint8_t> buffer(size);
reader.ReadBytes(buffer);
reader.Close();
thumbnailData = std::string(buffer.begin(), buffer.end());
stream.Close();
}
std::string artist = toStdString(mediaProperties.Artist());
std::string albumName = toStdString(mediaProperties.AlbumTitle());
if (artist == "")
artist = toStdString(mediaProperties.AlbumArtist()); // Needed for some apps
if (artist.find(EM_DASH) != std::string::npos) {
albumName = artist.substr(artist.find(EM_DASH) + 3);
artist = artist.substr(0, artist.find(EM_DASH));
utils::trim(artist);
utils::trim(albumName);
}
std::string modelId = toStdString(currentSession.SourceAppUserModelId());
return std::make_shared<MediaInfo>(
playbackInfo.PlaybackStatus() == GlobalSystemMediaTransportControlsSessionPlaybackStatus::Paused,
toStdString(mediaProperties.Title()), std::move(artist), std::move(albumName), std::move(modelId),
std::move(thumbnailData), endTime, elapsedTime);
} catch (...) {
auto mediaProperties = currentSession.TryGetMediaPropertiesAsync().get();
auto timelineInformation = currentSession.GetTimelineProperties();
if (!mediaProperties)
return nullptr;
auto endTime = std::chrono::duration_cast<std::chrono::milliseconds>(timelineInformation.EndTime()).count();
auto elapsedTime = std::chrono::duration_cast<std::chrono::milliseconds>(timelineInformation.Position()).count();
auto thumbnail = mediaProperties.Thumbnail();
std::string thumbnailData = "";
if (thumbnail) {
auto stream = thumbnail.OpenReadAsync().get();
size_t size = static_cast<size_t>(stream.Size());
DataReader reader(stream);
reader.LoadAsync(static_cast<uint32_t>(size)).get();
std::vector<uint8_t> buffer(size);
reader.ReadBytes(buffer);
reader.Close();
thumbnailData = std::string(buffer.begin(), buffer.end());
stream.Close();
}
std::string artist = toStdString(mediaProperties.Artist());
std::string albumName = toStdString(mediaProperties.AlbumTitle());
if (artist == "")
artist = toStdString(mediaProperties.AlbumArtist()); // Needed for some apps
if (artist.find(EM_DASH) != std::string::npos) {
albumName = artist.substr(artist.find(EM_DASH) + 3);
artist = artist.substr(0, artist.find(EM_DASH));
utils::trim(artist);
utils::trim(albumName);
}
std::string modelId = toStdString(currentSession.SourceAppUserModelId());
// I do know that this is disgusting, but for some reason microsoft decided to switch out the exe name with the
// ApplicationUserModelId in some version of windows 11. So we check if it's an exe name, if not we are on some
// newer windows version and need to get the exe name from the model id. We cannot directly work with the model id
// because it's unique per machine and therefore would mess up configs with preconfigured apps.
if (modelId.find(".exe") == std::string::npos)
modelId = getProcessNameFromAppModelId(modelId);
return std::make_shared<MediaInfo>(
playbackInfo.PlaybackStatus() == GlobalSystemMediaTransportControlsSessionPlaybackStatus::Paused,
toStdString(mediaProperties.Title()), std::move(artist), std::move(albumName), std::move(modelId),
std::move(thumbnailData), endTime, elapsedTime);
}
bool backend::init() {

View File

@ -44,17 +44,13 @@ public:
parameters["api_sig"] = getApiSignature(parameters);
std::string postBody = utils::getURLEncodedPostBody(parameters);
std::string response = utils::httpRequest(api_base, "POST", postBody);
try {
auto j = nlohmann::json::parse(response);
if (j.contains("error"))
return j["error"].get<LASTFM_STATUS>();
auto j = nlohmann::json::parse(response);
if (j.contains("error"))
return j["error"].get<LASTFM_STATUS>();
session_token = j["session"]["key"].get<std::string>();
authenticated = true;
return LASTFM_STATUS::SUCCESS;
} catch (...) {
return LASTFM_STATUS::UNKNOWN_ERROR;
}
session_token = j["session"]["key"].get<std::string>();
authenticated = true;
return LASTFM_STATUS::SUCCESS;
}
LASTFM_STATUS scrobble(std::string artist, std::string track) {

View File

@ -3,18 +3,15 @@
#include <wx/mstream.h>
#include <wx/statline.h>
#include <wx/taskbar.h>
#include <wx/hyperlink.h>
#include <wx/wx.h>
#include <chrono>
#include <cstddef>
#include <thread>
#include "backend.hpp"
#include "lastfm.hpp"
#include "rsrc.hpp"
#include "utils.hpp"
#include "wx/sizer.h"
std::string lastPlayingSong = "";
std::string lastMediaSource = "";
@ -51,22 +48,17 @@ void initLastFM(bool checkMode = false) {
lastfm = new LastFM(settings.lastfm.username, settings.lastfm.password, settings.lastfm.api_key,
settings.lastfm.api_secret);
LastFM::LASTFM_STATUS status = lastfm->authenticate();
if (status) {
delete lastfm;
lastfm = nullptr;
if (checkMode)
wxMessageBox(_("Error authenticating at LastFM!"), _("PlayerLink"), wxOK | wxICON_ERROR);
} else if (checkMode)
if (status)
wxMessageBox(_("Error authenticating at LastFM!"), _("PlayerLink"), wxOK | wxICON_ERROR);
else if (checkMode)
wxMessageBox(_("The LastFM authentication was successful."), _("PlayerLink"), wxOK | wxICON_INFORMATION);
}
void handleMediaTasks() {
initLastFM();
int64_t lastMs = 0;
while (true) {
std::this_thread::sleep_for(std::chrono::seconds(1));
if (!lastfm)
initLastFM();
auto mediaInformation = backend::getMediaInformation();
auto settings = utils::getSettings();
if (!mediaInformation) {
@ -93,6 +85,14 @@ void handleMediaTasks() {
if (shouldContinue)
continue;
if (lastPlayingSong.find(mediaInformation->songTitle + mediaInformation->songArtist +
mediaInformation->songAlbum) == std::string::npos &&
lastfm)
lastfm->scrobble(mediaInformation->songArtist, mediaInformation->songTitle);
lastPlayingSong = currentlyPlayingSong;
currentSongTitle = mediaInformation->songArtist + " - " + mediaInformation->songTitle;
std::string currentMediaSource = mediaInformation->playbackSource;
if (currentMediaSource != lastMediaSource) {
@ -102,14 +102,6 @@ void handleMediaTasks() {
auto app = utils::getApp(lastMediaSource);
if (lastPlayingSong.find(mediaInformation->songTitle + mediaInformation->songArtist +
mediaInformation->songAlbum) == std::string::npos &&
lastfm && app.enabled)
lastfm->scrobble(mediaInformation->songArtist, mediaInformation->songTitle);
lastPlayingSong = currentlyPlayingSong;
currentSongTitle = mediaInformation->songArtist + " - " + mediaInformation->songTitle;
if (!app.enabled) {
Discord_ClearPresence();
continue;
@ -118,7 +110,7 @@ void handleMediaTasks() {
std::string activityState = "by " + mediaInformation->songArtist;
DiscordRichPresence activity{};
activity.type = app.type;
activity.type = ActivityType::LISTENING;
activity.details = mediaInformation->songTitle.c_str();
activity.state = activityState.c_str();
activity.smallImageText = serviceName.c_str();
@ -151,7 +143,7 @@ void handleMediaTasks() {
}
std::string odesliUrl = utils::getOdesliURL(songInfo);
if (settings.odesli && songInfo.artworkURL != "") {
if(settings.odesli && songInfo.artworkURL != "") {
activity.button2name = "Show on Song.link";
activity.button2link = odesliUrl.c_str();
}
@ -159,202 +151,27 @@ void handleMediaTasks() {
Discord_UpdatePresence(&activity);
}
}
void SetWindowIcon(wxTopLevelWindow* win) {
const wxIcon icon = utils::loadIconFromMemory(icon_png, icon_png_size);
win->SetIcon(icon);
}
class AboutDialog : public wxDialog {
public:
AboutDialog(wxWindow* parent)
: wxDialog(parent, wxID_ANY, _("About PlayerLink"), wxDefaultPosition, wxDefaultSize,
wxDEFAULT_DIALOG_STYLE & ~wxRESIZE_BORDER) {
SetWindowIcon(this);
wxBoxSizer* mainSizer = new wxBoxSizer(wxVERTICAL);
wxStaticText* label = new wxStaticText(this, wxID_ANY, _("Made with <3 by EinTim"));
label->Wrap(300);
mainSizer->Add(label, 0, wxALL | wxALIGN_CENTER, 10);
wxStaticText* copyrightText = new wxStaticText(this, wxID_ANY, "© 2024-2025 EinTim. All rights reserved.");
copyrightText->Wrap(300);
mainSizer->Add(copyrightText, 0, wxALL | wxALIGN_CENTER, 10);
wxStaticText* creditsText =
new wxStaticText(this, wxID_ANY,
_("Credits:\n- Developer: EinTim\n- Inspiration: Alexandra Göttlicher\n- Icons from: "
"heroicons.com\n- App Icon from: localcc\n- Open source "
"projects used in this:\n wxWidgets, libcurl, libdbus, mbedtls, nlohmann-json."));
creditsText->Wrap(300);
mainSizer->Add(creditsText, 0, wxALL | wxALIGN_CENTER, 10);
wxHyperlinkCtrl* link = new wxHyperlinkCtrl(this, wxID_ANY, _("Visit my website"), _("https://eintim.dev"));
mainSizer->Add(link, 0, wxALL | wxALIGN_CENTER, 10);
wxButton* okButton = new wxButton(this, wxID_OK, _("OK"));
mainSizer->Add(okButton, 0, wxALL | wxALIGN_CENTER, 10);
this->SetSizerAndFit(mainSizer);
this->CenterOnScreen();
}
};
class EditAppDialog : public wxDialog {
public:
EditAppDialog(wxWindow* parent, const wxString title, utils::App* app)
: wxDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize,
wxDEFAULT_DIALOG_STYLE & ~wxRESIZE_BORDER) {
SetWindowIcon(this);
Bind(wxEVT_CLOSE_WINDOW, &EditAppDialog::OnClose, this);
wxBoxSizer* mainSizer = new wxBoxSizer(wxVERTICAL);
wxFlexGridSizer* formSizer = new wxFlexGridSizer(2, 0, 5);
// Make the second column growable (the one with the text controls)
formSizer->AddGrowableCol(1, 1);
// Application name
formSizer->Add(new wxStaticText(this, wxID_ANY, _("Application name:")), 0, wxALL | wxALIGN_CENTER_VERTICAL, 5);
auto nameInput = new wxTextCtrl(this, wxID_ANY);
nameInput->SetValue(app->appName);
nameInput->SetHint(_("Example: Apple Music"));
nameInput->Bind(wxEVT_TEXT,
[this, app](wxCommandEvent& event) { app->appName = event.GetString().ToStdString(); });
formSizer->Add(nameInput, 1, wxALL | wxEXPAND, 5);
// Client ID
formSizer->Add(new wxStaticText(this, wxID_ANY, _("Discord client id:")), 0, wxALL | wxALIGN_CENTER_VERTICAL,
5);
auto clientIdInput = new wxTextCtrl(this, wxID_ANY);
clientIdInput->SetHint(_("Example: 1337188104829665340"));
clientIdInput->SetValue(app->clientId);
clientIdInput->Bind(wxEVT_TEXT,
[this, app](wxCommandEvent& event) { app->clientId = event.GetString().ToStdString(); });
formSizer->Add(clientIdInput, 1, wxALL | wxEXPAND, 5);
// Search endpoint
formSizer->Add(new wxStaticText(this, wxID_ANY, _("Search endpoint:")), 0, wxALL | wxALIGN_CENTER_VERTICAL, 5);
auto searchEndpointInput = new wxTextCtrl(this, wxID_ANY);
searchEndpointInput->SetValue(app->searchEndpoint);
searchEndpointInput->SetHint(_("Search endpoint: https://music.apple.com/search?term="));
searchEndpointInput->Bind(
wxEVT_TEXT, [this, app](wxCommandEvent& event) { app->searchEndpoint = event.GetString().ToStdString(); });
formSizer->Add(searchEndpointInput, 1, wxALL | wxEXPAND, 5);
// Dropdown
formSizer->Add(new wxStaticText(this, wxID_ANY, "Activity Type:"), 0, wxALL | wxALIGN_CENTER_VERTICAL, 5);
wxString choices[] = {_("Listening"), _("Watching"), _("Playing")};
wxChoice* activityChoice = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 3, choices);
switch (app->type) {
case ActivityType::PLAYING:
activityChoice->SetSelection(2);
break;
case ActivityType::LISTENING:
activityChoice->SetSelection(0);
break;
case ActivityType::WATCHING:
activityChoice->SetSelection(1);
break;
default:
activityChoice->SetSelection(0);
}
activityChoice->Bind(wxEVT_CHOICE, [activityChoice, app](wxCommandEvent& event) {
const std::map<wxString, ActivityType> typeMap = {
{_("Listening"), ActivityType::LISTENING},
{_("Watching"), ActivityType::WATCHING},
{_("Playing"), ActivityType::PLAYING},
};
app->type = typeMap.at(event.GetString());
});
formSizer->Add(activityChoice, 1, wxALL | wxEXPAND, 5);
mainSizer->Add(formSizer, 0, wxEXPAND);
// Process names group
formSizer->Add(new wxStaticText(this, wxID_ANY, _("Process names:")), 0, wxALL | wxALIGN_CENTER_VERTICAL, 5);
wxBoxSizer* processBox = new wxBoxSizer(wxVERTICAL);
wxListBox* listBox = new wxListBox(this, wxID_ANY);
for (auto& process : app->processNames) {
listBox->Append(process);
}
if (app->processNames.size() == 0 && lastMediaSource != "") {
listBox->Append(lastMediaSource);
app->processNames.push_back(lastMediaSource);
}
processBox->Add(listBox, 1, wxALL | wxEXPAND, 5);
// Add input + buttons
wxBoxSizer* addSizer = new wxBoxSizer(wxHORIZONTAL);
auto processNameInput =
new wxTextCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(250, wxDefaultSize.GetHeight()), 0);
processNameInput->SetHint(_("Process name"));
const auto delete_button_texture = utils::loadSettingsIcon(trash_svg, trash_svg_size);
const auto add_button_texture = utils::loadSettingsIcon(plus_svg, plus_svg_size);
wxBitmapButton* addButton = new wxBitmapButton(this, wxID_ANY, add_button_texture);
addButton->SetToolTip(_("Add process to list"));
wxBitmapButton* removeButton = new wxBitmapButton(this, wxID_ANY, delete_button_texture);
removeButton->SetToolTip(_("Remove process from list"));
addSizer->Add(processNameInput, 1, wxALL | wxEXPAND, 5);
addSizer->Add(addButton, 0, wxALL, 5);
addSizer->Add(removeButton, 0, wxALL, 5);
processBox->Add(addSizer, 0, wxEXPAND);
mainSizer->Add(processBox, 1, wxALL | wxEXPAND);
SetSizerAndFit(mainSizer);
Centre();
// Bind events
addButton->Bind(wxEVT_BUTTON, [processNameInput, listBox, app](wxCommandEvent& event) {
wxString name = processNameInput->GetValue().Trim();
if (!name.IsEmpty()) {
listBox->Append(name);
app->processNames.push_back(name.ToStdString());
processNameInput->Clear();
}
});
removeButton->Bind(wxEVT_BUTTON, [listBox, app](wxCommandEvent& event) {
int selection = listBox->GetSelection();
if (selection != wxNOT_FOUND) {
app->processNames.erase(std::find(app->processNames.begin(), app->processNames.end(),
listBox->GetString(selection).ToStdString()));
listBox->Delete(selection);
}
});
wxBoxSizer* buttonSizer = new wxBoxSizer(wxHORIZONTAL);
wxButton* okButton = new wxButton(this, wxID_OK, _("Save"));
wxButton* cancelButton = new wxButton(this, wxID_CANCEL, _("Cancel"));
buttonSizer->Add(okButton, 0, wxALL | wxALIGN_CENTER, 10);
buttonSizer->Add(cancelButton, 0, wxALL | wxALIGN_CENTER, 10);
mainSizer->Add(buttonSizer, 0, wxALL | wxALIGN_CENTER);
this->SetSizerAndFit(mainSizer);
this->CenterOnParent();
}
private:
void OnClose(wxCloseEvent& event) { EndModal(wxID_CANCEL); }
};
class PlayerLinkIcon : public wxTaskBarIcon {
public:
PlayerLinkIcon(wxFrame* s) : settingsFrame(s), aboutDlg(nullptr) {}
PlayerLinkIcon(wxFrame* s) : settingsFrame(s) {}
void OnMenuOpen(wxCommandEvent& evt) {
settingsFrame->Show(true);
settingsFrame->Raise();
}
void OnCopyOdesliURL(wxCommandEvent& evt) { utils::copyToClipboard(utils::getOdesliURL(songInfo)); }
void OnMenuExit(wxCommandEvent& evt) { settingsFrame->Close(true); }
void OnMenuAbout(wxCommandEvent& evt) {
wxMessageBox(_("Made with <3 by EinTim"), _("PlayerLink"), wxOK | wxICON_INFORMATION);
}
protected:
virtual wxMenu* CreatePopupMenu() override {
wxMenu* menu = new wxMenu;
menu->Append(10004, currentSongTitle == "" ? _("Not Playing") : currentSongTitle);
menu->Append(10004, currentSongTitle == "" ? _("Not Playing") : wxString::FromUTF8(currentSongTitle));
menu->Enable(10004, false);
menu->AppendSeparator();
menu->Append(10005, _("Copy Odesli URL"));
@ -372,73 +189,115 @@ protected:
}
private:
void OnMenuOpen(wxCommandEvent& evt) {
settingsFrame->Show(true);
settingsFrame->Raise();
}
void OnCopyOdesliURL(wxCommandEvent& evt) { utils::copyToClipboard(utils::getOdesliURL(songInfo)); }
void OnMenuExit(wxCommandEvent& evt) { settingsFrame->Close(true); }
void OnMenuAbout(wxCommandEvent& evt) {
aboutDlg.Show(true);
aboutDlg.Raise();
}
wxFrame* settingsFrame;
AboutDialog aboutDlg;
};
class wxTextCtrlWithPlaceholder : public wxTextCtrl {
public:
wxTextCtrlWithPlaceholder(wxWindow* parent, wxWindowID id, const wxString& value,
const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize,
long style = 0, const wxValidator& validator = wxDefaultValidator,
const wxString& name = "textCtrl")
: wxTextCtrl(parent, id, value, pos, size, style, validator, name),
placeholder(""),
showPlaceholder(true),
isPassword((style & wxTE_PASSWORD) != 0) {
Bind(wxEVT_SET_FOCUS, &wxTextCtrlWithPlaceholder::OnFocus, this);
Bind(wxEVT_KILL_FOCUS, &wxTextCtrlWithPlaceholder::OnBlur, this);
}
void SetPlaceholderText(const wxString& p) {
placeholder = p;
if (GetValue().IsEmpty() || showPlaceholder)
UpdatePlaceholder();
}
protected:
void OnFocus(wxFocusEvent& event) {
if (showPlaceholder && GetValue() == placeholder) {
Clear();
if (isPassword)
SetStyleToPassword();
}
showPlaceholder = false;
event.Skip();
}
void OnBlur(wxFocusEvent& event) {
if (GetValue().IsEmpty()) {
showPlaceholder = true;
UpdatePlaceholder();
}
event.Skip();
}
private:
wxString placeholder;
bool showPlaceholder;
bool isPassword;
void UpdatePlaceholder() {
if (isPassword)
SetStyleToNormal();
SetValue(placeholder);
}
void SetStyleToPassword() { SetWindowStyle(GetWindowStyle() | wxTE_PASSWORD); }
void SetStyleToNormal() { SetWindowStyle(GetWindowStyle() & ~wxTE_PASSWORD); }
};
class PlayerLinkFrame : public wxFrame {
public:
PlayerLinkFrame(wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = wxEmptyString,
PlayerLinkFrame(wxWindow* parent, wxIcon& icon, wxWindowID id = wxID_ANY, const wxString& title = wxEmptyString,
const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize(300, 200),
long style = wxDEFAULT_FRAME_STYLE & ~wxRESIZE_BORDER & ~wxMAXIMIZE_BOX)
: wxFrame(parent, id, title, pos, size, style) {
this->SetSizeHints(wxDefaultSize, wxDefaultSize);
SetWindowIcon(this);
this->SetIcon(icon);
auto mainContainer = new wxBoxSizer(wxVERTICAL);
wxPanel* panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL);
panel->SetSizer(mainContainer);
wxBoxSizer* frameSizer = new wxBoxSizer(wxVERTICAL);
frameSizer->Add(panel, 1, wxEXPAND);
// header start
auto settingsText = new wxStaticText(panel, wxID_ANY, _("Settings"), wxDefaultPosition, wxDefaultSize, 0);
auto settingsText = new wxStaticText(this, wxID_ANY, _("Settings"), wxDefaultPosition, wxDefaultSize, 0);
settingsText->Wrap(-1);
mainContainer->Add(settingsText, 0, wxALIGN_CENTER | wxALL, 5);
// header end
// enabled apps start
auto settingsDivider = new wxStaticLine(panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL);
auto settingsDivider = new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL);
mainContainer->Add(settingsDivider, 0, wxEXPAND | wxALL, 5);
wxBoxSizer* enabledAppsContainer;
enabledAppsContainer = new wxBoxSizer(wxHORIZONTAL);
auto enabledAppsText =
new wxStaticText(panel, wxID_ANY, _("Enabled Apps:"), wxDefaultPosition, wxDefaultSize, 0);
new wxStaticText(this, wxID_ANY, _("Enabled Apps:"), wxDefaultPosition, wxDefaultSize, 0);
enabledAppsText->Wrap(-1);
enabledAppsContainer->Add(enabledAppsText, 0, wxALL, 5);
const auto edit_button_texture = utils::loadSettingsIcon(pencil_svg, pencil_svg_size);
const auto delete_button_texture = utils::loadSettingsIcon(trash_svg, trash_svg_size);
const auto add_button_texture = utils::loadSettingsIcon(plus_svg, plus_svg_size);
wxBoxSizer* appCheckboxContainer;
appCheckboxContainer = new wxBoxSizer(wxVERTICAL);
wxBoxSizer* appCheckboxContainer = new wxBoxSizer(wxVERTICAL);
wxBoxSizer* vSizer = new wxBoxSizer(wxVERTICAL);
enabledAppsContainer->Add(vSizer, 0, wxEXPAND);
auto settings = utils::getSettings();
for (const auto& app : settings.apps) {
addCheckboxToContainer(panel, appCheckboxContainer, frameSizer, size, delete_button_texture,
edit_button_texture, app);
for (auto app : settings.apps) {
auto checkbox = new wxCheckBox(this, wxID_ANY, app.appName, wxDefaultPosition, wxDefaultSize, 0);
checkbox->SetValue(app.enabled);
checkbox->SetClientData(new utils::App(app));
checkbox->Bind(wxEVT_CHECKBOX, [checkbox](wxCommandEvent& event) {
bool isChecked = checkbox->IsChecked();
utils::App* appData = static_cast<utils::App*>(checkbox->GetClientData());
appData->enabled = isChecked;
utils::saveSettings(appData);
});
checkbox->Bind(wxEVT_DESTROY, [checkbox](wxWindowDestroyEvent&) {
delete static_cast<utils::App*>(checkbox->GetClientData());
});
appCheckboxContainer->Add(checkbox, 0, wxALL, 5);
}
wxBoxSizer* checkboxRowSizer = new wxBoxSizer(wxHORIZONTAL);
auto anyOtherCheckbox = new wxCheckBox(panel, wxID_ANY, _("Any other"), wxDefaultPosition, wxDefaultSize, 0);
auto anyOtherCheckbox = new wxCheckBox(this, wxID_ANY, _("Any other"), wxDefaultPosition, wxDefaultSize, 0);
anyOtherCheckbox->SetValue(settings.anyOtherEnabled);
anyOtherCheckbox->Bind(wxEVT_CHECKBOX, [](wxCommandEvent& event) {
bool isChecked = event.IsChecked();
@ -447,50 +306,28 @@ public:
utils::saveSettings(settings);
});
wxBitmapButton* addButton = new wxBitmapButton(panel, wxID_ANY, add_button_texture);
addButton->SetToolTip(_("Add application"));
addButton->Bind(wxEVT_BUTTON, [this, panel, frameSizer, size, delete_button_texture, edit_button_texture,
appCheckboxContainer](wxCommandEvent& event) {
utils::App* app = new utils::App();
EditAppDialog dlg{this, _("Add new application"), app};
if (dlg.ShowModal() == wxID_OK) {
auto settings = utils::getSettings();
settings.apps.push_back(*app);
utils::saveSettings(settings);
addCheckboxToContainer(panel, appCheckboxContainer, frameSizer, size, delete_button_texture,
edit_button_texture, *app);
this->SetSizerAndFit(frameSizer);
wxSize currentSize = this->GetSize();
this->SetSize(size.GetWidth(), currentSize.GetHeight());
this->Layout();
} else
delete app;
});
appCheckboxContainer->Add(anyOtherCheckbox, 0, wxALL, 5);
checkboxRowSizer->Add(anyOtherCheckbox, 1, wxALL | wxALIGN_CENTER_VERTICAL);
checkboxRowSizer->Add(addButton, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, 5);
vSizer->Add(appCheckboxContainer, 1, wxEXPAND, 5);
vSizer->Add(checkboxRowSizer, 1, wxALL, 5);
enabledAppsContainer->Add(appCheckboxContainer, 1, wxEXPAND, 5);
mainContainer->Add(enabledAppsContainer, 0, 0, 5);
// enabled apps end
// LastFM start
auto lastfmDivider = new wxStaticLine(panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL);
auto lastfmDivider = new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL);
mainContainer->Add(lastfmDivider, 0, wxEXPAND | wxALL, 5);
wxBoxSizer* lastFMContainer;
lastFMContainer = new wxBoxSizer(wxHORIZONTAL);
auto lastfmText = new wxStaticText(panel, wxID_ANY, _("LastFM:"), wxDefaultPosition, wxDefaultSize, 0);
auto lastfmText = new wxStaticText(this, wxID_ANY, _("LastFM:"), wxDefaultPosition, wxDefaultSize, 0);
lastfmText->Wrap(-1);
lastFMContainer->Add(lastfmText, 0, wxALL, 5);
wxBoxSizer* lastfmSettingsContainer;
lastfmSettingsContainer = new wxBoxSizer(wxVERTICAL);
auto lastfmEnabledCheckbox = new wxCheckBox(panel, wxID_ANY, _("Enabled"), wxDefaultPosition, wxDefaultSize, 0);
auto lastfmEnabledCheckbox = new wxCheckBox(this, wxID_ANY, _("Enabled"), wxDefaultPosition, wxDefaultSize, 0);
lastfmEnabledCheckbox->SetValue(settings.lastfm.enabled);
lastfmEnabledCheckbox->Bind(wxEVT_CHECKBOX, [](wxCommandEvent& event) {
bool isChecked = event.IsChecked();
@ -501,8 +338,9 @@ public:
lastfmSettingsContainer->Add(lastfmEnabledCheckbox, 0, wxALIGN_CENTER | wxALL, 5);
lastFMContainer->Add(lastfmSettingsContainer, 1, wxEXPAND, 5);
auto usernameInput = new wxTextCtrl(panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0);
usernameInput->SetHint(_("Username"));
auto usernameInput =
new wxTextCtrlWithPlaceholder(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0);
usernameInput->SetPlaceholderText(_("Username"));
usernameInput->SetValue(settings.lastfm.username);
usernameInput->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) {
auto settings = utils::getSettings();
@ -510,9 +348,9 @@ public:
settings.lastfm.username = data;
utils::saveSettings(settings);
});
auto passwordInput =
new wxTextCtrl(panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_PASSWORD);
passwordInput->SetHint(_("Password"));
auto passwordInput = new wxTextCtrlWithPlaceholder(this, wxID_ANY, wxEmptyString, wxDefaultPosition,
wxDefaultSize, wxTE_PASSWORD);
passwordInput->SetPlaceholderText(_("Password"));
passwordInput->SetValue(settings.lastfm.password);
passwordInput->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) {
auto settings = utils::getSettings();
@ -520,8 +358,9 @@ public:
settings.lastfm.password = data;
utils::saveSettings(settings);
});
auto apikeyInput = new wxTextCtrl(panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0);
apikeyInput->SetHint(_("API-Key"));
auto apikeyInput =
new wxTextCtrlWithPlaceholder(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0);
apikeyInput->SetPlaceholderText(_("API-Key"));
apikeyInput->SetValue(settings.lastfm.api_key);
apikeyInput->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) {
auto settings = utils::getSettings();
@ -529,9 +368,9 @@ public:
settings.lastfm.api_key = data;
utils::saveSettings(settings);
});
auto apisecretInput =
new wxTextCtrl(panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_PASSWORD);
apisecretInput->SetHint(_("API-Secret"));
auto apisecretInput = new wxTextCtrlWithPlaceholder(this, wxID_ANY, wxEmptyString, wxDefaultPosition,
wxDefaultSize, wxTE_PASSWORD);
apisecretInput->SetPlaceholderText(_("API-Secret"));
apisecretInput->SetValue(settings.lastfm.api_secret);
apisecretInput->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) {
auto settings = utils::getSettings();
@ -540,7 +379,7 @@ public:
utils::saveSettings(settings);
});
auto checkButton = new wxButton(panel, wxID_ANY, _("Check credentials"));
auto checkButton = new wxButton(this, wxID_ANY, _("Check credentials"));
checkButton->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event) { initLastFM(true); });
mainContainer->Add(lastFMContainer, 0, 0, 5);
@ -553,7 +392,7 @@ public:
// Last FM End
// settings start
auto appsDivider = new wxStaticLine(panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL);
auto appsDivider = new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL);
mainContainer->Add(appsDivider, 0, wxEXPAND | wxALL, 5);
wxBoxSizer* settingsContainer;
@ -562,12 +401,12 @@ public:
wxBoxSizer* startupContainer;
startupContainer = new wxBoxSizer(wxHORIZONTAL);
auto startupText = new wxStaticText(panel, wxID_ANY, _("Startup:"), wxDefaultPosition, wxDefaultSize, 0);
auto startupText = new wxStaticText(this, wxID_ANY, _("Startup:"), wxDefaultPosition, wxDefaultSize, 0);
startupText->Wrap(-1);
startupContainer->Add(startupText, 0, wxALL, 5);
auto autostartCheckbox =
new wxCheckBox(panel, wxID_ANY, _("Launch at login"), wxDefaultPosition, wxDefaultSize, 0);
new wxCheckBox(this, wxID_ANY, _("Launch at login"), wxDefaultPosition, wxDefaultSize, 0);
autostartCheckbox->SetValue(settings.autoStart);
autostartCheckbox->Bind(wxEVT_CHECKBOX, [](wxCommandEvent& event) {
bool isChecked = event.IsChecked();
@ -578,7 +417,7 @@ public:
});
auto odesliCheckbox =
new wxCheckBox(panel, wxID_ANY, _("Odesli integration"), wxDefaultPosition, wxDefaultSize, 0);
new wxCheckBox(this, wxID_ANY, _("Odesli integration"), wxDefaultPosition, wxDefaultSize, 0);
odesliCheckbox->SetValue(settings.odesli);
odesliCheckbox->Bind(wxEVT_CHECKBOX, [](wxCommandEvent& event) {
bool isChecked = event.IsChecked();
@ -591,80 +430,17 @@ public:
settingsContainer->Add(odesliCheckbox, 0, wxALL, 5);
startupContainer->Add(settingsContainer);
mainContainer->Add(startupContainer, 0, wxEXPAND, 5);
// settings end
this->SetSizerAndFit(frameSizer);
// settings end
this->SetSizerAndFit(mainContainer);
wxSize currentSize = this->GetSize();
this->SetSize(size.GetWidth(), currentSize.GetHeight());
this->Layout();
this->Centre(wxBOTH);
panel->SetFocus();
}
private:
void addCheckboxToContainer(wxPanel* panel, wxBoxSizer* container, wxBoxSizer* frameSizer, const wxSize& size,
const wxBitmap& delete_button_texture, const wxBitmap& edit_button_texture,
const utils::App& app) {
wxBoxSizer* checkboxRowSizer = new wxBoxSizer(wxHORIZONTAL);
auto checkbox = new wxCheckBox(panel, wxID_ANY, app.appName, wxDefaultPosition, wxDefaultSize, 0);
checkbox->SetValue(app.enabled);
checkbox->SetClientData(new utils::App(app));
checkbox->Bind(wxEVT_CHECKBOX, [checkbox](wxCommandEvent& event) {
bool isChecked = checkbox->IsChecked();
utils::App* appData = static_cast<utils::App*>(checkbox->GetClientData());
appData->enabled = isChecked;
utils::saveSettings(appData);
});
checkbox->Bind(wxEVT_DESTROY, [checkbox](wxWindowDestroyEvent&) {
delete static_cast<utils::App*>(checkbox->GetClientData());
});
wxBitmapButton* editButton = new wxBitmapButton(panel, wxID_ANY, edit_button_texture);
editButton->SetToolTip(_("Edit application"));
editButton->Bind(wxEVT_BUTTON, [this, checkbox](wxCommandEvent& event) {
utils::App* appData = static_cast<utils::App*>(checkbox->GetClientData());
utils::App backupApp = *appData;
EditAppDialog dlg{this, _("Edit application") + " " + appData->appName, appData};
if (dlg.ShowModal() == wxID_OK) {
auto settings = utils::getSettings();
for (auto& app : settings.apps) {
if (app == backupApp)
app = *appData;
}
utils::saveSettings(settings);
checkbox->SetLabelText(appData->appName);
this->Layout();
}
});
wxBitmapButton* deleteButton = new wxBitmapButton(panel, wxID_ANY, delete_button_texture);
deleteButton->SetToolTip(_("Delete application"));
deleteButton->Bind(wxEVT_BUTTON,
[this, checkboxRowSizer, container, frameSizer, checkbox, size](wxCommandEvent& event) {
utils::App* appData = static_cast<utils::App*>(checkbox->GetClientData());
auto settings = utils::getSettings();
settings.apps.erase(std::find(settings.apps.begin(), settings.apps.end(), *appData));
utils::saveSettings(settings);
container->Detach(checkboxRowSizer);
this->CallAfter([this, checkboxRowSizer, frameSizer, size]() {
checkboxRowSizer->Clear(true);
this->SetSizerAndFit(frameSizer);
wxSize currentSize = this->GetSize();
this->SetSize(size.GetWidth(), currentSize.GetHeight());
this->Layout();
});
});
checkboxRowSizer->Add(checkbox, 1, wxALL | wxALIGN_CENTER_VERTICAL);
checkboxRowSizer->Add(editButton, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, 5);
checkboxRowSizer->Add(deleteButton, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, 5);
container->Add(checkboxRowSizer, 0, wxALL, 5);
}
};
class PlayerLink : public wxApp {
public:
virtual bool OnInit() override {
@ -677,8 +453,8 @@ public:
this->SetAppearance(wxAppBase::Appearance::Dark);
wxInitAllImageHandlers();
wxIcon tray_icon = utils::loadIconFromMemory(menubar_icon_png, menubar_icon_png_size);
PlayerLinkFrame* frame = new PlayerLinkFrame(nullptr, wxID_ANY, _("PlayerLink"));
wxIcon icon = utils::loadIconFromMemory(icon_png, icon_png_size);
PlayerLinkFrame* frame = new PlayerLinkFrame(nullptr, icon, wxID_ANY, _("PlayerLink"));
trayIcon = new PlayerLinkIcon(frame);
frame->Bind(wxEVT_CLOSE_WINDOW, [=](wxCloseEvent& event) {
if (event.CanVeto()) {
@ -688,7 +464,7 @@ public:
std::exit(0);
});
trayIcon->SetIcon(tray_icon, _("PlayerLink"));
trayIcon->SetIcon(icon, _("PlayerLink"));
return true;
}

View File

@ -4,6 +4,7 @@
#include <wx/mstream.h>
#include <wx/wx.h>
#include <wx/clipbrd.h>
#include <filesystem>
#include <fstream>
#include <nlohmann-json/single_include/nlohmann/json.hpp>
@ -20,14 +21,10 @@
namespace utils {
struct App {
bool enabled;
int type;
std::string appName;
std::string clientId;
std::string searchEndpoint;
std::vector<std::string> processNames;
bool operator==(const App& other) const {
return appName == other.appName && clientId == other.clientId && type == other.type;
}
};
struct LastFMSettings {
@ -59,57 +56,18 @@ namespace utils {
}
}
inline wxBitmap loadImageFromMemory(const unsigned char* data, size_t size, int width = 0, int height = 0) {
inline wxIcon loadIconFromMemory(const unsigned char* data, size_t size) {
wxMemoryInputStream stream(data, size);
wxImage img(stream, wxBITMAP_TYPE_PNG);
if (img.IsOk()) {
if (width != 0 || height != 0)
img.Rescale(width, height, wxIMAGE_QUALITY_HIGH);
wxBitmap bmp(img);
return bmp;
wxIcon icon;
icon.CopyFromBitmap(bmp);
return icon;
}
return wxNullBitmap;
return wxNullIcon;
}
inline wxIcon loadIconFromMemory(const unsigned char* data, size_t size, int width = 0, int height = 0) {
wxIcon icn{};
icn.CopyFromBitmap(loadImageFromMemory(data, size, width, height));
return icn;
}
inline wxBitmap loadColoredSVG(const unsigned char* svg, const unsigned int svg_size, const wxSize& size,
const wxColour& color) {
const std::string defaultColor = "currentColor";
std::string svg_data = std::string((const char*)svg, svg_size);
size_t start_pos = svg_data.find(defaultColor);
if (start_pos != std::string::npos)
svg_data.replace(start_pos, defaultColor.length(), color.GetAsString(wxC2S_HTML_SYNTAX));
wxBitmapBundle bundle = wxBitmapBundle::FromSVG(svg_data.c_str(), size);
if (!bundle.IsOk())
return wxNullBitmap;
wxBitmap bmp = bundle.GetBitmap(size);
if (!bmp.IsOk())
return wxNullBitmap;
return bmp;
}
inline wxBitmap loadSettingsIcon(const unsigned char* svg, const unsigned int svg_size,
const wxSize& size = wxSize(16, 16)) {
return loadColoredSVG(
svg, svg_size, size,
wxSystemSettings::GetAppearance().IsSystemDark() ? wxColor(255, 255, 255, 255) : wxColor(0, 0, 0, 255));
}
inline std::string toLower(const std::string& str) {
std::string lowerStr = str;
std::transform(lowerStr.begin(), lowerStr.end(), lowerStr.begin(),
[](unsigned char c) { return std::tolower(c); });
return lowerStr;
}
inline bool caseInsensitiveMatch(const std::string& a, const std::string& b) { return toLower(a) == toLower(b); }
inline std::string ltrim(std::string& s) {
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) { return !std::isspace(ch); }));
return s;
@ -186,23 +144,19 @@ namespace utils {
SongInfo ret{};
std::string response =
httpRequest("https://itunes.apple.com/search?media=music&entity=song&term=" + urlEncode(query));
try {
nlohmann::json j = nlohmann::json::parse(response);
auto results = j["results"];
if (results.size() > 0) {
ret.artworkURL = results[0]["artworkUrl100"].get<std::string>();
ret.trackId = results[0]["trackId"].get<int64_t>();
}
return ret;
} catch (...) {
return ret;
nlohmann::json j = nlohmann::json::parse(response);
auto results = j["results"];
if (results.size() > 0) {
ret.artworkURL = results[0]["artworkUrl100"].get<std::string>();
ret.trackId = results[0]["trackId"].get<int64_t>();
}
return ret;
}
inline std::string getOdesliURL(SongInfo& song) {
return std::string("https://song.link/i/" + std::to_string(song.trackId));
}
inline void saveSettings(const App* newApp) {
nlohmann::json j;
@ -270,7 +224,7 @@ namespace utils {
appJson["client_id"] = app.clientId;
appJson["search_endpoint"] = app.searchEndpoint;
appJson["enabled"] = app.enabled;
appJson["type"] = app.type;
for (const auto& processName : app.processNames) appJson["process_names"].push_back(processName);
j["apps"].push_back(appJson);
@ -315,9 +269,8 @@ namespace utils {
a.clientId = app.value("client_id", "");
a.searchEndpoint = app.value("search_endpoint", "");
a.enabled = app.value("enabled", false);
a.type = app.value("type", 2);
for (const auto& process : app.value("process_names", nlohmann::json())) a.processNames.push_back(process.get<std::string>());
for (const auto& process : app["process_names"]) a.processNames.push_back(process.get<std::string>());
ret.apps.push_back(a);
}
@ -330,7 +283,7 @@ namespace utils {
auto settings = getSettings();
for (auto app : settings.apps) {
for (auto procName : app.processNames) {
if (caseInsensitiveMatch(procName, processName))
if (procName == processName)
return app;
}
}
@ -338,7 +291,6 @@ namespace utils {
a.clientId = DEFAULT_CLIENT_ID;
a.appName = DEFAULT_APP_NAME;
a.enabled = settings.anyOtherEnabled;
a.type = 2; // Default to listening
a.searchEndpoint = "";
return a;
}

View File

@ -19,8 +19,6 @@ set(wxBUILD_SHARED OFF)
set(wxBUILD_MONOLITHIC ON)
set(wxUSE_GUI ON)
set(wxUSE_WEBVIEW OFF)
set(wxUSE_UNICODE_UTF8 ON)
set(wxUSE_UTF8_LOCALE_ONLY ON)
add_subdirectory("wxWidgets")
if(UNIX AND NOT APPLE)
add_subdirectory("dbus")

2
vendor/discord-rpc vendored

@ -1 +1 @@
Subproject commit 6bc42071f109083178ae824898ca4b3cb40a8307
Subproject commit e86d7a81de7a33323e2038182ab53a26c69f7880

2
vendor/wxWidgets vendored

@ -1 +1 @@
Subproject commit d7a696de4c301948f02dc5d7979f867f07f9d684
Subproject commit 138937b7775c117b57f55374a0c507a35a1102f6

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 260 KiB