Compare commits

...

44 Commits
v1.0 ... main

Author SHA1 Message Date
EinTim23 7d4bc6dd89 - fix lastfm scrobbling on disabled apps 2025-05-13 14:08:29 +02:00
EinTim23 fa29020cea - auto retry last fm on fail 2025-05-13 14:01:01 +02:00
EinTim23 36be257462 - fix lastfm crash if internet is not present 2025-05-13 13:41:34 +02:00
EinTim23 958def7527 updated screenshots 2025-05-13 00:57:04 +02:00
EinTim23 28a198983c experimental arm64 linux 2025-05-12 23:34:32 +02:00
EinTim23 374a95e68d new icon 2025-05-12 23:00:53 +02:00
EinTim23 49e1bb0262 try to fix arm64 github actions build 2025-05-12 22:36:28 +02:00
EinTim23 cfecd99858 set specific target for github actions 2025-05-12 21:48:44 +02:00
EinTim23 c805dfe829 bump discord-rpc for windows arm64 compatibility 2025-05-12 21:36:42 +02:00
EinTim23 c3bf12a997 - add windows arm64 build 2025-05-12 16:45:22 +02:00
EinTim23 5428f57891 - add tooltips to buttons
- fix alignment on mac os
- repositioned add application button
2025-05-11 19:09:08 +02:00
EinTim23 66a25e62f2 add proper exception handling to winrt 2025-05-10 22:13:36 +02:00
EinTim23 6807fe00fa convenience improvement: automatically add last detected app to applist when you create a new app 2025-05-10 21:58:27 +02:00
EinTim23 4e57f272a1 removed unnecessary padding from processBox 2025-05-10 21:47:32 +02:00
EinTim23 7fc1ac5d5d - fix crash if process_names are ommitted
- minor ui improvements
2025-05-10 21:43:01 +02:00
EinTim23 a6f5d3277b Ui Update done 2025-05-10 21:18:11 +02:00
EinTim23 563080643f - added svg icon handling
- began ui update to manage apps from ui
- force utf8 only mode
- new about dialog
2025-05-10 17:26:35 +02:00
EinTim23 90607d2f5d - add support for different activity types
- switch from process naming matching to app user model id matching on windows
- updated example settings file
- added exception handling to itunes api
2025-05-10 12:27:15 +02:00
EinTim23 cd5656de63 fix crashing on mac os due to invalid stderror handle 2025-05-10 12:00:09 +02:00
EinTim c53f02b35c
Revert the change 2025-05-03 23:23:04 +02:00
EinTim dbbec3b027
Improve github actions build times 2025-05-03 23:14:35 +02:00
EinTim23 bccf4c6100 add old mediaremote way of getting playback data, as osascript way only works on sonoma and newer. 2025-05-03 22:59:52 +02:00
EinTim23 349058c32d migrated our script to jxa for cleaner code and proper json serialization 2025-05-03 22:08:03 +02:00
EinTim23 9821b6a294 fix mac os 15.4 support because apple restricted the mediaremote to processes that hold internal entitlements 2025-05-03 21:07:39 +02:00
EinTim23 8fb4af3a49 get rid of unsupported runner 2025-05-03 02:16:08 +02:00
EinTim23 191fd2c375 Bump wxWidgets 2025-05-03 02:13:19 +02:00
EinTim23 c35416c928 feat: odesli/song.link integration 2025-01-16 13:56:29 +01:00
EinTim23 be3a50d42a updated readme images 2025-01-12 21:31:18 +01:00
EinTim23 aa734cf45a fixed linux app image build 2025-01-12 20:54:26 +01:00
EinTim23 cbc96614d0 fixed placeholder text on password inputs 2025-01-12 20:17:00 +01:00
EinTim23 e8ac296f35 automatic release creation 2025-01-12 15:02:15 +01:00
EinTim23 a6c77e256f add automated mac os build 2025-01-12 14:02:56 +01:00
EinTim23 3e16670a70 add automated windows build 2025-01-12 13:22:02 +01:00
EinTim23 017b21dbeb automated linux build 2025-01-12 12:40:29 +01:00
EinTim23 d13231e84e - Fix: settings window not auto focussing on mac os
- Optimized the windows backend
- Updated dependencies
2025-01-11 15:50:39 +01:00
EinTim23 4a68eba106 cleaned up the checkboxes 2024-11-25 21:21:21 +01:00
EinTim23 6f29111c40 lastfm gui 2024-11-25 20:21:05 +01:00
EinTim23 35d0ce747b feat: lastfm support (still missing gui) 2024-11-20 22:51:15 +01:00
EinTim23 bb093cdefe fix process detection on newer windows 11 builds 2024-11-20 16:44:21 +01:00
EinTim23 26af0323f2 Save config in a proper location 2024-11-08 19:57:34 +01:00
EinTim 7b5666a906
Remove doubled semicolon 2024-11-07 00:28:32 +01:00
EinTim23 322ceeaddb ensure that the autostart directories actually exist 2024-11-07 00:27:20 +01:00
EinTim 0c274c55c5
MACOS: fix set CMAKE_OSX_DEPLOYMENT_TARGET 2024-11-06 22:28:59 +01:00
EinTim23 31afe3372d LINUX: add system bus support 2024-11-06 19:03:01 +01:00
32 changed files with 2070 additions and 514 deletions

View File

@ -8,3 +8,4 @@ IndentCaseLabels: false
AccessModifierOffset: -4
ColumnLimit: 120
NamespaceIndentation: All
SortIncludes: false

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

