Compare commits
44 Commits
Author | SHA1 | Date |
---|---|---|
|
7d4bc6dd89 | |
|
fa29020cea | |
|
36be257462 | |
|
958def7527 | |
|
28a198983c | |
|
374a95e68d | |
|
49e1bb0262 | |
|
cfecd99858 | |
|
c805dfe829 | |
|
c3bf12a997 | |
|
5428f57891 | |
|
66a25e62f2 | |
|
6807fe00fa | |
|
4e57f272a1 | |
|
7fc1ac5d5d | |
|
a6f5d3277b | |
|
563080643f | |
|
90607d2f5d | |
|
cd5656de63 | |
|
c53f02b35c | |
|
dbbec3b027 | |
|
bccf4c6100 | |
|
349058c32d | |
|
9821b6a294 | |
|
8fb4af3a49 | |
|
191fd2c375 | |
|
c35416c928 | |
|
be3a50d42a | |
|
aa734cf45a | |
|
cbc96614d0 | |
|
e8ac296f35 | |
|
a6c77e256f | |
|
3e16670a70 | |
|
017b21dbeb | |
|
d13231e84e | |
|
4a68eba106 | |
|
6f29111c40 | |
|
35d0ce747b | |
|
bb093cdefe | |
|
26af0323f2 | |
|
7b5666a906 | |
|
322ceeaddb | |
|
0c274c55c5 | |
|
31afe3372d |
|
@ -8,3 +8,4 @@ IndentCaseLabels: false
|
|||
AccessModifierOffset: -4
|
||||
ColumnLimit: 120
|
||||
NamespaceIndentation: All
|
||||
SortIncludes: false
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||
|
|
19
README.md
|
@ -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.
|
BIN
img/linux.png
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 263 KiB |
BIN
img/macos.png
Before Width: | Height: | Size: 248 KiB After Width: | Height: | Size: 692 KiB |
BIN
img/windows.png
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 39 KiB |
|
@ -0,0 +1,6 @@
|
|||
[Desktop Entry]
|
||||
Name=PlayerLink
|
||||
Exec=PlayerLink
|
||||
Icon=PlayerLink
|
||||
Type=Application
|
||||
Categories=Utility;GTK;
|
After Width: | Height: | Size: 41 KiB |
|
@ -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() });
|
||||
}
|
BIN
osx/icon.icns
BIN
rsrc/icon.png
Before Width: | Height: | Size: 285 KiB After Width: | Height: | Size: 817 KiB |
After Width: | Height: | Size: 34 KiB |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 "
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
549
src/main.cpp
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
162
src/utils.hpp
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit e86d7a81de7a33323e2038182ab53a26c69f7880
|
||||
Subproject commit 6bc42071f109083178ae824898ca4b3cb40a8307
|
|
@ -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
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit 12b09a5e5ea76a1a0c27b769e821b37d803a4cb7
|
||||
Subproject commit d7a696de4c301948f02dc5d7979f867f07f9d684
|
BIN
win/icon.ico
Before Width: | Height: | Size: 260 KiB After Width: | Height: | Size: 156 KiB |