@ -0,0 +1,292 @@
name: Build and Package AppImage, Windows Executable, and macOS DMG
on:
push:
branches:
- main
tags:
- 'v*'
pull_request:
jobs:
build-linux:
runs-on: ubuntu-22.04
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-x86_64.AppImage"
wget "https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh"
chmod +x linuxdeploy-x86_64.AppImage linuxdeploy-plugin-gtk.sh
sudo mv linuxdeploy-x86_64.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
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
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: Configure with CMake
run: cmake -B build -S . -DCMAKE_BUILD_TYPE=Release
- name: Build the project
run: cmake --build build --config Release --target PlayerLink
- 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
path: build/Release/*
build-macos:
runs-on: macos-15
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: 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 $(sysctl -n hw.physicalcpu)
- name: Create DMG package
run: |
mkdir -p dmg/PlayerLink
cp -r build/PlayerLink.app dmg/PlayerLink/
hdiutil create -volname "PlayerLink" -srcfolder dmg/PlayerLink -ov -format UDZO PlayerLink.dmg
- name: Upload macOS DMG artifact
uses: actions/upload-artifact@v4
with:
name: PlayerLink-macOS-DMG
path: PlayerLink.dmg
create-release:
if: startsWith(github.ref, 'refs/tags/')
needs: [build-linux, build-linux-arm64, build-windows, build-windows-arm64, build-macos]
runs-on: ubuntu-latest
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: ./release-assets
merge-multiple: true
- name: Display structure of downloaded files
run: ls -R ./release-assets
- name: Create GitHub Release
uses: actions/create-release@v1
id: create_release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref_name }}
release_name: Release ${{ github.ref_name }}
draft: false
prerelease: false
- name: Upload 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-x86_64.AppImage
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:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./release-assets/PlayerLink.exe
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:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./release-assets/PlayerLink.dmg
asset_name: PlayerLink.dmg
asset_content_type: application/octet-stream

6
.gitignore vendored
View File

@ -4,3 +4,9 @@ build/*
src/rsrc.hpp
PlayerLink.exe
.vscode/*
linux/AppDir/usr/share/doc/*
linux/AppDir/usr/lib/*
linux/AppDir/usr/bin/*
linux/AppDir/*
!linux/AppDir/usr
!.gitkeep

View File

@ -1,13 +1,13 @@
cmake_minimum_required (VERSION 3.8)
cmake_minimum_required (VERSION 3.12)
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)
list(APPEND SOURCES "src/backends/darwin.mm" ${CMAKE_SOURCE_DIR}/osx/icon.icns ${CMAKE_SOURCE_DIR}/osx/MediaRemote.js)
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "" FORCE)
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15")
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING "" FORCE)
project ("PlayerLink" LANGUAGES C CXX OBJCXX)
else()
project ("PlayerLink" LANGUAGES C CXX)
@ -40,6 +40,7 @@ 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

@ -7,7 +7,7 @@ Cross platform, universal discord rich presence for media players.
- Pretty much any linux distribution with gtk3 and dbus support
## Showcase
You can add predefined players to the settings.json to customise the name it shows in discord, edit the search button base url, and app icon. By default it will just display as "Music" without a search button or app icon. In the future I want to add an option to the ui to add custom apps.
You can add predefined players to the settings.json to customise the name it shows in discord, edit the search button base url, and app icon. By default it will just display as "Music" without a search button or app icon.
<p align="center" width="100%">
<img src="img/showcase.png" alt="rich presence" />
@ -32,8 +32,15 @@ The Mac OS backend is powered by the private MediaRemote framework. It provides
### Linux
The linux backend is powered by [MPRIS](https://specifications.freedesktop.org/mpris-spec/latest/). It allows to query the system wide media information via dbus.
## Adding custom apps to the settings.json
The config is currently located in the same folder as PlayerLink, this will be changed in a future release. An example on how to add custom apps to the json can be found [here](./settings.example.json). In the future there will be a UI to configure custom apps in a more user friendly way.
## Config
**Mac OS**
`~/Library/Application Support/PlayerLink`
**Linux**
`~/.config/PlayerLink`
**Windows**
`%appdata%\PlayerLink`
An example on how to add custom apps to the config can be found [here](./settings.example.json). In the future there will be a UI to configure custom apps in a more user friendly way.
## Building
@ -73,13 +80,13 @@ The config is currently located in the same folder as PlayerLink, this will be c
4. Build the project :)
```bash
# for a release build
cmake --build build --config Release
cmake --build build --config Release --target PlayerLink
# for a debug build
cmake --build build
cmake --build build --target PlayerLink
```
## 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 Aurora's MusicRPC](https://github.com/AlexandraAurora/MusicRPC) and her project may provide a better experience when being on Mac OS only.
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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 248 KiB

After

Width:  |  Height:  |  Size: 692 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 39 KiB

View File

View File

View File

@ -0,0 +1,6 @@
[Desktop Entry]
Name=PlayerLink
Exec=PlayerLink
Icon=PlayerLink
Type=Application
Categories=Utility;GTK;

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

33
osx/MediaRemote.js Normal file
View File

@ -0,0 +1,33 @@
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: 285 KiB

After

Width:  |  Height:  |  Size: 817 KiB

BIN
rsrc/menubar_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

3
rsrc/pencil.svg Normal file
View File

@ -0,0 +1,3 @@
<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>

After

Width:  |  Height:  |  Size: 367 B

3
rsrc/plus.svg Normal file
View File

@ -0,0 +1,3 @@
<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>

After

Width:  |  Height:  |  Size: 239 B

3
rsrc/trash.svg Normal file
View File

@ -0,0 +1,3 @@
<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>

After

Width:  |  Height:  |  Size: 751 B

View File

@ -5,13 +5,34 @@
"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

@ -4,6 +4,7 @@
#include <memory>
#include <string>
#include <filesystem>
struct MediaInfo {
bool paused;
@ -30,6 +31,7 @@ struct MediaInfo {
namespace backend {
bool init();
bool toggleAutostart(bool enabled);
std::filesystem::path getConfigDirectory();
std::shared_ptr<MediaInfo> getMediaInformation();
} // namespace backend

View File

@ -1,9 +1,11 @@
#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"
@ -16,77 +18,141 @@ 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() {
__block NSString *appName = nil;
__block NSDictionary *playingInfo = nil;
// 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]);
dispatch_group_t group = dispatch_group_create();
std::string appName = j["player"].get<std::string>();
if (appName == "none")
return nullptr;
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);
});
bool paused = j["playbackStatus"].get<int>() == 0;
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 songTitle = j["title"].get<std::string>();
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
dispatch_release(group);
if (appName == nil || playingInfo == nil)
return nullptr;
std::string songAlbum = j["album"].get<std::string>();
bool paused = [playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoPlaybackRate] intValue] == 0;
std::string songArtist = j["artist"].get<std::string>();
NSString *title = playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoTitle];
std::string songTitle = title ? [title UTF8String] : "";
int64_t elapsedTimeMs = 0;
int64_t durationMs = 0;
try {
double durationNumber = j["duration"].get<double>();
durationMs = static_cast<int64_t>(durationNumber * 1000);
NSString *album = playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoAlbum];
std::string songAlbum = album ? [album UTF8String] : "";
double elapsedTimeNumber = j["elapsed"].get<double>();
elapsedTimeMs = static_cast<int64_t>(elapsedTimeNumber * 1000);
} catch (...) {
}
NSString *artist = playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoArtist];
std::string songArtist = artist ? [artist UTF8String] : "";
return std::make_shared<MediaInfo>(paused, songTitle, songArtist, songAlbum, appName, "", durationMs,
elapsedTimeMs);
} else {
__block NSString *appName = nil;
__block NSDictionary *playingInfo = nil;
NSData *artworkData = playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoArtworkData];
dispatch_group_t group = dispatch_group_create();
std::string thumbnailData;
if (artworkData)
thumbnailData = std::string((const char *)[artworkData bytes], [artworkData length]);
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);
});
NSNumber *elapsedTimeNumber = playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoElapsedTime];
dispatch_group_enter(group);
MRMediaRemoteGetNowPlayingInfo(dispatch_get_main_queue(), ^(CFDictionaryRef result) {
if (result)
playingInfo = [[(__bridge NSDictionary *)result copy] retain];
dispatch_group_leave(group);
});
NSNumber *durationNumber = playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoDuration];
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
dispatch_release(group);
if (appName == nil || playingInfo == nil)
return nullptr;
int64_t elapsedTimeMs = elapsedTimeNumber ? static_cast<int64_t>([elapsedTimeNumber doubleValue] * 1000) : 0;
bool paused = [playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoPlaybackRate] intValue] == 0;
int64_t durationMs = durationNumber ? static_cast<int64_t>([durationNumber doubleValue] * 1000) : 0;
NSString *title = playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoTitle];
std::string songTitle = title ? [title UTF8String] : "";
std::string appNameString = appName.UTF8String;
NSString *album = playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoAlbum];
std::string songAlbum = album ? [album UTF8String] : "";
[appName release];
[playingInfo release];
return std::make_shared<MediaInfo>(paused, songTitle, songArtist, songAlbum, appNameString, thumbnailData,
durationMs, elapsedTimeMs);
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);
}
}
std::filesystem::path backend::getConfigDirectory() {
std::filesystem::path configDirectoryPath = std::getenv("HOME");
configDirectoryPath = configDirectoryPath / "Library" / "Application Support" / "PlayerLink";
return configDirectoryPath;
}
bool backend::toggleAutostart(bool enabled) {
std::filesystem::path launchAgentPath = std::getenv("HOME");
launchAgentPath = launchAgentPath / "Library" / "LaunchAgents" / "PlayerLink.plist";
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

@ -12,6 +12,9 @@
DBusConnection* conn = nullptr;
std::string getExecutablePath() {
if (const char* appImagePath = std::getenv("APPIMAGE"))
return std::string(appImagePath);
char result[PATH_MAX]{};
ssize_t count = readlink("/proc/self/exe", result, PATH_MAX);
return (count != -1) ? std::string(result, count) : std::string();
@ -189,8 +192,17 @@ bool backend::init() {
conn = dbus_bus_get(DBUS_BUS_SESSION, &err);
if (!conn) {
dbus_error_free(&err);
return false;
if (dbus_error_is_set(&err))
dbus_error_free(&err);
//fallback to system bus if user doesn't have a session specific bus
conn = dbus_bus_get(DBUS_BUS_SYSTEM, &err);
if(!conn) {
if (dbus_error_is_set(&err))
dbus_error_free(&err);
return false;
}
}
return true;
}
@ -206,6 +218,12 @@ std::shared_ptr<MediaInfo> backend::getMediaInformation() {
return std::make_shared<MediaInfo>(ret);
}
std::filesystem::path backend::getConfigDirectory() {
std::filesystem::path configDirectoryPath = std::getenv("HOME");
configDirectoryPath = configDirectoryPath / ".config" / "PlayerLink";
return configDirectoryPath;
}
bool backend::toggleAutostart(bool enabled) {
const char* xdgHome = std::getenv("XDG_CONFIG_HOME");

View File

@ -1,15 +1,16 @@
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <objbase.h>
#include <psapi.h>
#include <shlobj.h>
#include <windows.h>
#include <appmodel.h>
#include <winrt/windows.foundation.h>
#include <winrt/windows.foundation.metadata.h>
#include <winrt/windows.media.control.h>
#include <winrt/windows.storage.streams.h>
#include <chrono>
#include <codecvt>
#include <filesystem>
#include "../backend.hpp"
@ -19,11 +20,19 @@ using namespace winrt;
using namespace Windows::Media::Control;
using namespace Windows::Storage::Streams;
#define EM_DASH "\xE2\x80\x94"
// codecvt is deprecated, but there is no good portable way to do this, I could technically use the winapi as this is
// the windows backend tho
std::string toStdString(winrt::hstring in) {
std::wstring_convert<std::codecvt_utf8<wchar_t>> converter;
return converter.to_bytes(in.c_str());
std::string toStdString(winrt::hstring& in) {
const wchar_t* wideStr = in.c_str();
int wideStrLen = static_cast<int>(in.size());
int bufferSize = WideCharToMultiByte(CP_UTF8, 0, wideStr, wideStrLen, nullptr, 0, nullptr, nullptr);
if (bufferSize <= 0)
return "";
std::string result(bufferSize, 0);
WideCharToMultiByte(CP_UTF8, 0, wideStr, wideStrLen, result.data(), bufferSize, nullptr, nullptr);
return result;
}
bool CreateShortcut(std::string source, std::string target) {
@ -54,9 +63,18 @@ bool CreateShortcut(std::string source, std::string target) {
return SUCCEEDED(hr);
}
std::filesystem::path backend::getConfigDirectory() {
std::filesystem::path configDirectoryPath = std::getenv("APPDATA");
configDirectoryPath = configDirectoryPath / "PlayerLink";
return configDirectoryPath;
}
bool backend::toggleAutostart(bool enabled) {
std::filesystem::path shortcutPath = std::getenv("APPDATA");
shortcutPath = shortcutPath / "Microsoft" / "Windows" / "Start Menu" / "Programs" / "Startup" / "PlayerLink.lnk";
shortcutPath = shortcutPath / "Microsoft" / "Windows" / "Start Menu" / "Programs" / "Startup";
std::filesystem::create_directories(shortcutPath);
shortcutPath = shortcutPath / "PlayerLink.lnk";
if (!enabled && std::filesystem::exists(shortcutPath)) {
std::filesystem::remove(shortcutPath);
return true;
@ -68,54 +86,61 @@ bool backend::toggleAutostart(bool enabled) {
}
std::shared_ptr<MediaInfo> backend::getMediaInformation() {
auto sessionManager = GlobalSystemMediaTransportControlsSessionManager::RequestAsync().get();
static auto sessionManager = GlobalSystemMediaTransportControlsSessionManager::RequestAsync().get();
auto currentSession = sessionManager.GetCurrentSession();
if (!currentSession)
return nullptr;
auto playbackInfo = currentSession.GetPlaybackInfo();
auto mediaProperties = currentSession.TryGetMediaPropertiesAsync().get();
auto timelineInformation = currentSession.GetTimelineProperties();
if (!mediaProperties)
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 (...) {
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);
}
return std::make_shared<MediaInfo>(
playbackInfo.PlaybackStatus() == GlobalSystemMediaTransportControlsSessionPlaybackStatus::Paused,
toStdString(mediaProperties.Title()), artist, albumName, toStdString(currentSession.SourceAppUserModelId()),
thumbnailData, endTime, elapsedTime);
}
bool backend::init() {

88
src/lastfm.hpp Normal file
View File

@ -0,0 +1,88 @@
#ifndef _LASTFM_
#define _LASTFM_
#include <md5.hpp>
#include <string>
#include "utils.hpp"
class LastFM {
public:
enum LASTFM_STATUS {
SUCCESS = 0,
AUTHENTICATION_FAILED = 4,
INVALID_API_KEY = 10,
RATE_LIMIT_REACHED = 29,
API_KEY_SUSPENDED = 26,
UNKNOWN_ERROR = 16,
INVALID_SESSION_KEY = 9,
SERVICE_TEMPORARILY_UNAVAILABLE = 13,
};
LastFM(std::string u, std::string p, std::string ak, std::string as)
: username(u), password(p), api_key(ak), api_secret(as), authenticated(false) {}
std::string getApiSignature(const std::map<std::string, std::string>& parameters) {
std::string unhashedSignature = "";
std::map<std::string, std::string> sortedParameters = parameters;
for (const auto& parameter : sortedParameters) {
if (parameter.first == "format" || parameter.first == "callback")
continue;
unhashedSignature += parameter.first + parameter.second;
}
unhashedSignature += api_secret;
return md5::md5_hash_hex(unhashedSignature);
}
LASTFM_STATUS authenticate() {
std::map<std::string, std::string> parameters = {{"api_key", api_key},
{"password", password},
{"username", username},
{"method", "auth.getMobileSession"},
{"format", "json"}};
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>();
session_token = j["session"]["key"].get<std::string>();
authenticated = true;
return LASTFM_STATUS::SUCCESS;
} catch (...) {
return LASTFM_STATUS::UNKNOWN_ERROR;
}
}
LASTFM_STATUS scrobble(std::string artist, std::string track) {
if (!authenticated)
return LASTFM_STATUS::AUTHENTICATION_FAILED;
std::map<std::string, std::string> parameters = {
{"api_key", api_key}, {"method", "track.scrobble"},
{"sk", session_token}, {"artist", artist},
{"track", track}, {"timestamp", std::to_string(time(NULL))},
{"format", "json"}};
parameters["api_sig"] = getApiSignature(parameters);
std::string postBody = utils::getURLEncodedPostBody(parameters);
std::string response = utils::httpRequest(api_base, "POST", postBody);
return LASTFM_STATUS::SUCCESS;
}
private:
bool authenticated;
std::string session_token;
std::string username;
std::string password;
std::string api_key;
std::string api_secret;
const std::string api_base = "https://ws.audioscrobbler.com/2.0/";
};
#endif

View File

@ -3,18 +3,24 @@
#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 = "";
std::string currentSongTitle = "";
utils::SongInfo songInfo;
LastFM* lastfm = nullptr;
void handleRPCTasks() {
while (true) {
@ -36,11 +42,33 @@ void handleRPCTasks() {
}
}
void initLastFM(bool checkMode = false) {
if (lastfm)
delete lastfm;
auto settings = utils::getSettings();
if (!settings.lastfm.enabled && !checkMode)
return;
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)
wxMessageBox(_("The LastFM authentication was successful."), _("PlayerLink"), wxOK | wxICON_INFORMATION);
}
void handleMediaTasks() {
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) {
currentSongTitle = "";
Discord_ClearPresence(); // Nothing is playing rn, clear presence
@ -49,7 +77,6 @@ void handleMediaTasks() {
if (mediaInformation->paused) {
lastMs = 0;
lastPlayingSong = "";
currentSongTitle = "";
Discord_ClearPresence();
continue;
@ -66,9 +93,6 @@ void handleMediaTasks() {
if (shouldContinue)
continue;
lastPlayingSong = currentlyPlayingSong;
currentSongTitle = mediaInformation->songArtist + " - " + mediaInformation->songTitle;
std::string currentMediaSource = mediaInformation->playbackSource;
if (currentMediaSource != lastMediaSource) {
@ -78,6 +102,14 @@ 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;
@ -86,19 +118,19 @@ void handleMediaTasks() {
std::string activityState = "by " + mediaInformation->songArtist;
DiscordRichPresence activity{};
activity.type = ActivityType::LISTENING;
activity.type = app.type;
activity.details = mediaInformation->songTitle.c_str();
activity.state = activityState.c_str();
activity.smallImageText = serviceName.c_str();
std::string artworkURL = utils::getArtworkURL(mediaInformation->songTitle + " " + mediaInformation->songArtist +
" " + mediaInformation->songAlbum);
songInfo = utils::getSongInfo(mediaInformation->songTitle + " " + mediaInformation->songArtist + " " +
mediaInformation->songAlbum);
activity.smallImageKey = "appicon";
if (artworkURL == "") {
if (songInfo.artworkURL == "") {
activity.smallImageKey = "";
activity.largeImageKey = "appicon";
} else {
activity.largeImageKey = artworkURL.c_str();
activity.largeImageKey = songInfo.artworkURL.c_str();
}
activity.largeImageText = mediaInformation->songAlbum.c_str();
@ -118,31 +150,221 @@ void handleMediaTasks() {
activity.button1link = buttonText.c_str();
}
std::string odesliUrl = utils::getOdesliURL(songInfo);
if (settings.odesli && songInfo.artworkURL != "") {
activity.button2name = "Show on Song.link";
activity.button2link = odesliUrl.c_str();
}
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) {}
void OnMenuOpen(wxCommandEvent& evt) { settingsFrame->Show(true); }
void OnMenuExit(wxCommandEvent& evt) { settingsFrame->Close(true); }
void OnMenuAbout(wxCommandEvent& evt) {
wxMessageBox(_("Made with <3 by EinTim"), _("PlayerLink"), wxOK | wxICON_INFORMATION);
}
PlayerLinkIcon(wxFrame* s) : settingsFrame(s), aboutDlg(nullptr) {}
protected:
virtual wxMenu* CreatePopupMenu() override {
wxMenu* menu = new wxMenu;
menu->Append(10004, currentSongTitle == "" ? _("Not Playing") : wxString::FromUTF8(currentSongTitle));
menu->Append(10004, currentSongTitle == "" ? _("Not Playing") : currentSongTitle);
menu->Enable(10004, false);
menu->AppendSeparator();
menu->Append(10005, _("Copy Odesli URL"));
if (songInfo.artworkURL == "" || currentSongTitle == "")
menu->Enable(10005, false);
menu->Append(10001, _("Settings"));
menu->Append(10003, _("About PlayerLink"));
menu->AppendSeparator();
menu->Append(10002, _("Quit PlayerLink..."));
Bind(wxEVT_MENU, &PlayerLinkIcon::OnCopyOdesliURL, this, 10005);
Bind(wxEVT_MENU, &PlayerLinkIcon::OnMenuOpen, this, 10001);
Bind(wxEVT_MENU, &PlayerLinkIcon::OnMenuExit, this, 10002);
Bind(wxEVT_MENU, &PlayerLinkIcon::OnMenuAbout, this, 10003);
@ -150,112 +372,299 @@ 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 PlayerLinkFrame : public wxFrame {
protected:
wxStaticText* settingsText;
wxStaticLine* settingsDivider;
wxStaticText* enabledAppsText;
wxCheckBox* anyOtherCheckbox;
wxStaticLine* appsDivider;
wxStaticText* startupText;
wxCheckBox* autostartCheckbox;
public:
PlayerLinkFrame(wxWindow* parent, wxIcon& icon, wxWindowID id = wxID_ANY, const wxString& title = wxEmptyString,
PlayerLinkFrame(wxWindow* parent, 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);
this->SetIcon(icon);
SetWindowIcon(this);
wxBoxSizer* mainContainer;
mainContainer = new wxBoxSizer(wxVERTICAL);
auto mainContainer = new wxBoxSizer(wxVERTICAL);
wxPanel* panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL);
panel->SetSizer(mainContainer);
settingsText = new wxStaticText(this, wxID_ANY, _("Settings"), wxDefaultPosition, wxDefaultSize, 0);
wxBoxSizer* frameSizer = new wxBoxSizer(wxVERTICAL);
frameSizer->Add(panel, 1, wxEXPAND);
// header start
auto settingsText = new wxStaticText(panel, wxID_ANY, _("Settings"), wxDefaultPosition, wxDefaultSize, 0);
settingsText->Wrap(-1);
mainContainer->Add(settingsText, 0, wxALIGN_CENTER | wxALL, 5);
// header end
settingsDivider = new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL);
// enabled apps start
auto settingsDivider = new wxStaticLine(panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL);
mainContainer->Add(settingsDivider, 0, wxEXPAND | wxALL, 5);
wxBoxSizer* enabledAppsContainer;
enabledAppsContainer = new wxBoxSizer(wxHORIZONTAL);
enabledAppsText = new wxStaticText(this, wxID_ANY, _("Enabled Apps:"), wxDefaultPosition, wxDefaultSize, 0);
auto enabledAppsText =
new wxStaticText(panel, wxID_ANY, _("Enabled Apps:"), wxDefaultPosition, wxDefaultSize, 0);
enabledAppsText->Wrap(-1);
enabledAppsContainer->Add(enabledAppsText, 0, wxALL, 5);
wxBoxSizer* appCheckboxContainer;
appCheckboxContainer = new wxBoxSizer(wxVERTICAL);
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 = new wxBoxSizer(wxVERTICAL);
wxBoxSizer* vSizer = new wxBoxSizer(wxVERTICAL);
enabledAppsContainer->Add(vSizer, 0, wxEXPAND);
auto settings = utils::getSettings();
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);
for (const auto& app : settings.apps) {
addCheckboxToContainer(panel, appCheckboxContainer, frameSizer, size, delete_button_texture,
edit_button_texture, app);
}
anyOtherCheckbox = new wxCheckBox(this, wxID_ANY, _("Any other"), wxDefaultPosition, wxDefaultSize, 0);
wxBoxSizer* checkboxRowSizer = new wxBoxSizer(wxHORIZONTAL);
auto anyOtherCheckbox = new wxCheckBox(panel, wxID_ANY, _("Any other"), wxDefaultPosition, wxDefaultSize, 0);
anyOtherCheckbox->SetValue(settings.anyOtherEnabled);
anyOtherCheckbox->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent& event) {
bool isChecked = this->anyOtherCheckbox->IsChecked();
anyOtherCheckbox->Bind(wxEVT_CHECKBOX, [](wxCommandEvent& event) {
bool isChecked = event.IsChecked();
auto settings = utils::getSettings();
settings.anyOtherEnabled = isChecked;
utils::saveSettings(settings);
});
appCheckboxContainer->Add(anyOtherCheckbox, 0, wxALL, 5);
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;
});
enabledAppsContainer->Add(appCheckboxContainer, 1, wxEXPAND, 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);
mainContainer->Add(enabledAppsContainer, 0, 0, 5);
// enabled apps end
appsDivider = new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL);
// LastFM start
auto lastfmDivider = new wxStaticLine(panel, 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);
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);
lastfmEnabledCheckbox->SetValue(settings.lastfm.enabled);
lastfmEnabledCheckbox->Bind(wxEVT_CHECKBOX, [](wxCommandEvent& event) {
bool isChecked = event.IsChecked();
auto settings = utils::getSettings();
settings.lastfm.enabled = isChecked;
utils::saveSettings(settings);
});
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"));
usernameInput->SetValue(settings.lastfm.username);
usernameInput->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) {
auto settings = utils::getSettings();
std::string data = event.GetString().ToStdString();
settings.lastfm.username = data;
utils::saveSettings(settings);
});
auto passwordInput =
new wxTextCtrl(panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_PASSWORD);
passwordInput->SetHint(_("Password"));
passwordInput->SetValue(settings.lastfm.password);
passwordInput->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) {
auto settings = utils::getSettings();
std::string data = event.GetString().ToStdString();
settings.lastfm.password = data;
utils::saveSettings(settings);
});
auto apikeyInput = new wxTextCtrl(panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0);
apikeyInput->SetHint(_("API-Key"));
apikeyInput->SetValue(settings.lastfm.api_key);
apikeyInput->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) {
auto settings = utils::getSettings();
std::string data = event.GetString().ToStdString();
settings.lastfm.api_key = data;
utils::saveSettings(settings);
});
auto apisecretInput =
new wxTextCtrl(panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_PASSWORD);
apisecretInput->SetHint(_("API-Secret"));
apisecretInput->SetValue(settings.lastfm.api_secret);
apisecretInput->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) {
auto settings = utils::getSettings();
std::string data = event.GetString().ToStdString();
settings.lastfm.api_secret = data;
utils::saveSettings(settings);
});
auto checkButton = new wxButton(panel, wxID_ANY, _("Check credentials"));
checkButton->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event) { initLastFM(true); });
mainContainer->Add(lastFMContainer, 0, 0, 5);
mainContainer->Add(usernameInput, 0, wxEXPAND | wxALL, 5);
mainContainer->Add(passwordInput, 0, wxEXPAND | wxALL, 5);
mainContainer->Add(apikeyInput, 0, wxEXPAND | wxALL, 5);
mainContainer->Add(apisecretInput, 0, wxEXPAND | wxALL, 5);
mainContainer->Add(checkButton, 0, wxEXPAND | wxALL, 10);
// Last FM End
// settings start
auto appsDivider = new wxStaticLine(panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL);
mainContainer->Add(appsDivider, 0, wxEXPAND | wxALL, 5);
wxBoxSizer* settingsContainer;
settingsContainer = new wxBoxSizer(wxHORIZONTAL);
settingsContainer = new wxBoxSizer(wxVERTICAL);
startupText = new wxStaticText(this, wxID_ANY, _("Startup:"), wxDefaultPosition, wxDefaultSize, 0);
wxBoxSizer* startupContainer;
startupContainer = new wxBoxSizer(wxHORIZONTAL);
auto startupText = new wxStaticText(panel, wxID_ANY, _("Startup:"), wxDefaultPosition, wxDefaultSize, 0);
startupText->Wrap(-1);
settingsContainer->Add(startupText, 0, wxALL, 5);
startupContainer->Add(startupText, 0, wxALL, 5);
autostartCheckbox = new wxCheckBox(this, wxID_ANY, _("Launch at login"), wxDefaultPosition, wxDefaultSize, 0);
auto autostartCheckbox =
new wxCheckBox(panel, wxID_ANY, _("Launch at login"), wxDefaultPosition, wxDefaultSize, 0);
autostartCheckbox->SetValue(settings.autoStart);
autostartCheckbox->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent& event) {
bool isChecked = this->autostartCheckbox->IsChecked();
autostartCheckbox->Bind(wxEVT_CHECKBOX, [](wxCommandEvent& event) {
bool isChecked = event.IsChecked();
auto settings = utils::getSettings();
settings.autoStart = isChecked;
backend::toggleAutostart(isChecked);
utils::saveSettings(settings);
});
auto odesliCheckbox =
new wxCheckBox(panel, wxID_ANY, _("Odesli integration"), wxDefaultPosition, wxDefaultSize, 0);
odesliCheckbox->SetValue(settings.odesli);
odesliCheckbox->Bind(wxEVT_CHECKBOX, [](wxCommandEvent& event) {
bool isChecked = event.IsChecked();
auto settings = utils::getSettings();
settings.odesli = isChecked;
utils::saveSettings(settings);
});
settingsContainer->Add(autostartCheckbox, 0, wxALL, 5);
mainContainer->Add(settingsContainer, 0, wxEXPAND, 5);
this->SetSizerAndFit(mainContainer);
settingsContainer->Add(odesliCheckbox, 0, wxALL, 5);
startupContainer->Add(settingsContainer);
mainContainer->Add(startupContainer, 0, wxEXPAND, 5);
// settings end
this->SetSizerAndFit(frameSizer);
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 {
@ -268,8 +677,8 @@ public:
this->SetAppearance(wxAppBase::Appearance::Dark);
wxInitAllImageHandlers();
wxIcon icon = utils::loadIconFromMemory(icon_png, icon_png_size);
PlayerLinkFrame* frame = new PlayerLinkFrame(nullptr, icon, wxID_ANY, _("PlayerLink"));
wxIcon tray_icon = utils::loadIconFromMemory(menubar_icon_png, menubar_icon_png_size);
PlayerLinkFrame* frame = new PlayerLinkFrame(nullptr, wxID_ANY, _("PlayerLink"));
trayIcon = new PlayerLinkIcon(frame);
frame->Bind(wxEVT_CLOSE_WINDOW, [=](wxCloseEvent& event) {
if (event.CanVeto()) {
@ -279,7 +688,7 @@ public:
std::exit(0);
});
trayIcon->SetIcon(icon, _("PlayerLink"));
trayIcon->SetIcon(tray_icon, _("PlayerLink"));
return true;
}

View File

@ -3,7 +3,7 @@
#include <curl/include/curl/curl.h>
#include <wx/mstream.h>
#include <wx/wx.h>
#include <wx/clipbrd.h>
#include <filesystem>
#include <fstream>
#include <nlohmann-json/single_include/nlohmann/json.hpp>
@ -11,37 +11,105 @@
#include <string>
#include <vector>
#include "backend.hpp"
#define DEFAULT_CLIENT_ID "1301849203378622545"
#define DEFAULT_APP_NAME "Music"
#define CONFIG_FILENAME "settings.json"
#define CONFIG_FILENAME backend::getConfigDirectory() / "settings.json"
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 {
bool enabled;
std::string username;
std::string password;
std::string api_key;
std::string api_secret;
};
struct Settings {
bool odesli;
bool autoStart;
bool anyOtherEnabled;
LastFMSettings lastfm;
std::vector<App> apps;
};
inline wxIcon loadIconFromMemory(const unsigned char* data, size_t size) {
struct SongInfo {
std::string artworkURL;
int64_t trackId;
};
inline void copyToClipboard(const wxString& text) {
if (wxTheClipboard->Open()) {
wxTheClipboard->Clear();
wxTheClipboard->SetData(new wxTextDataObject(text));
wxTheClipboard->Close();
}
}
inline wxBitmap loadImageFromMemory(const unsigned char* data, size_t size, int width = 0, int height = 0) {
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);
wxIcon icon;
icon.CopyFromBitmap(bmp);
return icon;
return bmp;
}
return wxNullIcon;
return wxNullBitmap;
}
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;
@ -78,7 +146,21 @@ namespace utils {
return size * nmemb;
}
inline std::string getRequest(std::string url) {
inline std::string getURLEncodedPostBody(const std::map<std::string, std::string>& parameters) {
if (parameters.empty())
return "";
std::string encodedPostBody = "";
for (const auto& parameter : parameters) {
encodedPostBody += parameter.first;
encodedPostBody += "=";
encodedPostBody += urlEncode(parameter.second);
encodedPostBody += "&";
}
return encodedPostBody.erase(encodedPostBody.length() - 1);
}
inline std::string httpRequest(std::string url, std::string requestType = "GET", std::string postData = "") {
CURL* curl;
CURLcode res;
std::string buf;
@ -86,6 +168,10 @@ namespace utils {
curl = curl_easy_init();
if (curl) {
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, requestType.c_str());
if (requestType != "GET" && requestType != "DELETE" && postData.length() > 0)
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postData.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curlWriteCallback);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf);
@ -96,16 +182,27 @@ namespace utils {
return buf;
}
inline std::string getArtworkURL(std::string query) {
inline SongInfo getSongInfo(std::string query) {
SongInfo ret{};
std::string response =
getRequest("https://itunes.apple.com/search?media=music&entity=song&term=" + urlEncode(query));
nlohmann::json j = nlohmann::json::parse(response);
auto results = j["results"];
if (results.size() > 0) {
return results[0]["artworkUrl100"].get<std::string>();
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;
}
return "";
}
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;
@ -159,6 +256,13 @@ namespace utils {
nlohmann::json j;
j["autostart"] = settings.autoStart;
j["any_other"] = settings.anyOtherEnabled;
j["odesli"] = settings.odesli;
j["lastfm"]["enabled"] = settings.lastfm.enabled;
j["lastfm"]["api_key"] = settings.lastfm.api_key;
j["lastfm"]["api_secret"] = settings.lastfm.api_secret;
j["lastfm"]["username"] = settings.lastfm.username;
j["lastfm"]["password"] = settings.lastfm.password;
for (const auto& app : settings.apps) {
nlohmann::json appJson;
@ -166,7 +270,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);
@ -177,9 +281,15 @@ namespace utils {
o.close();
}
inline Settings getSettings() {
std::filesystem::create_directories(backend::getConfigDirectory());
Settings ret;
if (!std::filesystem::exists(CONFIG_FILENAME))
if (!std::filesystem::exists(CONFIG_FILENAME)) {
ret.anyOtherEnabled = true;
ret.autoStart = false;
ret.odesli = false;
saveSettings(ret);
return ret;
}
try {
std::ifstream i(CONFIG_FILENAME);
@ -188,6 +298,16 @@ namespace utils {
ret.autoStart = j.value("autostart", false);
ret.anyOtherEnabled = j.value("any_other", false);
ret.odesli = j.value("odesli", false);
if (j.contains("lastfm")) {
auto lastfm = j["lastfm"];
ret.lastfm.enabled = lastfm.value("enabled", false);
ret.lastfm.api_key = lastfm.value("api_key", "");
ret.lastfm.api_secret = lastfm.value("api_secret", "");
ret.lastfm.username = lastfm.value("username", "");
ret.lastfm.password = lastfm.value("password", "");
}
for (const auto& app : j["apps"]) {
App a;
@ -195,13 +315,14 @@ 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["process_names"]) a.processNames.push_back(process.get<std::string>());
for (const auto& process : app.value("process_names", nlohmann::json())) a.processNames.push_back(process.get<std::string>());
ret.apps.push_back(a);
}
} catch (const nlohmann::json::parse_error&) {
} // TODO: handle error
}
return ret;
}
@ -209,7 +330,7 @@ namespace utils {
auto settings = getSettings();
for (auto app : settings.apps) {
for (auto procName : app.processNames) {
if (procName == processName)
if (caseInsensitiveMatch(procName, processName))
return app;
}
}
@ -217,6 +338,7 @@ 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

@ -11,13 +11,16 @@ SET(BUILD_STATIC_LIBS ON)
SET(BUILD_SHARED_LIBS OFF)
SET(BUILD_CURL_EXE OFF)
SET(MBEDTLS_INCLUDE_DIRS ../mbedtls/include)
file(REMOVE curl/CMake/FindMbedTLS.cmake) #replace curls FindMbedTLS that expects mbedtls to be prebuilt with a dummy
file(RENAME curl/CMake/FindMbedTLS.cmake curl/CMake/FindMbedTLS.cmake.bak) #replace curls FindMbedTLS that expects mbedtls to be prebuilt with a dummy
set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/dummy ${CMAKE_MODULE_PATH})
add_subdirectory("curl")
file(RENAME curl/CMake/FindMbedTLS.cmake.bak curl/CMake/FindMbedTLS.cmake)
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 e86d7a81de7a33323e2038182ab53a26c69f7880
Subproject commit 6bc42071f109083178ae824898ca4b3cb40a8307

447
vendor/md5.hpp vendored Normal file
View File

@ -0,0 +1,447 @@
/*
md5.hpp is a reformulation of the md5.h and md5.c code from
http://www.opensource.apple.com/source/cups/cups-59/cups/md5.c to allow it to
function as a component of a header only library. This conversion was done by
Peter Thorson (webmaster@zaphoyd.com) in 2012 for the WebSocket++ project. The
changes are released under the same license as the original (listed below)
*/
/*
Copyright (C) 1999, 2002 Aladdin Enterprises. All rights reserved.
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
L. Peter Deutsch
ghost@aladdin.com
*/
/* $Id: md5.h,v 1.4 2002/04/13 19:20:28 lpd Exp $ */
/*
Independent implementation of MD5 (RFC 1321).
This code implements the MD5 Algorithm defined in RFC 1321, whose
text is available at
http://www.ietf.org/rfc/rfc1321.txt
The code is derived from the text of the RFC, including the test suite
(section A.5) but excluding the rest of Appendix A. It does not include
any code or documentation that is identified in the RFC as being
copyrighted.
The original and principal author of md5.h is L. Peter Deutsch
<ghost@aladdin.com>. Other authors are noted in the change history
that follows (in reverse chronological order):
2002-04-13 lpd Removed support for non-ANSI compilers; removed
references to Ghostscript; clarified derivation from RFC 1321;
now handles byte order either statically or dynamically.
1999-11-04 lpd Edited comments slightly for automatic TOC extraction.
1999-10-18 lpd Fixed typo in header comment (ansi2knr rather than md5);
added conditionalization for C++ compilation from Martin
Purschke <purschke@bnl.gov>.
1999-05-03 lpd Original version.
*/
#ifndef WEBSOCKETPP_COMMON_MD5_HPP
#define WEBSOCKETPP_COMMON_MD5_HPP
/*
* This package supports both compile-time and run-time determination of CPU
* byte order. If ARCH_IS_BIG_ENDIAN is defined as 0, the code will be
* compiled to run only on little-endian CPUs; if ARCH_IS_BIG_ENDIAN is
* defined as non-zero, the code will be compiled to run only on big-endian
* CPUs; if ARCH_IS_BIG_ENDIAN is not defined, the code will be compiled to
* run on either big- or little-endian CPUs, but will run slightly less
* efficiently on either one than if ARCH_IS_BIG_ENDIAN is defined.
*/
#include <stddef.h>
#include <string>
#include <cstring>
/// Provides MD5 hashing functionality
namespace md5 {
typedef unsigned char md5_byte_t; /* 8-bit byte */
typedef unsigned int md5_word_t; /* 32-bit word */
/* Define the state of the MD5 Algorithm. */
typedef struct md5_state_s {
md5_word_t count[2]; /* message length in bits, lsw first */
md5_word_t abcd[4]; /* digest buffer */
md5_byte_t buf[64]; /* accumulate block */
} md5_state_t;
/* Initialize the algorithm. */
inline void md5_init(md5_state_t *pms);
/* Append a string to the message. */
inline void md5_append(md5_state_t *pms, md5_byte_t const * data, size_t nbytes);
/* Finish the message and return the digest. */
inline void md5_finish(md5_state_t *pms, md5_byte_t digest[16]);
#undef ZSW_MD5_BYTE_ORDER /* 1 = big-endian, -1 = little-endian, 0 = unknown */
#ifdef ARCH_IS_BIG_ENDIAN
# define ZSW_MD5_BYTE_ORDER (ARCH_IS_BIG_ENDIAN ? 1 : -1)
#else
# define ZSW_MD5_BYTE_ORDER 0
#endif
#define ZSW_MD5_T_MASK ((md5_word_t)~0)
#define ZSW_MD5_T1 /* 0xd76aa478 */ (ZSW_MD5_T_MASK ^ 0x28955b87)
#define ZSW_MD5_T2 /* 0xe8c7b756 */ (ZSW_MD5_T_MASK ^ 0x173848a9)
#define ZSW_MD5_T3 0x242070db
#define ZSW_MD5_T4 /* 0xc1bdceee */ (ZSW_MD5_T_MASK ^ 0x3e423111)
#define ZSW_MD5_T5 /* 0xf57c0faf */ (ZSW_MD5_T_MASK ^ 0x0a83f050)
#define ZSW_MD5_T6 0x4787c62a
#define ZSW_MD5_T7 /* 0xa8304613 */ (ZSW_MD5_T_MASK ^ 0x57cfb9ec)
#define ZSW_MD5_T8 /* 0xfd469501 */ (ZSW_MD5_T_MASK ^ 0x02b96afe)
#define ZSW_MD5_T9 0x698098d8
#define ZSW_MD5_T10 /* 0x8b44f7af */ (ZSW_MD5_T_MASK ^ 0x74bb0850)
#define ZSW_MD5_T11 /* 0xffff5bb1 */ (ZSW_MD5_T_MASK ^ 0x0000a44e)
#define ZSW_MD5_T12 /* 0x895cd7be */ (ZSW_MD5_T_MASK ^ 0x76a32841)
#define ZSW_MD5_T13 0x6b901122
#define ZSW_MD5_T14 /* 0xfd987193 */ (ZSW_MD5_T_MASK ^ 0x02678e6c)
#define ZSW_MD5_T15 /* 0xa679438e */ (ZSW_MD5_T_MASK ^ 0x5986bc71)
#define ZSW_MD5_T16 0x49b40821
#define ZSW_MD5_T17 /* 0xf61e2562 */ (ZSW_MD5_T_MASK ^ 0x09e1da9d)
#define ZSW_MD5_T18 /* 0xc040b340 */ (ZSW_MD5_T_MASK ^ 0x3fbf4cbf)
#define ZSW_MD5_T19 0x265e5a51
#define ZSW_MD5_T20 /* 0xe9b6c7aa */ (ZSW_MD5_T_MASK ^ 0x16493855)
#define ZSW_MD5_T21 /* 0xd62f105d */ (ZSW_MD5_T_MASK ^ 0x29d0efa2)
#define ZSW_MD5_T22 0x02441453
#define ZSW_MD5_T23 /* 0xd8a1e681 */ (ZSW_MD5_T_MASK ^ 0x275e197e)
#define ZSW_MD5_T24 /* 0xe7d3fbc8 */ (ZSW_MD5_T_MASK ^ 0x182c0437)
#define ZSW_MD5_T25 0x21e1cde6
#define ZSW_MD5_T26 /* 0xc33707d6 */ (ZSW_MD5_T_MASK ^ 0x3cc8f829)
#define ZSW_MD5_T27 /* 0xf4d50d87 */ (ZSW_MD5_T_MASK ^ 0x0b2af278)
#define ZSW_MD5_T28 0x455a14ed
#define ZSW_MD5_T29 /* 0xa9e3e905 */ (ZSW_MD5_T_MASK ^ 0x561c16fa)
#define ZSW_MD5_T30 /* 0xfcefa3f8 */ (ZSW_MD5_T_MASK ^ 0x03105c07)
#define ZSW_MD5_T31 0x676f02d9
#define ZSW_MD5_T32 /* 0x8d2a4c8a */ (ZSW_MD5_T_MASK ^ 0x72d5b375)
#define ZSW_MD5_T33 /* 0xfffa3942 */ (ZSW_MD5_T_MASK ^ 0x0005c6bd)
#define ZSW_MD5_T34 /* 0x8771f681 */ (ZSW_MD5_T_MASK ^ 0x788e097e)
#define ZSW_MD5_T35 0x6d9d6122
#define ZSW_MD5_T36 /* 0xfde5380c */ (ZSW_MD5_T_MASK ^ 0x021ac7f3)
#define ZSW_MD5_T37 /* 0xa4beea44 */ (ZSW_MD5_T_MASK ^ 0x5b4115bb)
#define ZSW_MD5_T38 0x4bdecfa9
#define ZSW_MD5_T39 /* 0xf6bb4b60 */ (ZSW_MD5_T_MASK ^ 0x0944b49f)
#define ZSW_MD5_T40 /* 0xbebfbc70 */ (ZSW_MD5_T_MASK ^ 0x4140438f)
#define ZSW_MD5_T41 0x289b7ec6
#define ZSW_MD5_T42 /* 0xeaa127fa */ (ZSW_MD5_T_MASK ^ 0x155ed805)
#define ZSW_MD5_T43 /* 0xd4ef3085 */ (ZSW_MD5_T_MASK ^ 0x2b10cf7a)
#define ZSW_MD5_T44 0x04881d05
#define ZSW_MD5_T45 /* 0xd9d4d039 */ (ZSW_MD5_T_MASK ^ 0x262b2fc6)
#define ZSW_MD5_T46 /* 0xe6db99e5 */ (ZSW_MD5_T_MASK ^ 0x1924661a)
#define ZSW_MD5_T47 0x1fa27cf8
#define ZSW_MD5_T48 /* 0xc4ac5665 */ (ZSW_MD5_T_MASK ^ 0x3b53a99a)
#define ZSW_MD5_T49 /* 0xf4292244 */ (ZSW_MD5_T_MASK ^ 0x0bd6ddbb)
#define ZSW_MD5_T50 0x432aff97
#define ZSW_MD5_T51 /* 0xab9423a7 */ (ZSW_MD5_T_MASK ^ 0x546bdc58)
#define ZSW_MD5_T52 /* 0xfc93a039 */ (ZSW_MD5_T_MASK ^ 0x036c5fc6)
#define ZSW_MD5_T53 0x655b59c3
#define ZSW_MD5_T54 /* 0x8f0ccc92 */ (ZSW_MD5_T_MASK ^ 0x70f3336d)
#define ZSW_MD5_T55 /* 0xffeff47d */ (ZSW_MD5_T_MASK ^ 0x00100b82)
#define ZSW_MD5_T56 /* 0x85845dd1 */ (ZSW_MD5_T_MASK ^ 0x7a7ba22e)
#define ZSW_MD5_T57 0x6fa87e4f
#define ZSW_MD5_T58 /* 0xfe2ce6e0 */ (ZSW_MD5_T_MASK ^ 0x01d3191f)
#define ZSW_MD5_T59 /* 0xa3014314 */ (ZSW_MD5_T_MASK ^ 0x5cfebceb)
#define ZSW_MD5_T60 0x4e0811a1
#define ZSW_MD5_T61 /* 0xf7537e82 */ (ZSW_MD5_T_MASK ^ 0x08ac817d)
#define ZSW_MD5_T62 /* 0xbd3af235 */ (ZSW_MD5_T_MASK ^ 0x42c50dca)
#define ZSW_MD5_T63 0x2ad7d2bb
#define ZSW_MD5_T64 /* 0xeb86d391 */ (ZSW_MD5_T_MASK ^ 0x14792c6e)
static void md5_process(md5_state_t *pms, md5_byte_t const * data /*[64]*/) {
md5_word_t
a = pms->abcd[0], b = pms->abcd[1],
c = pms->abcd[2], d = pms->abcd[3];
md5_word_t t;
#if ZSW_MD5_BYTE_ORDER > 0
/* Define storage only for big-endian CPUs. */
md5_word_t X[16];
#else
/* Define storage for little-endian or both types of CPUs. */
md5_word_t xbuf[16];
md5_word_t const * X;
#endif
{
#if ZSW_MD5_BYTE_ORDER == 0
/*
* Determine dynamically whether this is a big-endian or
* little-endian machine, since we can use a more efficient
* algorithm on the latter.
*/
static int const w = 1;
if (*((md5_byte_t const *)&w)) /* dynamic little-endian */
#endif
#if ZSW_MD5_BYTE_ORDER <= 0 /* little-endian */
{
/*
* On little-endian machines, we can process properly aligned
* data without copying it.
*/
if (!((data - (md5_byte_t const *)0) & 3)) {
/* data are properly aligned */
X = (md5_word_t const *)data;
} else {
/* not aligned */
std::memcpy(xbuf, data, 64);
X = xbuf;
}
}
#endif
#if ZSW_MD5_BYTE_ORDER == 0
else /* dynamic big-endian */
#endif
#if ZSW_MD5_BYTE_ORDER >= 0 /* big-endian */
{
/*
* On big-endian machines, we must arrange the bytes in the
* right order.
*/
const md5_byte_t *xp = data;
int i;
# if ZSW_MD5_BYTE_ORDER == 0
X = xbuf; /* (dynamic only) */
# else
# define xbuf X /* (static only) */
# endif
for (i = 0; i < 16; ++i, xp += 4)
xbuf[i] = xp[0] + (xp[1] << 8) + (xp[2] << 16) + (xp[3] << 24);
}
#endif
}
#define ZSW_MD5_ROTATE_LEFT(x, n) (((x) << (n)) | ((x) >> (32 - (n))))
/* Round 1. */
/* Let [abcd k s i] denote the operation
a = b + ((a + F(b,c,d) + X[k] + T[i]) <<< s). */
#define ZSW_MD5_F(x, y, z) (((x) & (y)) | (~(x) & (z)))
#define SET(a, b, c, d, k, s, Ti)\
t = a + ZSW_MD5_F(b,c,d) + X[k] + Ti;\
a = ZSW_MD5_ROTATE_LEFT(t, s) + b
/* Do the following 16 operations. */
SET(a, b, c, d, 0, 7, ZSW_MD5_T1);
SET(d, a, b, c, 1, 12, ZSW_MD5_T2);
SET(c, d, a, b, 2, 17, ZSW_MD5_T3);
SET(b, c, d, a, 3, 22, ZSW_MD5_T4);
SET(a, b, c, d, 4, 7, ZSW_MD5_T5);
SET(d, a, b, c, 5, 12, ZSW_MD5_T6);
SET(c, d, a, b, 6, 17, ZSW_MD5_T7);
SET(b, c, d, a, 7, 22, ZSW_MD5_T8);
SET(a, b, c, d, 8, 7, ZSW_MD5_T9);
SET(d, a, b, c, 9, 12, ZSW_MD5_T10);
SET(c, d, a, b, 10, 17, ZSW_MD5_T11);
SET(b, c, d, a, 11, 22, ZSW_MD5_T12);
SET(a, b, c, d, 12, 7, ZSW_MD5_T13);
SET(d, a, b, c, 13, 12, ZSW_MD5_T14);
SET(c, d, a, b, 14, 17, ZSW_MD5_T15);
SET(b, c, d, a, 15, 22, ZSW_MD5_T16);
#undef SET
/* Round 2. */
/* Let [abcd k s i] denote the operation
a = b + ((a + G(b,c,d) + X[k] + T[i]) <<< s). */
#define ZSW_MD5_G(x, y, z) (((x) & (z)) | ((y) & ~(z)))
#define SET(a, b, c, d, k, s, Ti)\
t = a + ZSW_MD5_G(b,c,d) + X[k] + Ti;\
a = ZSW_MD5_ROTATE_LEFT(t, s) + b
/* Do the following 16 operations. */
SET(a, b, c, d, 1, 5, ZSW_MD5_T17);
SET(d, a, b, c, 6, 9, ZSW_MD5_T18);
SET(c, d, a, b, 11, 14, ZSW_MD5_T19);
SET(b, c, d, a, 0, 20, ZSW_MD5_T20);
SET(a, b, c, d, 5, 5, ZSW_MD5_T21);
SET(d, a, b, c, 10, 9, ZSW_MD5_T22);
SET(c, d, a, b, 15, 14, ZSW_MD5_T23);
SET(b, c, d, a, 4, 20, ZSW_MD5_T24);
SET(a, b, c, d, 9, 5, ZSW_MD5_T25);
SET(d, a, b, c, 14, 9, ZSW_MD5_T26);
SET(c, d, a, b, 3, 14, ZSW_MD5_T27);
SET(b, c, d, a, 8, 20, ZSW_MD5_T28);
SET(a, b, c, d, 13, 5, ZSW_MD5_T29);
SET(d, a, b, c, 2, 9, ZSW_MD5_T30);
SET(c, d, a, b, 7, 14, ZSW_MD5_T31);
SET(b, c, d, a, 12, 20, ZSW_MD5_T32);
#undef SET
/* Round 3. */
/* Let [abcd k s t] denote the operation
a = b + ((a + H(b,c,d) + X[k] + T[i]) <<< s). */
#define ZSW_MD5_H(x, y, z) ((x) ^ (y) ^ (z))
#define SET(a, b, c, d, k, s, Ti)\
t = a + ZSW_MD5_H(b,c,d) + X[k] + Ti;\
a = ZSW_MD5_ROTATE_LEFT(t, s) + b
/* Do the following 16 operations. */
SET(a, b, c, d, 5, 4, ZSW_MD5_T33);
SET(d, a, b, c, 8, 11, ZSW_MD5_T34);
SET(c, d, a, b, 11, 16, ZSW_MD5_T35);
SET(b, c, d, a, 14, 23, ZSW_MD5_T36);
SET(a, b, c, d, 1, 4, ZSW_MD5_T37);
SET(d, a, b, c, 4, 11, ZSW_MD5_T38);
SET(c, d, a, b, 7, 16, ZSW_MD5_T39);
SET(b, c, d, a, 10, 23, ZSW_MD5_T40);
SET(a, b, c, d, 13, 4, ZSW_MD5_T41);
SET(d, a, b, c, 0, 11, ZSW_MD5_T42);
SET(c, d, a, b, 3, 16, ZSW_MD5_T43);
SET(b, c, d, a, 6, 23, ZSW_MD5_T44);
SET(a, b, c, d, 9, 4, ZSW_MD5_T45);
SET(d, a, b, c, 12, 11, ZSW_MD5_T46);
SET(c, d, a, b, 15, 16, ZSW_MD5_T47);
SET(b, c, d, a, 2, 23, ZSW_MD5_T48);
#undef SET
/* Round 4. */
/* Let [abcd k s t] denote the operation
a = b + ((a + I(b,c,d) + X[k] + T[i]) <<< s). */
#define ZSW_MD5_I(x, y, z) ((y) ^ ((x) | ~(z)))
#define SET(a, b, c, d, k, s, Ti)\
t = a + ZSW_MD5_I(b,c,d) + X[k] + Ti;\
a = ZSW_MD5_ROTATE_LEFT(t, s) + b
/* Do the following 16 operations. */
SET(a, b, c, d, 0, 6, ZSW_MD5_T49);
SET(d, a, b, c, 7, 10, ZSW_MD5_T50);
SET(c, d, a, b, 14, 15, ZSW_MD5_T51);
SET(b, c, d, a, 5, 21, ZSW_MD5_T52);
SET(a, b, c, d, 12, 6, ZSW_MD5_T53);
SET(d, a, b, c, 3, 10, ZSW_MD5_T54);
SET(c, d, a, b, 10, 15, ZSW_MD5_T55);
SET(b, c, d, a, 1, 21, ZSW_MD5_T56);
SET(a, b, c, d, 8, 6, ZSW_MD5_T57);
SET(d, a, b, c, 15, 10, ZSW_MD5_T58);
SET(c, d, a, b, 6, 15, ZSW_MD5_T59);
SET(b, c, d, a, 13, 21, ZSW_MD5_T60);
SET(a, b, c, d, 4, 6, ZSW_MD5_T61);
SET(d, a, b, c, 11, 10, ZSW_MD5_T62);
SET(c, d, a, b, 2, 15, ZSW_MD5_T63);
SET(b, c, d, a, 9, 21, ZSW_MD5_T64);
#undef SET
/* Then perform the following additions. (That is increment each
of the four registers by the value it had before this block
was started.) */
pms->abcd[0] += a;
pms->abcd[1] += b;
pms->abcd[2] += c;
pms->abcd[3] += d;
}
void md5_init(md5_state_t *pms) {
pms->count[0] = pms->count[1] = 0;
pms->abcd[0] = 0x67452301;
pms->abcd[1] = /*0xefcdab89*/ ZSW_MD5_T_MASK ^ 0x10325476;
pms->abcd[2] = /*0x98badcfe*/ ZSW_MD5_T_MASK ^ 0x67452301;
pms->abcd[3] = 0x10325476;
}
void md5_append(md5_state_t *pms, md5_byte_t const * data, size_t nbytes) {
md5_byte_t const * p = data;
size_t left = nbytes;
int offset = (pms->count[0] >> 3) & 63;
md5_word_t nbits = (md5_word_t)(nbytes << 3);
if (nbytes <= 0)
return;
/* Update the message length. */
pms->count[1] += nbytes >> 29;
pms->count[0] += nbits;
if (pms->count[0] < nbits)
pms->count[1]++;
/* Process an initial partial block. */
if (offset) {
int copy = (offset + nbytes > 64 ? 64 - offset : static_cast<int>(nbytes));
std::memcpy(pms->buf + offset, p, copy);
if (offset + copy < 64)
return;
p += copy;
left -= copy;
md5_process(pms, pms->buf);
}
/* Process full blocks. */
for (; left >= 64; p += 64, left -= 64)
md5_process(pms, p);
/* Process a final partial block. */
if (left)
std::memcpy(pms->buf, p, left);
}
void md5_finish(md5_state_t *pms, md5_byte_t digest[16]) {
static md5_byte_t const pad[64] = {
0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
};
md5_byte_t data[8];
int i;
/* Save the length before padding. */
for (i = 0; i < 8; ++i)
data[i] = (md5_byte_t)(pms->count[i >> 2] >> ((i & 3) << 3));
/* Pad to 56 bytes mod 64. */
md5_append(pms, pad, ((55 - (pms->count[0] >> 3)) & 63) + 1);
/* Append the length. */
md5_append(pms, data, 8);
for (i = 0; i < 16; ++i)
digest[i] = (md5_byte_t)(pms->abcd[i >> 2] >> ((i & 3) << 3));
}
// some convenience c++ functions
inline std::string md5_hash_string(std::string const & s) {
char digest[16];
md5_state_t state;
md5_init(&state);
md5_append(&state, (md5_byte_t const *)s.c_str(), s.size());
md5_finish(&state, (md5_byte_t *)digest);
std::string ret;
ret.resize(16);
std::copy(digest,digest+16,ret.begin());
return ret;
}
const char hexval[16] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
inline std::string md5_hash_hex(std::string const & input) {
std::string hash = md5_hash_string(input);
std::string hex;
for (size_t i = 0; i < hash.size(); i++) {
hex.push_back(hexval[((hash[i] >> 4) & 0xF)]);
hex.push_back(hexval[(hash[i]) & 0x0F]);
}
return hex;
}
} // md5
#endif // WEBSOCKETPP_COMMON_MD5_HPP

2
vendor/wxWidgets vendored

@ -1 +1 @@
Subproject commit 12b09a5e5ea76a1a0c27b769e821b37d803a4cb7
Subproject commit d7a696de4c301948f02dc5d7979f867f07f9d684

Binary file not shown.

Before

Width:  |  Height:  |  Size: 260 KiB

After

Width:  |  Height:  |  Size: 156 KiB