├── .clang-format ├── .editorconfig ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── build.yml ├── .gitignore ├── CMakeLists.txt ├── ChangeLog.md ├── LICENSE ├── README.md ├── ci ├── build.cmd ├── build.sh ├── docker │ ├── .dockerignore │ ├── build_image.sh │ ├── install.sh │ ├── install_cmake.sh │ ├── install_node.sh │ ├── site-config.jam │ ├── ubuntu-14.04.dockerfile │ └── ubuntu-22.04.dockerfile ├── install.cmd ├── install.sh ├── run_in_docker.sh ├── test.cmd └── test.sh ├── cmake ├── CPackProjectConfig.cmake ├── EnvInfo.cmake ├── Packages.cmake ├── ProjectInfo.cmake └── Utils.cmake ├── cpp ├── CMakeLists.txt ├── extlibs │ ├── CMakeLists.txt │ ├── apply_patches.cmd │ ├── apply_patches.sh │ ├── boost │ │ ├── CMakeLists.txt │ │ ├── files.windows │ │ │ └── install.cmd │ │ └── local │ │ │ └── FindBoost.cmake │ ├── catch │ │ ├── CMakeLists.txt │ │ ├── local │ │ │ └── FindCatch.cmake │ │ └── system │ │ │ └── FindCatch.cmake │ ├── deadbeef │ │ ├── CMakeLists.txt │ │ ├── local │ │ │ └── FindDeadbeef.cmake │ │ └── system │ │ │ └── FindDeadbeef.cmake │ ├── foosdk │ │ ├── CMakeLists.txt │ │ ├── local │ │ │ └── FindFoosdk.cmake │ │ └── patches │ │ │ └── 001-disable-debug.patch │ ├── licenses.txt │ ├── nljson │ │ ├── CMakeLists.txt │ │ ├── local │ │ │ └── FindNljson.cmake │ │ └── system │ │ │ └── FindNljson.cmake │ ├── stringencoders │ │ ├── CMakeLists.txt │ │ ├── files │ │ │ ├── CMakeLists.txt │ │ │ └── src │ │ │ │ ├── config.h │ │ │ │ ├── modp_ascii_data.h │ │ │ │ ├── modp_b16_data.h │ │ │ │ ├── modp_b2_data.h │ │ │ │ ├── modp_b64_data.h │ │ │ │ ├── modp_b64r_data.h │ │ │ │ ├── modp_b64w_data.h │ │ │ │ ├── modp_b85_data.h │ │ │ │ ├── modp_bjavascript_data.h │ │ │ │ ├── modp_burl_data.h │ │ │ │ └── modp_json_data.h │ │ ├── local │ │ │ └── FindStringEncoders.cmake │ │ ├── patches │ │ │ └── 001-no-stdint-hacks.patch │ │ └── system │ │ │ └── FindStringEncoders.cmake │ └── zlib │ │ ├── CMakeLists.txt │ │ ├── files │ │ └── CMakeLists.txt │ │ ├── local │ │ └── FindZLIB.cmake │ │ └── make-import-lib.cmd └── server │ ├── CMakeLists.txt │ ├── artwork_controller.cpp │ ├── artwork_controller.hpp │ ├── asio.hpp │ ├── asio_adapters.cpp │ ├── asio_adapters.hpp │ ├── base64.cpp │ ├── base64.hpp │ ├── basic_auth_filter.cpp │ ├── basic_auth_filter.hpp │ ├── beast.hpp │ ├── beast_connection.cpp │ ├── beast_connection.hpp │ ├── beast_listener.cpp │ ├── beast_listener.hpp │ ├── beast_request.cpp │ ├── beast_request.hpp │ ├── beast_server.cpp │ ├── beast_server.hpp │ ├── browser_controller.cpp │ ├── browser_controller.hpp │ ├── cache_support_filter.cpp │ ├── cache_support_filter.hpp │ ├── charset.hpp │ ├── charset_posix.cpp │ ├── charset_windows.cpp │ ├── chrono.hpp │ ├── client_config_controller.cpp │ ├── client_config_controller.hpp │ ├── compression_filter.cpp │ ├── compression_filter.hpp │ ├── content_type_map.cpp │ ├── content_type_map.hpp │ ├── controller.hpp │ ├── core_types.hpp │ ├── core_types_json.cpp │ ├── core_types_json.hpp │ ├── core_types_parsers.cpp │ ├── core_types_parsers.hpp │ ├── deadbeef │ ├── CMakeLists.txt │ ├── add_items_scope.cpp │ ├── add_items_scope.hpp │ ├── artwork_fetcher.hpp │ ├── artwork_fetcher_v1.cpp │ ├── artwork_fetcher_v2.cpp │ ├── common.cpp │ ├── common.hpp │ ├── dummy_gui.c │ ├── linker_script.in │ ├── nullout2.c │ ├── player.hpp │ ├── player_control.cpp │ ├── player_misc.cpp │ ├── player_options.cpp │ ├── player_options.hpp │ ├── player_playlists.cpp │ ├── playlist_mapping.cpp │ ├── playlist_mapping.hpp │ ├── plugin.cpp │ ├── plugin.hpp │ ├── utils.cpp │ └── utils.hpp │ ├── defines.hpp │ ├── env_info.hpp.in │ ├── file_system.cpp │ ├── file_system.hpp │ ├── file_system_posix.cpp │ ├── file_system_windows.cpp │ ├── fnv_hash.hpp │ ├── foobar2000 │ ├── CMakeLists.txt │ ├── common.hpp │ ├── main_prefs_page.cpp │ ├── main_prefs_page.hpp │ ├── permissions_prefs_page.cpp │ ├── permissions_prefs_page.hpp │ ├── player.hpp │ ├── player_control.cpp │ ├── player_misc.cpp │ ├── player_options.cpp │ ├── player_options.hpp │ ├── player_playlists.cpp │ ├── playlist_mapping.cpp │ ├── playlist_mapping.hpp │ ├── plugin.cpp │ ├── plugin.hpp │ ├── prefs_page.cpp │ ├── prefs_page.hpp │ ├── resource.h │ ├── resource.rc │ ├── settings.cpp │ ├── settings.hpp │ ├── utils.cpp │ └── utils.hpp │ ├── gzip.cpp │ ├── gzip.hpp │ ├── http.cpp │ ├── http.hpp │ ├── json.hpp │ ├── log.cpp │ ├── log.hpp │ ├── outputs_controller.cpp │ ├── outputs_controller.hpp │ ├── parsing.cpp │ ├── parsing.hpp │ ├── play_queue_controller.cpp │ ├── play_queue_controller.hpp │ ├── player_api.hpp │ ├── player_api_json.cpp │ ├── player_api_json.hpp │ ├── player_api_parsers.cpp │ ├── player_api_parsers.hpp │ ├── player_controller.cpp │ ├── player_controller.hpp │ ├── player_events.cpp │ ├── player_events.hpp │ ├── playlists_controller.cpp │ ├── playlists_controller.hpp │ ├── project_info.hpp.in │ ├── query_controller.cpp │ ├── query_controller.hpp │ ├── request.cpp │ ├── request.hpp │ ├── request_filter.cpp │ ├── request_filter.hpp │ ├── response.cpp │ ├── response.hpp │ ├── response_headers_filter.cpp │ ├── response_headers_filter.hpp │ ├── response_sender.cpp │ ├── response_sender.hpp │ ├── router.cpp │ ├── router.hpp │ ├── safe_windows.h │ ├── server.cpp │ ├── server.hpp │ ├── server_core.hpp │ ├── server_host.cpp │ ├── server_host.hpp │ ├── server_thread.cpp │ ├── server_thread.hpp │ ├── settings.cpp │ ├── settings.hpp │ ├── static_controller.cpp │ ├── static_controller.hpp │ ├── string_utils.cpp │ ├── string_utils.hpp │ ├── system.cpp │ ├── system.hpp │ ├── system_posix.cpp │ ├── system_windows.cpp │ ├── tests │ ├── CMakeLists.txt │ ├── base64_tests.cpp │ ├── echo_server.cpp │ ├── fnv_hash_tests.cpp │ ├── parsing_tests.cpp │ ├── router_tests.cpp │ ├── runner.cpp │ ├── server_tests.cpp │ ├── string_utils_tests.cpp │ ├── test_main.hpp │ └── timers_tests.cpp │ ├── timers.cpp │ ├── timers.hpp │ ├── work_queue.cpp │ └── work_queue.hpp ├── docs ├── advanced-config.md ├── building.md └── player-api.yml ├── js ├── CMakeLists.txt ├── api_tests │ ├── CMakeLists.txt │ ├── package.json │ ├── sources.cmake │ ├── src │ │ ├── artwork_api_tests.js │ │ ├── authentication_tests.js │ │ ├── browser_api_tests.js │ │ ├── client_config_api_tests.js │ │ ├── deadbeef │ │ │ ├── player_controller.js │ │ │ └── test_context.js │ │ ├── event_expectation.js │ │ ├── foobar2000 │ │ │ ├── player_controller.js │ │ │ └── test_context.js │ │ ├── http_features_tests.js │ │ ├── main.js │ │ ├── outputs_api_tests.js │ │ ├── permissions_tests.js │ │ ├── play_queue_api_tests.js │ │ ├── player_api_tests.js │ │ ├── playlists_api_tests.js │ │ ├── query_api_tests.js │ │ ├── request_handler.js │ │ ├── static_files_tests.js │ │ ├── test_context.js │ │ ├── test_env.js │ │ ├── test_player_client.js │ │ └── utils.js │ ├── tracks │ │ ├── cover-white.png.hidden │ │ ├── cover.png │ │ ├── empty │ │ │ └── no-music-files │ │ ├── subdir │ │ │ ├── track2.flac │ │ │ └── track3.flac │ │ ├── track1.flac │ │ └── track2.flac │ └── webroot │ │ ├── file.css │ │ ├── file.htm │ │ ├── file.html │ │ ├── file.jpeg │ │ ├── file.jpg │ │ ├── file.js │ │ ├── file.png │ │ ├── file.svg │ │ ├── file.txt │ │ ├── index.html │ │ ├── large.txt │ │ └── subdir │ │ ├── file.html │ │ └── index.html ├── client │ ├── CMakeLists.txt │ ├── package.json │ ├── sources.cmake │ ├── src │ │ ├── enums.js │ │ ├── index.js │ │ ├── player_client.js │ │ └── utils.js │ └── webpack.config.js ├── config.mjs ├── package.json ├── update_sources.sh ├── webui │ ├── .babelrc │ ├── CMakeLists.txt │ ├── package.json │ ├── sources.cmake │ ├── src │ │ ├── about_box.js │ │ ├── album_art_viewer.js │ │ ├── app.js │ │ ├── app_model.js │ │ ├── columns.js │ │ ├── columns_settings.js │ │ ├── columns_settings_menu.js │ │ ├── columns_settings_model.js │ │ ├── control_bar.js │ │ ├── css_settings_controller.js │ │ ├── data_source.js │ │ ├── data_table.js │ │ ├── defaults_settings.js │ │ ├── dialogs.js │ │ ├── dom_utils.js │ │ ├── dropdown.js │ │ ├── elements.js │ │ ├── file_browser.js │ │ ├── file_browser_header.js │ │ ├── file_browser_model.js │ │ ├── general_settings.js │ │ ├── index.html │ │ ├── index.js │ │ ├── loader.gif │ │ ├── media_size_controller.js │ │ ├── media_theme_controller.js │ │ ├── model_binding.js │ │ ├── navigation_model.js │ │ ├── notification_group.js │ │ ├── notification_model.js │ │ ├── output_settings.js │ │ ├── output_settings_model.js │ │ ├── play_queue_model.js │ │ ├── playback_control.js │ │ ├── playback_info_bar.js │ │ ├── player_features.js │ │ ├── player_model.js │ │ ├── playlist_content.js │ │ ├── playlist_menu.js │ │ ├── playlist_model.js │ │ ├── playlist_switcher.js │ │ ├── position_control.js │ │ ├── request_handler.js │ │ ├── sandbox │ │ │ └── index.js │ │ ├── scroll_manager.js │ │ ├── service_context.js │ │ ├── setting_editor.js │ │ ├── settings_content.js │ │ ├── settings_header.js │ │ ├── settings_model.js │ │ ├── settings_model_base.js │ │ ├── settings_store.js │ │ ├── status_bar.js │ │ ├── style.less │ │ ├── tests │ │ │ ├── index.html │ │ │ └── index.js │ │ ├── timer.js │ │ ├── touch_mode_controller.js │ │ ├── urls.js │ │ ├── utils.js │ │ ├── view_switcher.js │ │ ├── volume_control.js │ │ └── window_controller.js │ └── webpack.config.js └── yarn.lock ├── scripts ├── dev_install.cmd ├── dev_install.sh ├── install │ ├── deadbeef.sh │ ├── foobar2000.cmd │ ├── foobar2000 │ │ ├── v1.6 │ │ │ ├── configuration │ │ │ │ └── Core.cfg │ │ │ └── theme.fth │ │ ├── v2.0-x64 │ │ │ └── profile │ │ │ │ ├── config.sqlite │ │ │ │ └── theme.fth │ │ ├── v2.0 │ │ │ └── profile │ │ │ │ ├── config.sqlite │ │ │ │ └── theme.fth │ │ ├── v2.1-x64 │ │ │ └── profile │ │ │ │ ├── config.sqlite │ │ │ │ └── theme.fth │ │ ├── v2.1 │ │ │ └── profile │ │ │ │ ├── config.sqlite │ │ │ │ └── theme.fth │ │ ├── v2.24-x64 │ │ │ └── profile │ │ │ │ ├── config.sqlite │ │ │ │ └── theme.fth │ │ └── v2.24 │ │ │ └── profile │ │ │ ├── config.sqlite │ │ │ └── theme.fth │ ├── install.sh │ └── patch.cmd ├── print_versions.sh └── update_version.sh └── version.sh /.clang-format: -------------------------------------------------------------------------------- 1 | # Generated from CLion C/C++ Code Style settings 2 | BasedOnStyle: LLVM 3 | AccessModifierOffset: -4 4 | AlignAfterOpenBracket: Align 5 | AlignConsecutiveAssignments: None 6 | AlignOperands: DontAlign 7 | AllowAllArgumentsOnNextLine: false 8 | AllowAllConstructorInitializersOnNextLine: false 9 | AllowAllParametersOfDeclarationOnNextLine: false 10 | AllowShortBlocksOnASingleLine: Always 11 | AllowShortCaseLabelsOnASingleLine: false 12 | AllowShortFunctionsOnASingleLine: None 13 | AllowShortIfStatementsOnASingleLine: Never 14 | AllowShortLambdasOnASingleLine: All 15 | AllowShortLoopsOnASingleLine: false 16 | AlwaysBreakAfterReturnType: None 17 | AlwaysBreakTemplateDeclarations: Yes 18 | BreakBeforeBraces: Custom 19 | BraceWrapping: 20 | AfterCaseLabel: false 21 | AfterClass: true 22 | AfterControlStatement: Always 23 | AfterEnum: true 24 | AfterFunction: true 25 | AfterNamespace: false 26 | AfterUnion: true 27 | BeforeCatch: true 28 | BeforeElse: true 29 | IndentBraces: false 30 | SplitEmptyFunction: true 31 | SplitEmptyRecord: true 32 | BreakBeforeBinaryOperators: NonAssignment 33 | BreakBeforeTernaryOperators: true 34 | BreakConstructorInitializers: BeforeColon 35 | BreakInheritanceList: BeforeColon 36 | ColumnLimit: 0 37 | CompactNamespaces: false 38 | ContinuationIndentWidth: 4 39 | IndentCaseLabels: false 40 | IndentPPDirectives: None 41 | IndentWidth: 4 42 | KeepEmptyLinesAtTheStartOfBlocks: true 43 | MaxEmptyLinesToKeep: 2 44 | NamespaceIndentation: None 45 | ObjCSpaceAfterProperty: false 46 | ObjCSpaceBeforeProtocolList: true 47 | PointerAlignment: Left 48 | ReflowComments: false 49 | SpaceAfterCStyleCast: true 50 | SpaceAfterLogicalNot: false 51 | SpaceAfterTemplateKeyword: false 52 | SpaceBeforeAssignmentOperators: true 53 | SpaceBeforeCpp11BracedList: false 54 | SpaceBeforeCtorInitializerColon: true 55 | SpaceBeforeInheritanceColon: true 56 | SpaceBeforeParens: ControlStatements 57 | SpaceBeforeRangeBasedForLoopColon: true 58 | SpaceInEmptyParentheses: false 59 | SpacesBeforeTrailingComments: 0 60 | SpacesInAngles: false 61 | SpacesInCStyleCastParentheses: false 62 | SpacesInContainerLiterals: true 63 | SpacesInParentheses: false 64 | SpacesInSquareBrackets: false 65 | TabWidth: 4 66 | UseTab: Never 67 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | end_of_line = lf 4 | indent_size = 4 5 | indent_style = space 6 | insert_final_newline = true 7 | max_line_length = 120 8 | tab_width = 4 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ['https://hyperblast.org/donate/'] 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: '/' 5 | schedule: 6 | interval: daily 7 | allow: 8 | - dependency-type: production 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | apps 3 | build 4 | ci_build 5 | cmake-build* 6 | node_modules 7 | 8 | js/.idea 9 | js/client/dist 10 | 11 | cpp/.idea 12 | cpp/build 13 | cpp/extlibs/.cache 14 | 15 | *.user 16 | *.orig 17 | *.rej 18 | *.so 19 | *.aps 20 | *.log 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015-2025 Hyperblast 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /ci/build.cmd: -------------------------------------------------------------------------------- 1 | @setlocal 2 | @cd "%~dp0.." 3 | 4 | @if [%BUILD_TYPE%] == [] ( 5 | @echo BUILD_TYPE is not set, aborting 6 | @cmd /c "exit 1" 7 | goto :end 8 | ) 9 | 10 | @if [%BUILD_ARCH%] == [] ( 11 | @echo BUILD_ARCH is not set, aborting 12 | @cmd /c "exit 1" 13 | goto :end 14 | ) 15 | 16 | @echo. 17 | @echo === Cleaning build directory === 18 | @echo. 19 | 20 | rmdir /S /Q ci_build\%BUILD_TYPE% 21 | mkdir ci_build\%BUILD_TYPE% 22 | cd ci_build\%BUILD_TYPE% 23 | 24 | @if errorlevel 1 goto :end 25 | 26 | @echo. 27 | @echo === Configuring === 28 | @echo. 29 | 30 | cmake ../.. -A %BUILD_ARCH% -DENABLE_TESTS=ON -DENABLE_GIT_REV=ON 31 | 32 | @if errorlevel 1 goto :end 33 | 34 | @echo. 35 | @echo === Building === 36 | @echo. 37 | 38 | cmake --build . --config %BUILD_TYPE% 39 | 40 | @if errorlevel 1 goto :end 41 | 42 | @echo. 43 | @echo === Creating packages === 44 | @echo. 45 | 46 | cpack -C %BUILD_TYPE% 47 | 48 | @if errorlevel 1 goto :end 49 | 50 | ren *.zip *.fb2k-component 51 | 52 | :end 53 | -------------------------------------------------------------------------------- /ci/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | function banner 6 | { 7 | echo 8 | echo "=== $1 ===" 9 | echo 10 | } 11 | 12 | function main 13 | { 14 | banner 'Cleaning build directory' 15 | rm -rf ci_build/$BUILD_TYPE 16 | mkdir -p ci_build/$BUILD_TYPE 17 | cd ci_build/$BUILD_TYPE 18 | 19 | banner 'Configuring' 20 | cmake ../.. \ 21 | -DCMAKE_BUILD_TYPE=$BUILD_TYPE \ 22 | -DENABLE_TESTS=ON \ 23 | -DENABLE_WERROR=ON \ 24 | -DENABLE_STATIC_STDLIB=ON \ 25 | -DENABLE_GIT_REV=ON 26 | 27 | banner 'Building' 28 | cmake --build . --parallel 29 | 30 | banner 'Creating packages' 31 | cpack -G TXZ 32 | cmake . -DENABLE_DEADBEEF_SINGLE_DIR=OFF 33 | cpack -G DEB 34 | } 35 | 36 | source "$(dirname $0)/run_in_docker.sh" 37 | -------------------------------------------------------------------------------- /ci/docker/.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | build-image.sh 3 | *.dockerfile 4 | -------------------------------------------------------------------------------- /ci/docker/build_image.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | cd "$(dirname $0)" 6 | 7 | if [ -z "$DOCKER_IMAGE" ]; then 8 | echo DOCKER_IMAGE is required 9 | exit 1 10 | fi 11 | 12 | docker_file="$DOCKER_IMAGE.dockerfile" 13 | 14 | if ! [ -f "$docker_file" ]; then 15 | echo "$docker_file does not exist" 16 | exit 1 17 | fi 18 | 19 | docker build -t "beefweb-dev:$DOCKER_IMAGE" -f "$docker_file" . 20 | -------------------------------------------------------------------------------- /ci/docker/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | url="$1" 6 | hash="$2" 7 | 8 | if [ -z "$url" ] || [ -z "$hash" ]; then 9 | echo "usage: $(basename $0) " 10 | exit 1 11 | fi 12 | 13 | file="${url##*/}" 14 | 15 | cd /usr/local 16 | 17 | curl --silent --fail --show-error --location -o "$file" "$url" 18 | echo "$hash *$file" | sha256sum -c 19 | tar xf "$file" --strip-components=1 20 | rm "$file" 21 | -------------------------------------------------------------------------------- /ci/docker/install_cmake.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | "$(dirname $0)/install.sh" \ 6 | 'https://github.com/Kitware/CMake/releases/download/v3.25.2/cmake-3.25.2-linux-x86_64.tar.gz' \ 7 | '783da74f132fd1fea91b8236d267efa4df5b91c5eec1dea0a87f0cf233748d99' 8 | -------------------------------------------------------------------------------- /ci/docker/install_node.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | "$(dirname $0)/install.sh" \ 6 | 'https://nodejs.org/dist/v12.22.12/node-v12.22.12-linux-x64.tar.xz' \ 7 | 'e6d052364bfa2c17da92cf31794100cfd709ba147415ddaeed2222eec9ca1469' 8 | 9 | export PATH="/usr/local/bin:$PATH" 10 | 11 | npm i -g yarn@1.22.22 12 | -------------------------------------------------------------------------------- /ci/docker/site-config.jam: -------------------------------------------------------------------------------- 1 | using gcc : 9 : /usr/bin/g++-9 ; 2 | -------------------------------------------------------------------------------- /ci/docker/ubuntu-14.04.dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:14.04 2 | 3 | ARG DEBIAN_FRONTEND=noninteractive 4 | 5 | COPY ./install.sh \ 6 | ./install_cmake.sh \ 7 | ./install_node.sh \ 8 | /scripts/ 9 | 10 | COPY ./site-config.jam /etc/ 11 | 12 | RUN apt-get update && \ 13 | apt-get upgrade -y && \ 14 | apt-get install -y \ 15 | tzdata software-properties-common libasound2 build-essential file curl git zlib1g-dev && \ 16 | add-apt-repository -y ppa:ubuntu-toolchain-r/test && \ 17 | apt-get update && \ 18 | apt-get install -y gcc-9 g++-9 && \ 19 | /scripts/install_cmake.sh && \ 20 | /scripts/install_node.sh 21 | 22 | ENV CC=gcc-9 23 | ENV CXX=g++-9 24 | 25 | CMD ["/bin/bash"] 26 | -------------------------------------------------------------------------------- /ci/docker/ubuntu-22.04.dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | 3 | ARG DEBIAN_FRONTEND=noninteractive 4 | 5 | RUN apt-get update && \ 6 | apt-get upgrade -y && \ 7 | apt-get install -y \ 8 | tzdata libasound2 build-essential file curl git cmake nodejs yarnpkg zlib1g-dev && \ 9 | ln -s /usr/bin/yarnpkg /usr/bin/yarn && \ 10 | git config --global --add safe.directory /work 11 | 12 | CMD ["/bin/bash"] 13 | -------------------------------------------------------------------------------- /ci/install.cmd: -------------------------------------------------------------------------------- 1 | setlocal 2 | cd "%~dp0.." 3 | 4 | cmd /c scripts\install\patch.cmd 5 | @if errorlevel 1 goto :end 6 | 7 | if "%BUILD_ARCH%" == "x64" ( 8 | cmd /c scripts\install\foobar2000.cmd v2.0-x64 9 | @if errorlevel 1 goto :end 10 | 11 | cmd /c scripts\install\foobar2000.cmd v2.1-x64 12 | @if errorlevel 1 goto :end 13 | 14 | cmd /c scripts\install\foobar2000.cmd v2.24-x64 15 | @if errorlevel 1 goto :end 16 | ) else ( 17 | cmd /c scripts\install\foobar2000.cmd v1.6 18 | @if errorlevel 1 goto :end 19 | 20 | cmd /c scripts\install\foobar2000.cmd v2.0 21 | @if errorlevel 1 goto :end 22 | 23 | cmd /c scripts\install\foobar2000.cmd v2.1 24 | @if errorlevel 1 goto :end 25 | 26 | cmd /c scripts\install\foobar2000.cmd v2.24 27 | @if errorlevel 1 goto :end 28 | ) 29 | 30 | :end 31 | -------------------------------------------------------------------------------- /ci/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | function main 6 | { 7 | cd scripts/install 8 | 9 | ./deadbeef.sh v1.8 10 | ./deadbeef.sh v1.9 11 | ./deadbeef.sh v1.10 12 | } 13 | 14 | source "$(dirname $0)/run_in_docker.sh" 15 | -------------------------------------------------------------------------------- /ci/run_in_docker.sh: -------------------------------------------------------------------------------- 1 | if [ -z "$BUILD_TYPE" ]; then 2 | echo BUILD_TYPE is required 3 | exit 1 4 | fi 5 | 6 | if [ "$IN_DOCKER" == "1" ]; then 7 | cd "$(dirname $0)/.." 8 | main 9 | exit 0 10 | fi 11 | 12 | if [ -z "$DOCKER_IMAGE" ]; then 13 | echo DOCKER_IMAGE is required 14 | exit 1 15 | fi 16 | 17 | SCRIPT_PATH_ABS="$(realpath "$0")" 18 | cd "$(dirname $0)/.." 19 | SCRIPT_PATH_REL="$(realpath --relative-to="$(pwd)" "$SCRIPT_PATH_ABS")" 20 | 21 | docker run --rm \ 22 | -e IN_DOCKER=1 -e BUILD_TYPE \ 23 | -v "$(pwd):/work:z" \ 24 | "beefweb-dev:$DOCKER_IMAGE" "/work/$SCRIPT_PATH_REL" 25 | -------------------------------------------------------------------------------- /ci/test.cmd: -------------------------------------------------------------------------------- 1 | setlocal 2 | cd "%~dp0.." 3 | 4 | set BEEFWEB_BINARY_DIR_BASE=ci_build 5 | set BEEFWEB_TEST_BUILD_TYPE=%BUILD_TYPE% 6 | 7 | @echo. 8 | @echo === Running server tests === 9 | @echo. 10 | ci_build\%BUILD_TYPE%\cpp\server\tests\%BUILD_TYPE%\core_tests.exe 11 | @if errorlevel 1 goto :end 12 | 13 | set API_TEST_ERROR=0 14 | 15 | @pushd js\api_tests 16 | 17 | @if "%BUILD_ARCH%" == "x64" ( 18 | @echo. 19 | @echo === Running API tests on foobar2000 v2.0-x64 === 20 | @echo. 21 | set BEEFWEB_TEST_FOOBAR2000_VERSION=v2.0-x64 22 | cmd /c yarn test 23 | @if errorlevel 1 set API_TEST_ERROR=1 24 | 25 | @echo. 26 | @echo === Running API tests on foobar2000 v2.1-x64 === 27 | @echo. 28 | set BEEFWEB_TEST_FOOBAR2000_VERSION=v2.1-x64 29 | cmd /c yarn test 30 | @if errorlevel 1 set API_TEST_ERROR=1 31 | 32 | @echo. 33 | @echo === Running API tests on foobar2000 v2.24-x64 === 34 | @echo. 35 | set BEEFWEB_TEST_FOOBAR2000_VERSION=v2.24-x64 36 | cmd /c yarn test 37 | @if errorlevel 1 set API_TEST_ERROR=1 38 | ) else ( 39 | @echo. 40 | @echo === Running API tests on foobar2000 v1.6 === 41 | @echo. 42 | set BEEFWEB_TEST_FOOBAR2000_VERSION=v1.6 43 | cmd /c yarn test 44 | @if errorlevel 1 set API_TEST_ERROR=1 45 | 46 | @echo. 47 | @echo === Running API tests on foobar2000 v2.0 === 48 | @echo. 49 | set BEEFWEB_TEST_FOOBAR2000_VERSION=v2.0 50 | cmd /c yarn test 51 | @if errorlevel 1 set API_TEST_ERROR=1 52 | 53 | @echo. 54 | @echo === Running API tests on foobar2000 v2.1 === 55 | @echo. 56 | set BEEFWEB_TEST_FOOBAR2000_VERSION=v2.1 57 | cmd /c yarn test 58 | @if errorlevel 1 set API_TEST_ERROR=1 59 | 60 | @echo. 61 | @echo === Running API tests on foobar2000 v2.24 === 62 | @echo. 63 | set BEEFWEB_TEST_FOOBAR2000_VERSION=v2.24 64 | cmd /c yarn test 65 | @if errorlevel 1 set API_TEST_ERROR=1 66 | ) 67 | 68 | @popd 69 | 70 | exit %API_TEST_ERROR% 71 | 72 | :end 73 | -------------------------------------------------------------------------------- /ci/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | function banner 6 | { 7 | echo 8 | echo "=== $1 ===" 9 | echo 10 | } 11 | 12 | function run_server_tests 13 | { 14 | banner 'Running server tests' 15 | ci_build/$BUILD_TYPE/cpp/server/tests/core_tests 16 | } 17 | 18 | function run_api_tests 19 | { 20 | ( 21 | banner "Running API tests on deadbeef $1" 22 | 23 | export BEEFWEB_BINARY_DIR_BASE=ci_build 24 | export BEEFWEB_TEST_DEADBEEF_VERSION=$1 25 | export BEEFWEB_TEST_BUILD_TYPE=$BUILD_TYPE 26 | 27 | apps/deadbeef/$1/deadbeef --version 28 | cd js/api_tests 29 | yarn test 30 | ) 31 | } 32 | 33 | function main 34 | { 35 | run_server_tests 36 | run_api_tests v1.8 37 | run_api_tests v1.9 38 | run_api_tests v1.10 39 | } 40 | 41 | source "$(dirname $0)/run_in_docker.sh" 42 | -------------------------------------------------------------------------------- /cmake/CPackProjectConfig.cmake: -------------------------------------------------------------------------------- 1 | if(${CPACK_GENERATOR} STREQUAL "TXZ") 2 | if(NOT CPACK_DEADBEEF_SINGLE_DIR) 3 | message(FATAL_ERROR "ENABLE_DEADBEEF_SINGLE_DIR should be ON when building .tar.xz package") 4 | endif() 5 | endif() 6 | 7 | if(${CPACK_GENERATOR} STREQUAL "DEB") 8 | if(CPACK_DEADBEEF_SINGLE_DIR) 9 | message(FATAL_ERROR "ENABLE_DEADBEEF_SINGLE_DIR should be OFF when building .deb package") 10 | endif() 11 | 12 | set(CPACK_PACKAGING_INSTALL_PREFIX "/opt/deadbeef") 13 | endif() 14 | -------------------------------------------------------------------------------- /cmake/EnvInfo.cmake: -------------------------------------------------------------------------------- 1 | set(OS_WINDOWS OFF) 2 | set(OS_POSIX OFF) 3 | 4 | if(WIN32 AND NOT CYGWIN) 5 | set(OS_WINDOWS ON) 6 | elseif(UNIX) 7 | set(OS_POSIX ON) 8 | set(SCRIPT_SUFFIX .sh) 9 | else() 10 | message(SEND_ERROR "Target OS is not Windows or POSIX" ) 11 | endif() 12 | 13 | set(HOST_OS_WINDOWS OFF) 14 | set(HOST_OS_POSIX OFF) 15 | 16 | if(CMAKE_HOST_SYSTEM_NAME MATCHES "^Windows.*") 17 | set(HOST_OS_WINDOWS ON) 18 | set(SCRIPT_SUFFIX .cmd) 19 | else() 20 | set(HOST_OS_POSIX ON) 21 | set(SCRIPT_SUFFIX .sh) 22 | endif() 23 | 24 | set(CXX_GCC OFF) 25 | set(CXX_CLANG OFF) 26 | set(CXX_MSVC OFF) 27 | 28 | if(CMAKE_CXX_COMPILER_ID STREQUAL GNU) 29 | set(CXX_GCC ON) 30 | elseif(CMAKE_CXX_COMPILER_ID STREQUAL Clang) 31 | set(CXX_CLANG ON) 32 | elseif(MSVC) 33 | set(CXX_MSVC ON) 34 | endif() 35 | 36 | get_property(IS_MULTI_CONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) 37 | -------------------------------------------------------------------------------- /cmake/Packages.cmake: -------------------------------------------------------------------------------- 1 | set(CPACK_PROJECT_CONFIG_FILE "${CMAKE_SOURCE_DIR}/cmake/CPackProjectConfig.cmake") 2 | set(CPACK_DEADBEEF_SINGLE_DIR "${ENABLE_DEADBEEF_SINGLE_DIR}") 3 | set(CPACK_DEBIAN_PACKAGE_VERSION "${CMAKE_PROJECT_VERSION}") 4 | 5 | set(PACKAGE_SUFFIX "-${CMAKE_PROJECT_VERSION}") 6 | 7 | if(NOT PROJECT_VERSION_FINAL) 8 | set(PACKAGE_SUFFIX "${PACKAGE_SUFFIX}_${PROJECT_GIT_REV}") 9 | set(CPACK_DEBIAN_PACKAGE_VERSION "${CPACK_DEBIAN_PACKAGE_VERSION}~git.${PROJECT_GIT_REV}") 10 | endif() 11 | 12 | if(${CMAKE_SIZEOF_VOID_P} STREQUAL 4) 13 | set(PACKAGE_SUFFIX "${PACKAGE_SUFFIX}-x86") 14 | elseif(${CMAKE_SIZEOF_VOID_P} STREQUAL 8) 15 | set(PACKAGE_SUFFIX "${PACKAGE_SUFFIX}-x86_64") 16 | endif() 17 | 18 | set(CPACK_ARCHIVE_DEADBEEF_PLUGIN_FILE_NAME "${DEADBEEF_PACKAGE_NAME}${PACKAGE_SUFFIX}") 19 | set(CPACK_ARCHIVE_FOOBAR2000_PLUGIN_FILE_NAME "${FOOBAR2000_PACKAGE_NAME}${PACKAGE_SUFFIX}") 20 | 21 | unset(PACKAGE_SUFFIX) 22 | 23 | set(CPACK_STRIP_FILES ON) 24 | set(CPACK_ARCHIVE_COMPONENT_INSTALL ON) 25 | set(CPACK_DEB_COMPONENT_INSTALL ON) 26 | set(CPACK_INCLUDE_TOPLEVEL_DIRECTORY OFF) 27 | 28 | if(OS_WINDOWS) 29 | set(CPACK_GENERATOR ZIP) 30 | else() 31 | set(CPACK_GENERATOR TXZ) 32 | endif() 33 | 34 | set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT) 35 | set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) 36 | 37 | if(ENABLE_DEADBEEF) 38 | set(CPACK_COMPONENTS_ALL deadbeef_plugin) 39 | endif() 40 | 41 | if(ENABLE_FOOBAR2000) 42 | set(CPACK_COMPONENTS_ALL foobar2000_plugin) 43 | endif() 44 | -------------------------------------------------------------------------------- /cmake/ProjectInfo.cmake: -------------------------------------------------------------------------------- 1 | set(DEADBEEF_ENTRY_POINT "beefweb_load") 2 | set(DEADBEEF_PLUGIN_FILE "beefweb") 3 | set(DEADBEEF_PACKAGE_NAME "ddb_beefweb") 4 | 5 | set(FOOBAR2000_PLUGIN_FILE "foo_beefweb") 6 | set(FOOBAR2000_PACKAGE_NAME "foo_beefweb") 7 | 8 | set(WEBUI_ROOT "beefweb.root") 9 | set(WEBUI_LICENSES_FILE "third-party-licenses.txt") 10 | 11 | set(CPACK_PACKAGE_VENDOR "Hyperblast") 12 | set(CPACK_PACKAGE_CONTACT "Hyperblast ") 13 | 14 | set(CPACK_DEBIAN_DEADBEEF_PLUGIN_PACKAGE_NAME "deadbeef-beefweb") 15 | set(CPACK_DEBIAN_DEADBEEF_PLUGIN_PACKAGE_DEPENDS "deadbeef-static (>=1.8.0)") 16 | set(CPACK_DEBIAN_DEADBEEF_PLUGIN_PACKAGE_SECTION "sound") 17 | 18 | set( 19 | CPACK_DEBIAN_DEADBEEF_PLUGIN_DESCRIPTION 20 | "Plugin for DeaDBeeF that provides web interface and HTTP API for controlling player remotely" 21 | ) 22 | -------------------------------------------------------------------------------- /cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(COMMON_C_FLAGS "") 2 | 3 | if(OS_POSIX) 4 | set(COMMON_C_FLAGS "${COMMON_C_FLAGS} -fPIC -pthread -D_GNU_SOURCE=1 -D_FILE_OFFSET_BITS=64") 5 | endif() 6 | 7 | if(CXX_MSVC) 8 | set(COMMON_C_FLAGS "${COMMON_C_FLAGS} /MP /D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR") 9 | endif() 10 | 11 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${COMMON_C_FLAGS}") 12 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${COMMON_C_FLAGS}") 13 | 14 | if(CXX_MSVC) 15 | if(ENABLE_STATIC_STDLIB) 16 | foreach(LANG_NAME C CXX) 17 | foreach(BUILD_TYPE DEBUG RELEASE MINSIZEREL RELWITHDEBINFO) 18 | set(VAR_NAME CMAKE_${LANG_NAME}_FLAGS_${BUILD_TYPE}) 19 | string(REPLACE "/MD" "/MT" ${VAR_NAME} "${${VAR_NAME}}") 20 | endforeach() 21 | endforeach() 22 | endif() 23 | 24 | foreach(TARGET EXE SHARED MODULE) 25 | set(VAR_NAME CMAKE_${TARGET}_LINKER_FLAGS_RELWITHDEBINFO) 26 | string(REPLACE "/INCREMENTAL" "/INCREMENTAL:NO" ${VAR_NAME} "${${VAR_NAME}}") 27 | set(${VAR_NAME} "${${VAR_NAME}} /OPT:REF /OPT:ICF") 28 | endforeach() 29 | endif() 30 | 31 | add_subdirectory(extlibs) 32 | add_subdirectory(server) 33 | -------------------------------------------------------------------------------- /cpp/extlibs/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(CMAKE_FOLDER cpp_libs) 2 | set(EXTLIB_CACHE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/.cache) 3 | set(EXTLIB_PATCHER ${CMAKE_CURRENT_SOURCE_DIR}/apply_patches${SCRIPT_SUFFIX}) 4 | 5 | set( 6 | EXTLIB_CMAKE_ARGS 7 | -DCMAKE_BUILD_TYPE=$ 8 | -DCMAKE_PREFIX_PATH=${EXTLIB_INSTALL_DIR} 9 | -DCMAKE_INSTALL_PREFIX=${EXTLIB_INSTALL_DIR} 10 | "-DCMAKE_C_FLAGS=${CMAKE_C_FLAGS}" 11 | "-DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS}" 12 | ) 13 | 14 | set(EXTLIB_C_FLAGS "${CMAKE_C_FLAGS}") 15 | set(EXTLIB_CXX_FLAGS "${CMAKE_CXX_FLAGS}") 16 | 17 | foreach(BUILD_TYPE DEBUG RELEASE MINSIZEREL RELWITHDEBINFO) 18 | set( 19 | EXTLIB_CMAKE_ARGS 20 | ${EXTLIB_CMAKE_ARGS} 21 | "-DCMAKE_C_FLAGS_${BUILD_TYPE}=${CMAKE_C_FLAGS_${BUILD_TYPE}}" 22 | "-DCMAKE_CXX_FLAGS_${BUILD_TYPE}=${CMAKE_CXX_FLAGS_${BUILD_TYPE}}" 23 | ) 24 | 25 | set( 26 | EXTLIB_C_FLAGS 27 | "${EXTLIB_C_FLAGS} $<$:${CMAKE_C_FLAGS_${BUILD_TYPE}}>" 28 | ) 29 | 30 | set( 31 | EXTLIB_CXX_FLAGS 32 | "${EXTLIB_CXX_FLAGS} $<$:${CMAKE_CXX_FLAGS_${BUILD_TYPE}}>" 33 | ) 34 | endforeach() 35 | 36 | option_dependency_check(ENABLE_STATIC_STDLIB ENABLE_LOCAL_BOOST) 37 | 38 | function(extlib_target TARGET_NAME) 39 | set_property(TARGET ${TARGET_NAME} PROPERTY FOLDER cpp_libs) 40 | add_dependencies(ext_all ${TARGET_NAME}) 41 | endfunction() 42 | 43 | add_custom_target(ext_all) 44 | 45 | if(ENABLE_LOCAL_BOOST) 46 | add_subdirectory(boost) 47 | endif() 48 | 49 | if(ENABLE_LOCAL_NLJSON) 50 | add_subdirectory(nljson) 51 | endif() 52 | 53 | if(ENABLE_LOCAL_DEADBEEF) 54 | add_subdirectory(deadbeef) 55 | endif() 56 | 57 | if(ENABLE_LOCAL_CATCH) 58 | add_subdirectory(catch) 59 | endif() 60 | 61 | if(ENABLE_LOCAL_STRINGENCODERS) 62 | add_subdirectory(stringencoders) 63 | endif() 64 | 65 | if(ENABLE_LOCAL_ZLIB) 66 | add_subdirectory(zlib) 67 | endif() 68 | 69 | if(ENABLE_FOOBAR2000) 70 | add_subdirectory(foosdk) 71 | endif() 72 | -------------------------------------------------------------------------------- /cpp/extlibs/apply_patches.cmd: -------------------------------------------------------------------------------- 1 | @setlocal 2 | 3 | @if "%1" == "" @goto :usage 4 | 5 | @set src_dir=%~dp0%1 6 | @set patch_tool_dir=%~dp0..\..\apps\patch 7 | 8 | @if exist "%patch_tool_dir%\patch.exe" set PATH=%patch_tool_dir%;%PATH% 9 | 10 | @call :apply_patches "%src_dir%\patches" 11 | @if errorlevel 1 goto :error 12 | 13 | @call :apply_patches "%src_dir%\patches.windows" 14 | @if errorlevel 1 goto :error 15 | 16 | @call :copy_files "%src_dir%\files" 17 | @if errorlevel 1 goto :error 18 | 19 | @call :copy_files "%src_dir%\files.windows" 20 | @if errorlevel 1 goto :error 21 | 22 | @goto :end 23 | 24 | :apply_patches 25 | @if not exist "%1\." @goto :end 26 | 27 | @for %%f in (%1\*.patch) do @( 28 | @echo applying %%~nxf 29 | @patch.exe -p1 --batch --binary < "%%f" 30 | @if errorlevel 1 goto :error 31 | ) 32 | 33 | @goto :end 34 | 35 | :copy_files 36 | @if not exist "%1\." @goto :end 37 | @xcopy /E /F /Y "%1\*" . 38 | @goto :end 39 | 40 | :usage 41 | @echo usage: %~nx0 target 42 | @echo. 43 | @exit /b 1 44 | 45 | :error 46 | @exit /b %errorlevel% 47 | 48 | :end 49 | -------------------------------------------------------------------------------- /cpp/extlibs/apply_patches.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | target="$1" 6 | 7 | if [ -z "$target" ]; then 8 | echo "usage: $(basename $0) target" 9 | exit 1 10 | fi 11 | 12 | build_dir="$(pwd)" 13 | src_dir="$(readlink -f $(dirname $0))/$target" 14 | patches_dir="$src_dir/patches" 15 | files_dir="$src_dir/files" 16 | 17 | if [ -d "$patches_dir" ]; then 18 | for patch_file in $patches_dir/*.patch; do 19 | echo "applying $(basename $patch_file)" 20 | patch -p1 --batch < "$patch_file" 21 | done 22 | fi 23 | 24 | if [ -d "$files_dir" ]; then 25 | (cd "$files_dir"; cp -r -v -t "$build_dir" *) 26 | fi 27 | -------------------------------------------------------------------------------- /cpp/extlibs/boost/files.windows/install.cmd: -------------------------------------------------------------------------------- 1 | :: Why custom installation script? 2 | 3 | :: Standard install target copies single files with copy command which is very slow 4 | :: Version prefix is added to installation directory which is unwanted 5 | :: Libraries need to be renamed to have fixed name regardless of build options 6 | 7 | @if "%1" == "" @goto :usage 8 | 9 | xcopy /E /I /Y boost\* "%~f1\include\boost" 10 | @if errorlevel 1 @goto :end 11 | 12 | if not exist "%~f1\lib\." mkdir "%~f1\lib" 13 | @if errorlevel 1 @goto :end 14 | 15 | call :copylib "%1" libboost_system 16 | @if errorlevel 1 @goto :end 17 | 18 | call :copylib "%1" libboost_filesystem 19 | @if errorlevel 1 @goto :end 20 | 21 | call :copylib "%1" libboost_thread 22 | @if errorlevel 1 @goto :end 23 | 24 | @goto :end 25 | 26 | :copylib 27 | copy /B /Y stage\lib\%2*.lib "%~f1\lib\%2.lib" 28 | @goto :end 29 | 30 | :usage 31 | @echo usage: %~nx0 target-dir 32 | @echo. 33 | @cmd /c exit 1 34 | 35 | :end 36 | -------------------------------------------------------------------------------- /cpp/extlibs/boost/local/FindBoost.cmake: -------------------------------------------------------------------------------- 1 | set(Boost_INCLUDE_DIRS ${EXTLIB_INSTALL_DIR}/include) 2 | 3 | if(MSVC) 4 | set( 5 | Boost_LIBRARIES 6 | ${EXTLIB_INSTALL_DIR}/lib/libboost_system.lib 7 | ${EXTLIB_INSTALL_DIR}/lib/libboost_filesystem.lib 8 | ${EXTLIB_INSTALL_DIR}/lib/libboost_thread.lib 9 | ) 10 | else() 11 | set( 12 | Boost_LIBRARIES 13 | ${EXTLIB_INSTALL_DIR}/lib/libboost_system.a 14 | ${EXTLIB_INSTALL_DIR}/lib/libboost_filesystem.a 15 | ${EXTLIB_INSTALL_DIR}/lib/libboost_thread.a 16 | ) 17 | endif() 18 | -------------------------------------------------------------------------------- /cpp/extlibs/catch/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(ExternalProject) 2 | 3 | ExternalProject_Add( 4 | ext_catch 5 | PREFIX 6 | ${EXTLIB_INSTALL_DIR} 7 | URL 8 | https://github.com/catchorg/Catch2/releases/download/v2.13.10/catch.hpp 9 | URL_HASH 10 | SHA256=3725c0f0a75f376a5005dde31ead0feb8f7da7507644c201b814443de8355170 11 | DOWNLOAD_DIR 12 | ${EXTLIB_CACHE_DIR}/catch 13 | DOWNLOAD_NO_EXTRACT 1 14 | CONFIGURE_COMMAND "" 15 | BUILD_COMMAND "" 16 | INSTALL_COMMAND 17 | ${CMAKE_COMMAND} -E copy_if_different ${EXTLIB_INSTALL_DIR}/include/catch.hpp 18 | LOG_DOWNLOAD 0 LOG_UPDATE 0 LOG_CONFIGURE 0 LOG_BUILD 0 LOG_INSTALL 1 19 | ) 20 | 21 | extlib_target(ext_catch) 22 | -------------------------------------------------------------------------------- /cpp/extlibs/catch/local/FindCatch.cmake: -------------------------------------------------------------------------------- 1 | set(CATCH_INCLUDE_DIRS ${EXTLIB_INSTALL_DIR}/include) 2 | -------------------------------------------------------------------------------- /cpp/extlibs/catch/system/FindCatch.cmake: -------------------------------------------------------------------------------- 1 | find_path( 2 | CATCH_INCLUDE_DIRS 3 | NAMES catch.hpp 4 | DOC "catch include directory" 5 | ) 6 | 7 | include(FindPackageHandleStandardArgs) 8 | 9 | find_package_handle_standard_args( 10 | CATCH REQUIRED_VARS CATCH_INCLUDE_DIRS 11 | ) 12 | 13 | mark_as_advanced(CATCH_INCLUDE_DIRS) 14 | -------------------------------------------------------------------------------- /cpp/extlibs/deadbeef/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(ExternalProject) 2 | 3 | ExternalProject_Add( 4 | ext_deadbeef 5 | PREFIX 6 | ${EXTLIB_INSTALL_DIR} 7 | URL 8 | https://github.com/hyperblast/deadbeef_headers/archive/88517e52ce5647953328bff9de051db658c1f1f1.zip 9 | DOWNLOAD_DIR 10 | ${EXTLIB_CACHE_DIR}/deadbeef 11 | CONFIGURE_COMMAND "" 12 | BUILD_COMMAND "" 13 | INSTALL_COMMAND 14 | ${CMAKE_COMMAND} -E copy_directory /include/ ${EXTLIB_INSTALL_DIR}/include/ 15 | LOG_DOWNLOAD 1 LOG_UPDATE 0 LOG_CONFIGURE 0 LOG_BUILD 0 LOG_INSTALL 1 16 | ) 17 | 18 | extlib_target(ext_deadbeef) 19 | -------------------------------------------------------------------------------- /cpp/extlibs/deadbeef/local/FindDeadbeef.cmake: -------------------------------------------------------------------------------- 1 | set(DEADBEEF_INCLUDE_DIRS ${EXTLIB_INSTALL_DIR}/include) 2 | -------------------------------------------------------------------------------- /cpp/extlibs/deadbeef/system/FindDeadbeef.cmake: -------------------------------------------------------------------------------- 1 | find_path( 2 | DEADBEEF_INCLUDE_DIRS 3 | NAMES deadbeef/deadbeef.h 4 | PATHS /opt/deadbeef/include 5 | DOC "deadbeef include directory" 6 | ) 7 | 8 | include(FindPackageHandleStandardArgs) 9 | 10 | find_package_handle_standard_args( 11 | DEADBEEF REQUIRED_VARS DEADBEEF_INCLUDE_DIRS 12 | ) 13 | 14 | mark_as_advanced(DEADBEEF_INCLUDE_DIRS) 15 | -------------------------------------------------------------------------------- /cpp/extlibs/foosdk/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(ExternalProject) 2 | 3 | ExternalProject_Add( 4 | ext_foosdk 5 | PREFIX 6 | ${EXTLIB_INSTALL_DIR} 7 | URL 8 | https://github.com/hyperblast/foosdk/archive/d5a694f3af9f3bb4803dc6eea38331e781ae4445.zip 9 | DOWNLOAD_DIR 10 | ${EXTLIB_CACHE_DIR}/foosdk 11 | PATCH_COMMAND 12 | ${EXTLIB_PATCHER} foosdk 13 | CMAKE_ARGS 14 | ${EXTLIB_CMAKE_ARGS} 15 | -DFOO_PPUI=OFF 16 | -DFOO_SDK_HELPERS=OFF 17 | -DFOO_STATIC_STDLIB=OFF 18 | LOG_DOWNLOAD 1 LOG_UPDATE 0 LOG_CONFIGURE 1 LOG_BUILD 1 LOG_INSTALL 1 19 | ) 20 | 21 | extlib_target(ext_foosdk) 22 | -------------------------------------------------------------------------------- /cpp/extlibs/foosdk/local/FindFoosdk.cmake: -------------------------------------------------------------------------------- 1 | set(FOOSDK_INCLUDE_DIRS ${EXTLIB_INSTALL_DIR}/include) 2 | 3 | if(${CMAKE_SIZEOF_VOID_P} STREQUAL 4) 4 | set(FOOSDK_ARCH_SUFFIX "Win32") 5 | elseif(${CMAKE_SIZEOF_VOID_P} STREQUAL 8) 6 | set(FOOSDK_ARCH_SUFFIX "x64") 7 | else() 8 | message(SEND_ERROR "Unknown pointer size") 9 | endif() 10 | 11 | set( 12 | FOOSDK_LIBRARIES 13 | ${EXTLIB_INSTALL_DIR}/lib/foosdk.lib 14 | ${EXTLIB_INSTALL_DIR}/lib/shared-${FOOSDK_ARCH_SUFFIX}.lib 15 | ) 16 | -------------------------------------------------------------------------------- /cpp/extlibs/foosdk/patches/001-disable-debug.patch: -------------------------------------------------------------------------------- 1 | diff --git a/sdk/foobar2000/foobar2000_component_client/component_client.cpp b/sdk/foobar2000/foobar2000_component_client/component_client.cpp 2 | index 7ad217a..d7a1017 100644 3 | --- a/sdk/foobar2000/foobar2000_component_client/component_client.cpp 4 | +++ b/sdk/foobar2000/foobar2000_component_client/component_client.cpp 5 | @@ -107,7 +107,7 @@ namespace { 6 | } 7 | 8 | bool is_debug() override { 9 | - return PFC_DEBUG != 0; 10 | + return false; 11 | } 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /cpp/extlibs/nljson/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(ExternalProject) 2 | 3 | ExternalProject_Add( 4 | ext_nljson 5 | PREFIX 6 | ${EXTLIB_INSTALL_DIR} 7 | URL 8 | https://github.com/nlohmann/json/releases/download/v3.11.3/json.hpp 9 | URL_HASH 10 | SHA256=9bea4c8066ef4a1c206b2be5a36302f8926f7fdc6087af5d20b417d0cf103ea6 11 | DOWNLOAD_DIR 12 | ${EXTLIB_CACHE_DIR}/nljson 13 | DOWNLOAD_NO_EXTRACT 1 14 | CONFIGURE_COMMAND "" 15 | BUILD_COMMAND "" 16 | INSTALL_COMMAND 17 | ${CMAKE_COMMAND} -E copy_if_different ${EXTLIB_INSTALL_DIR}/include/nlohmann/json.hpp 18 | LOG_DOWNLOAD 0 LOG_UPDATE 0 LOG_CONFIGURE 0 LOG_BUILD 0 LOG_INSTALL 1 19 | ) 20 | 21 | extlib_target(ext_nljson) 22 | -------------------------------------------------------------------------------- /cpp/extlibs/nljson/local/FindNljson.cmake: -------------------------------------------------------------------------------- 1 | set(NLJSON_INCLUDE_DIRS ${EXTLIB_INSTALL_DIR}/include) 2 | -------------------------------------------------------------------------------- /cpp/extlibs/nljson/system/FindNljson.cmake: -------------------------------------------------------------------------------- 1 | find_path( 2 | NLJSON_INCLUDE_DIRS 3 | NAMES nlohmann/json.h 4 | DOC "nlohmann json include directory" 5 | ) 6 | 7 | include(FindPackageHandleStandardArgs) 8 | 9 | find_package_handle_standard_args( 10 | NLJSON REQUIRED_VARS NLJSON_INCLUDE_DIR 11 | ) 12 | 13 | mark_as_advanced(NLJSON_INCLUDE_DIRS) 14 | -------------------------------------------------------------------------------- /cpp/extlibs/stringencoders/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(ExternalProject) 2 | 3 | ExternalProject_Add( 4 | ext_stringencoders 5 | PREFIX 6 | ${EXTLIB_INSTALL_DIR} 7 | URL 8 | https://github.com/client9/stringencoders/archive/e1448a9415f4ebf6f559c86718193ba067cbb99d.zip 9 | URL_HASH 10 | SHA256=96b4083d73a4d769ceabd7d40dbf54b03ad897d8159ec5569ed5ad9e78798c54 11 | DOWNLOAD_DIR 12 | ${EXTLIB_CACHE_DIR}/stringencoders 13 | PATCH_COMMAND 14 | ${EXTLIB_PATCHER} stringencoders 15 | CMAKE_ARGS 16 | ${EXTLIB_CMAKE_ARGS} 17 | LOG_DOWNLOAD 1 LOG_UPDATE 0 LOG_CONFIGURE 1 LOG_BUILD 1 LOG_INSTALL 1 18 | ) 19 | 20 | extlib_target(ext_stringencoders) 21 | -------------------------------------------------------------------------------- /cpp/extlibs/stringencoders/files/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.0) 2 | 3 | project(zlib C) 4 | 5 | set( 6 | SOURCE_FILES 7 | src/modp_ascii.c 8 | src/modp_ascii_data.h 9 | src/modp_b16.c 10 | src/modp_b16_data.h 11 | src/modp_b2.c 12 | src/modp_b2_data.h 13 | src/modp_b64.c 14 | src/modp_b64_data.h 15 | # src/modp_b64r.c 16 | # src/modp_b64r_data.h 17 | # src/modp_b64w.c 18 | # src/modp_b64w_data.h 19 | # src/modp_b85.c 20 | # src/modp_b85_data.h 21 | src/modp_bjavascript.c 22 | src/modp_bjavascript_data.h 23 | src/modp_burl.c 24 | src/modp_burl_data.h 25 | # src/modp_html.c 26 | src/modp_json.c 27 | src/modp_numtoa.c 28 | src/modp_qsiter.c 29 | src/modp_utf8.c 30 | src/modp_xml.c 31 | ) 32 | 33 | set( 34 | HEADER_FILES 35 | src/extern_c_begin.h 36 | src/extern_c_end.h 37 | src/modp_ascii.h 38 | src/modp_b16.h 39 | src/modp_b2.h 40 | src/modp_b64.h 41 | # src/modp_b64r.h 42 | # src/modp_b64w.h 43 | # src/modp_b85.h 44 | src/modp_bjavascript.h 45 | src/modp_burl.h 46 | # src/modp_html.h 47 | src/modp_json.h 48 | src/modp_numtoa.h 49 | src/modp_qsiter.h 50 | src/modp_stdint.h 51 | src/modp_utf8.h 52 | src/modp_xml.h 53 | ) 54 | 55 | add_library(modpbase64 STATIC ${SOURCE_FILES} ${HEADER_FILES}) 56 | 57 | install(TARGETS modpbase64 ARCHIVE DESTINATION lib) 58 | install(FILES ${HEADER_FILES} DESTINATION include) 59 | -------------------------------------------------------------------------------- /cpp/extlibs/stringencoders/files/src/modp_b85_data.h: -------------------------------------------------------------------------------- 1 | /* do not edit -- autogenerated from b85gen */ 2 | static const uint32_t gsCharToInt[256] = { 3 | 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 4 | 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 5 | 99, 99, 99, 99, 99, 99, 99, 99, 99, 0, 99, 1, 6 | 2, 3, 99, 4, 5, 6, 7, 8, 99, 9, 10, 11, 7 | 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 99, 8 | 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 9 | 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 10 | 47, 48, 49, 50, 51, 52, 53, 54, 99, 55, 56, 57, 11 | 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 12 | 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 13 | 82, 83, 84, 99, 99, 99, 99, 99, 99, 99, 99, 99, 14 | 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 15 | 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 16 | 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 17 | 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 18 | 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 19 | 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 20 | 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 21 | 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 22 | 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 23 | 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 24 | 99, 99, 99, 99 25 | }; 26 | static const uint8_t gsIntToChar[85] = { 27 | '!', '#', '$', '%', '\'', '(', ')', '*', '+', '-', 28 | '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', 29 | '8', '9', ':', '<', '=', '>', '?', '@', 'A', 'B', 30 | 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 31 | 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 32 | 'W', 'X', 'Y', 'Z', '[', ']', '^', '_', '`', 'a', 33 | 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 34 | 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 35 | 'v', 'w', 'x', 'y', 'z' 36 | }; 37 | -------------------------------------------------------------------------------- /cpp/extlibs/stringencoders/local/FindStringEncoders.cmake: -------------------------------------------------------------------------------- 1 | set(STRINGENCODERS_INCLUDE_DIRS ${EXTLIB_INSTALL_DIR}/include) 2 | 3 | if(MSVC) 4 | set(STRINGENCODERS_LIBRARIES ${EXTLIB_INSTALL_DIR}/lib/modpbase64.lib) 5 | else() 6 | set(STRINGENCODERS_LIBRARIES ${EXTLIB_INSTALL_DIR}/lib/libmodpbase64.a) 7 | endif() 8 | -------------------------------------------------------------------------------- /cpp/extlibs/stringencoders/patches/001-no-stdint-hacks.patch: -------------------------------------------------------------------------------- 1 | diff --git a/src/modp_stdint.h b/src/modp_stdint.h 2 | index bd9dea3..99100f8 100644 3 | --- a/src/modp_stdint.h 4 | +++ b/src/modp_stdint.h 5 | @@ -14,30 +14,7 @@ 6 | */ 7 | 8 | #include 9 | - 10 | -#ifndef _WIN32 11 | #include 12 | #include 13 | -#else 14 | -/* win64 is llp64 so these are the same for 32/64bit 15 | - so no check for _WIN64 is required. 16 | - */ 17 | -typedef unsigned char uint8_t; 18 | -typedef signed char int8_t; 19 | -typedef unsigned short uint16_t; 20 | -typedef signed short int16_t; 21 | -typedef unsigned int uint32_t; 22 | -typedef signed int int32_t; 23 | -typedef unsigned __int64 uint64_t; 24 | -typedef signed __int64 int64_t; 25 | - 26 | -/* windows doesn't do C99 and stdbool */ 27 | - 28 | -#ifndef __cplusplus 29 | -typedef unsigned char bool; 30 | -#define true 1 31 | -#define false 0 32 | -#endif 33 | 34 | -#endif /* _WIN32 */ 35 | #endif /* MODP_STDINT_H_ */ 36 | -------------------------------------------------------------------------------- /cpp/extlibs/stringencoders/system/FindStringEncoders.cmake: -------------------------------------------------------------------------------- 1 | find_path( 2 | STRINGENCODERS_INCLUDE_DIRS 3 | NAMES modp_b64.h 4 | DOC "stringencoders include directory" 5 | ) 6 | 7 | find_library( 8 | STRINGENCODERS_LIBRARIES 9 | NAMES modpbase64 10 | DOC "stringencoders libraries" 11 | ) 12 | 13 | include(FindPackageHandleStandardArgs) 14 | 15 | find_package_handle_standard_args( 16 | StringEncoders REQUIRED_VARS STRINGENCODERS_INCLUDE_DIRS STRINGENCODERS_LIBRARIES 17 | ) 18 | 19 | mark_as_advanced(STRINGENCODERS_INCLUDE_DIRS STRINGENCODERS_LIBRARIES) 20 | -------------------------------------------------------------------------------- /cpp/extlibs/zlib/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(ExternalProject) 2 | 3 | if(NOT OS_WINDOWS) 4 | message(SEND_ERROR "Building zlib requires Windows target, use system provided zlib with other OSes" ) 5 | endif() 6 | 7 | ExternalProject_Add( 8 | ext_zlib 9 | PREFIX 10 | ${EXTLIB_INSTALL_DIR} 11 | URL 12 | http://prdownloads.sourceforge.net/libpng/zlib-1.2.11.tar.gz 13 | URL_HASH 14 | SHA256=c3e5e9fdd5004dcb542feda5ee4f0ff0744628baf8ed2dd5d66f8ca1197cb1a1 15 | DOWNLOAD_DIR 16 | ${EXTLIB_CACHE_DIR}/zlib 17 | PATCH_COMMAND 18 | ${EXTLIB_PATCHER} zlib 19 | CMAKE_ARGS 20 | ${EXTLIB_CMAKE_ARGS} 21 | LOG_DOWNLOAD 1 LOG_UPDATE 0 LOG_CONFIGURE 1 LOG_BUILD 1 LOG_INSTALL 1 22 | ) 23 | 24 | if(CXX_MSVC) 25 | if(${CMAKE_SIZEOF_VOID_P} STREQUAL 4) 26 | set(LIB_MACHINE "x86") 27 | elseif(${CMAKE_SIZEOF_VOID_P} STREQUAL 8) 28 | set(LIB_MACHINE "x64") 29 | else() 30 | message(SEND_ERROR "Unknown pointer size, unable to provide /machine option for lib.exe" ) 31 | endif() 32 | 33 | ExternalProject_Add_Step( 34 | ext_zlib make_import_lib 35 | COMMAND 36 | ${CMAKE_CURRENT_SOURCE_DIR}/make-import-lib.cmd ${LIB_MACHINE} 37 | COMMENT 38 | "Creating import library for zlib1.dll" 39 | DEPENDEES 40 | install 41 | LOG 1 42 | ) 43 | endif() 44 | 45 | extlib_target(ext_zlib) 46 | -------------------------------------------------------------------------------- /cpp/extlibs/zlib/files/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.0) 2 | 3 | project(zlib C) 4 | 5 | set( 6 | SOURCE_FILES 7 | adler32.c 8 | compress.c 9 | crc32.c crc32.h 10 | deflate.c deflate.h 11 | gzclose.c gzguts.h 12 | gzlib.c 13 | gzread.c 14 | gzwrite.c 15 | infback.c 16 | inffast.c inffast.h 17 | inffixed.h 18 | inflate.c inflate.h 19 | inftrees.c inftrees.h 20 | trees.c trees.h 21 | uncompr.c 22 | zutil.c zutil.h 23 | ) 24 | 25 | set( 26 | HEADER_FILES 27 | zconf.h 28 | zlib.h 29 | ) 30 | 31 | add_definitions(-D_CRT_SECURE_NO_DEPRECATE -D_CRT_NONSTDC_NO_DEPRECATE) 32 | include_directories(${CMAKE_CURRENT_SOURCE_DIR}) 33 | 34 | add_library(zlib STATIC ${SOURCE_FILES} ${HEADER_FILES}) 35 | 36 | if(NOT MSVC) 37 | set_target_properties(zlib PROPERTIES OUTPUT_NAME z) 38 | endif() 39 | 40 | install(TARGETS zlib ARCHIVE DESTINATION lib) 41 | install(FILES ${HEADER_FILES} DESTINATION include) 42 | -------------------------------------------------------------------------------- /cpp/extlibs/zlib/local/FindZLIB.cmake: -------------------------------------------------------------------------------- 1 | set(ZLIB_INCLUDE_DIRS ${EXTLIB_INSTALL_DIR}/include) 2 | 3 | if(MSVC) 4 | set(ZLIB_LIBRARIES ${EXTLIB_INSTALL_DIR}/lib/zlib.lib) 5 | set(ZLIB_DLL_LIBRARIES ${EXTLIB_INSTALL_DIR}/lib/zlib_dll.lib) 6 | else() 7 | set(ZLIB_LIBRARIES ${EXTLIB_INSTALL_DIR}/lib/libz.a) 8 | endif() 9 | -------------------------------------------------------------------------------- /cpp/extlibs/zlib/make-import-lib.cmd: -------------------------------------------------------------------------------- 1 | @setlocal 2 | 3 | @if "%1" == "" goto :usage 4 | @if "%2" == "" goto :usage 5 | @if "%3" == "" goto :usage 6 | 7 | @set lib_dir=%3\lib 8 | 9 | if not exist "%lib_dir%\." mkdir "%lib_dir%" 10 | @if errorlevel 1 goto :end 11 | 12 | cd "%lib_dir%" 13 | @if errorlevel 1 goto :end 14 | 15 | copy /B /Y "%2\win32\zlib.def" zlib1.def 16 | @if errorlevel 1 goto :end 17 | 18 | lib.exe /nologo /machine:"%1" /def:zlib1.def /out:zlib_dll.lib 19 | @if errorlevel 1 goto :end 20 | 21 | del zlib1.def 22 | @if errorlevel 1 goto :end 23 | 24 | @goto :end 25 | 26 | :usage 27 | @echo Usage: %~nx0 machine zlib_source_dir install_dir 28 | @echo. 29 | @cmd /c exit 1 30 | 31 | :end 32 | -------------------------------------------------------------------------------- /cpp/server/artwork_controller.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "defines.hpp" 4 | #include "controller.hpp" 5 | #include "player_api.hpp" 6 | #include "cache_support_filter.hpp" 7 | 8 | namespace msrv { 9 | 10 | class Player; 11 | 12 | class Router; 13 | 14 | class ContentTypeMap; 15 | 16 | class ArtworkController : public ControllerBase 17 | { 18 | public: 19 | ArtworkController(Request* request, Player* player, const ContentTypeMap& contentTypes); 20 | ~ArtworkController(); 21 | 22 | ResponsePtr getCurrentArtwork(); 23 | ResponsePtr getArtwork(); 24 | 25 | static void defineRoutes(Router* router, WorkQueue* workQueue, Player* player, const ContentTypeMap& contentTypes); 26 | 27 | private: 28 | static ResponsePtr getNotFoundResponse(); 29 | 30 | ResponsePtr getResponse(ArtworkResult* result); 31 | 32 | Player* player_; 33 | const ContentTypeMap& contentTypes_; 34 | 35 | MSRV_NO_COPY_AND_ASSIGN(ArtworkController); 36 | }; 37 | 38 | } 39 | -------------------------------------------------------------------------------- /cpp/server/asio.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #ifdef MSRV_OS_WINDOWS 6 | #include "safe_windows.h" 7 | #endif 8 | 9 | namespace msrv { namespace asio = boost::asio; } 10 | -------------------------------------------------------------------------------- /cpp/server/asio_adapters.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "asio.hpp" 4 | #include "work_queue.hpp" 5 | #include "timers.hpp" 6 | 7 | namespace msrv { 8 | 9 | class AsioWorkQueue final : public ExternalWorkQueue 10 | { 11 | public: 12 | AsioWorkQueue(asio::io_context* context); 13 | virtual ~AsioWorkQueue(); 14 | 15 | protected: 16 | virtual void schedule(WorkCallback callback) override; 17 | 18 | private: 19 | asio::io_context* context_; 20 | }; 21 | 22 | class AsioTimer final : public Timer 23 | { 24 | public: 25 | AsioTimer(asio::io_context* context); 26 | virtual ~AsioTimer(); 27 | 28 | virtual TimerState state() const override 29 | { 30 | return state_; 31 | } 32 | 33 | virtual DurationMs period() const override 34 | { 35 | return period_; 36 | } 37 | 38 | virtual void setCallback(TimerCallback callback) override 39 | { 40 | callback_ = std::move(callback); 41 | } 42 | 43 | virtual void runOnce(DurationMs delay) override; 44 | virtual void runPeriodic(DurationMs period) override; 45 | virtual void stop() override; 46 | 47 | private: 48 | void schedule(DurationMs delay); 49 | void handleTimeout(const boost::system::error_code& error); 50 | 51 | asio::deadline_timer timer_; 52 | TimerState state_; 53 | DurationMs period_; 54 | TimerCallback callback_; 55 | }; 56 | 57 | class AsioTimerFactory final : public TimerFactory 58 | { 59 | public: 60 | AsioTimerFactory(asio::io_context* context); 61 | virtual ~AsioTimerFactory(); 62 | virtual TimerPtr createTimer() override; 63 | 64 | private: 65 | asio::io_context* context_; 66 | }; 67 | 68 | } 69 | -------------------------------------------------------------------------------- /cpp/server/base64.cpp: -------------------------------------------------------------------------------- 1 | #include "base64.hpp" 2 | 3 | #include 4 | 5 | namespace msrv { 6 | 7 | std::string base64Decode(const std::string& input) 8 | { 9 | std::string output; 10 | 11 | output.resize(modp_b64_decode_len(input.size())); 12 | 13 | auto outputSize = modp_b64_decode(&output[0], input.data(), input.size()); 14 | 15 | if (outputSize == static_cast(-1)) 16 | { 17 | output.resize(0); 18 | output.shrink_to_fit(); 19 | } 20 | else 21 | output.resize(outputSize); 22 | 23 | return output; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /cpp/server/base64.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "defines.hpp" 4 | #include 5 | 6 | namespace msrv { 7 | 8 | std::string base64Decode(const std::string& input); 9 | 10 | } 11 | -------------------------------------------------------------------------------- /cpp/server/basic_auth_filter.cpp: -------------------------------------------------------------------------------- 1 | #include "basic_auth_filter.hpp" 2 | #include "base64.hpp" 3 | #include "request.hpp" 4 | #include "response.hpp" 5 | #include "settings.hpp" 6 | #include "project_info.hpp" 7 | 8 | #include 9 | 10 | namespace msrv { 11 | 12 | namespace { 13 | 14 | const char BASIC_AUTH_PREFIX[] = "Basic "; 15 | 16 | const char WWW_AUTHENTICATE_VALUE[] = "Basic realm=\"" MSRV_PROJECT_NAME "\""; 17 | 18 | } 19 | 20 | BasicAuthFilter::BasicAuthFilter(SettingsDataPtr settings) 21 | : credentials_(settings->authUser + ":" + settings->authPassword) 22 | { 23 | assert(settings->authRequired); 24 | } 25 | 26 | BasicAuthFilter::~BasicAuthFilter() = default; 27 | 28 | void BasicAuthFilter::beginRequest(Request* request) 29 | { 30 | if (verifyCredentials(request)) 31 | return; 32 | 33 | setUnauthorizedResponse(request); 34 | } 35 | 36 | bool BasicAuthFilter::verifyCredentials(Request* request) 37 | { 38 | const auto& authValue = request->getHeader(HttpHeader::AUTHORIZATION); 39 | if (!boost::starts_with(authValue, BASIC_AUTH_PREFIX)) 40 | return false; 41 | 42 | const auto credentials = base64Decode(authValue.substr(sizeof(BASIC_AUTH_PREFIX) - 1)); 43 | return credentials == credentials_; 44 | } 45 | 46 | void BasicAuthFilter::setUnauthorizedResponse(Request* request) 47 | { 48 | request->response = Response::error(HttpStatus::S_401_UNAUTHORIZED, "Authentication required"); 49 | request->response->headers[HttpHeader::WWW_AUTHENTICATE] = WWW_AUTHENTICATE_VALUE; 50 | request->setProcessed(); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /cpp/server/basic_auth_filter.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "defines.hpp" 4 | #include "request_filter.hpp" 5 | #include "settings.hpp" 6 | 7 | namespace msrv { 8 | 9 | class BasicAuthFilter : public RequestFilter 10 | { 11 | public: 12 | explicit BasicAuthFilter(SettingsDataPtr settings); 13 | ~BasicAuthFilter() override; 14 | 15 | protected: 16 | virtual void beginRequest(Request* request) override; 17 | 18 | private: 19 | void setUnauthorizedResponse(Request* request); 20 | bool verifyCredentials(Request* request); 21 | 22 | std::string credentials_; 23 | }; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /cpp/server/beast.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "asio.hpp" 4 | 5 | #include 6 | 7 | namespace msrv { namespace beast = boost::beast; } 8 | -------------------------------------------------------------------------------- /cpp/server/beast_listener.cpp: -------------------------------------------------------------------------------- 1 | #include "beast_listener.hpp" 2 | #include "beast_connection.hpp" 3 | #include "log.hpp" 4 | 5 | namespace msrv { 6 | 7 | BeastListener::BeastListener( 8 | asio::io_context* ioContext, 9 | BeastConnectionContext* connectionContext, 10 | const asio::ip::tcp::endpoint& endpoint) 11 | : ioContext_(ioContext), 12 | connectionContext_(connectionContext), 13 | acceptor_(*ioContext), 14 | peerSocket_(*ioContext) 15 | { 16 | acceptor_.open(endpoint.protocol()); 17 | 18 | acceptor_.set_option(asio::socket_base::reuse_address(true)); 19 | 20 | if (endpoint.protocol() == asio::ip::tcp::v6()) 21 | acceptor_.set_option(asio::ip::v6_only(true)); 22 | 23 | acceptor_.bind(endpoint); 24 | acceptor_.listen(asio::socket_base::max_listen_connections); 25 | } 26 | 27 | BeastListener::~BeastListener() = default; 28 | 29 | void BeastListener::run() 30 | { 31 | accept(); 32 | } 33 | 34 | void BeastListener::accept() 35 | { 36 | auto thisPtr = shared_from_this(); 37 | 38 | acceptor_.async_accept( 39 | peerSocket_, 40 | [thisPtr](const boost::system::error_code& error) { 41 | thisPtr->handleAccept(error); 42 | }); 43 | } 44 | 45 | void BeastListener::handleAccept(const boost::system::error_code& error) 46 | { 47 | if (error) 48 | { 49 | logError("handleAccept: %s", error.message().c_str()); 50 | } 51 | else 52 | { 53 | auto connection = std::make_shared(connectionContext_, std::move(peerSocket_)); 54 | connection->run(); 55 | } 56 | 57 | if (!ioContext_->stopped()) 58 | accept(); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /cpp/server/beast_listener.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "beast.hpp" 4 | #include "server_core.hpp" 5 | 6 | namespace msrv { 7 | 8 | struct BeastConnectionContext; 9 | 10 | class BeastListener : public std::enable_shared_from_this 11 | { 12 | public: 13 | BeastListener( 14 | asio::io_context* ioContext, 15 | BeastConnectionContext* connectionContext, 16 | const asio::ip::tcp::endpoint& endpoint); 17 | 18 | ~BeastListener(); 19 | 20 | void run(); 21 | 22 | private: 23 | void accept(); 24 | void handleAccept(const boost::system::error_code& error); 25 | 26 | asio::io_context* ioContext_; 27 | BeastConnectionContext* connectionContext_; 28 | 29 | asio::ip::tcp::acceptor acceptor_; 30 | asio::ip::tcp::socket peerSocket_; 31 | asio::ip::tcp::endpoint peerEndpoint_; 32 | }; 33 | 34 | } 35 | -------------------------------------------------------------------------------- /cpp/server/beast_server.cpp: -------------------------------------------------------------------------------- 1 | #include "beast_server.hpp" 2 | #include "beast_listener.hpp" 3 | #include "log.hpp" 4 | 5 | namespace msrv { 6 | 7 | namespace { 8 | 9 | template 10 | asio::ip::tcp::endpoint makeEndpoint(int port, bool allowRemote) 11 | { 12 | return asio::ip::tcp::endpoint( 13 | allowRemote ? Address::any() : Address::loopback(), 14 | static_cast(port)); 15 | } 16 | 17 | } 18 | 19 | ServerCorePtr ServerCore::create() 20 | { 21 | return std::make_unique(); 22 | } 23 | 24 | BeastServer::BeastServer() 25 | : ioContext_(), 26 | connectionContext_(), 27 | workQueue_(&ioContext_), 28 | timerFactory_(&ioContext_) 29 | { 30 | } 31 | 32 | BeastServer::~BeastServer() = default; 33 | 34 | void BeastServer::setEventListener(RequestEventListener* listener) 35 | { 36 | connectionContext_.eventListener = listener; 37 | }; 38 | 39 | bool BeastServer::startListener(const asio::ip::tcp::endpoint& endpoint) 40 | { 41 | try 42 | { 43 | auto listener = std::make_shared( 44 | &ioContext_, &connectionContext_, endpoint); 45 | 46 | logInfo( 47 | "listening on [%s]:%d", 48 | endpoint.address().to_string().c_str(), 49 | static_cast(endpoint.port())); 50 | 51 | listener->run(); 52 | return true; 53 | } 54 | catch (std::exception& ex) 55 | { 56 | logError( 57 | "failed to bind to address [%s]:%d: %s", 58 | endpoint.address().to_string().c_str(), 59 | static_cast(endpoint.port()), 60 | ex.what()); 61 | 62 | return false; 63 | } 64 | } 65 | 66 | void BeastServer::bind(int port, bool allowRemote) 67 | { 68 | bool runningV4 = startListener(makeEndpoint(port, allowRemote)); 69 | bool runningV6 = startListener(makeEndpoint(port, allowRemote)); 70 | 71 | if (!runningV4 && !runningV6) 72 | throw std::runtime_error("failed to bind to any address"); 73 | } 74 | 75 | void BeastServer::run() 76 | { 77 | ioContext_.run(); 78 | } 79 | 80 | void BeastServer::exit() 81 | { 82 | ioContext_.stop(); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /cpp/server/beast_server.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "server_core.hpp" 4 | #include "beast.hpp" 5 | #include "asio_adapters.hpp" 6 | #include "beast_connection.hpp" 7 | #include "beast_listener.hpp" 8 | 9 | namespace msrv { 10 | 11 | class BeastServer : public ServerCore 12 | { 13 | public: 14 | BeastServer(); 15 | virtual ~BeastServer(); 16 | 17 | virtual WorkQueue* workQueue() override 18 | { 19 | return &workQueue_; 20 | } 21 | 22 | virtual TimerFactory* timerFactory() override 23 | { 24 | return &timerFactory_; 25 | } 26 | 27 | virtual void setEventListener(RequestEventListener* listener) override; 28 | 29 | virtual void bind(int port, bool allowRemote) override; 30 | virtual void run() override; 31 | virtual void exit() override; 32 | 33 | private: 34 | bool startListener(const asio::ip::tcp::endpoint& endpoint); 35 | 36 | asio::io_context ioContext_; 37 | BeastConnectionContext connectionContext_; 38 | AsioWorkQueue workQueue_; 39 | AsioTimerFactory timerFactory_; 40 | }; 41 | 42 | } 43 | -------------------------------------------------------------------------------- /cpp/server/browser_controller.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "defines.hpp" 4 | #include "controller.hpp" 5 | #include "settings.hpp" 6 | 7 | namespace msrv { 8 | 9 | class Router; 10 | 11 | class SettingsStore; 12 | 13 | class WorkQueue; 14 | 15 | class BrowserController : public ControllerBase 16 | { 17 | public: 18 | BrowserController(Request* request, SettingsDataPtr settings); 19 | ~BrowserController(); 20 | 21 | ResponsePtr getRoots(); 22 | ResponsePtr getEntries(); 23 | 24 | static void defineRoutes(Router* router, WorkQueue* workQueue, SettingsDataPtr settings); 25 | 26 | private: 27 | SettingsDataPtr settings_; 28 | }; 29 | 30 | } 31 | -------------------------------------------------------------------------------- /cpp/server/cache_support_filter.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "defines.hpp" 4 | #include "request_filter.hpp" 5 | #include "response.hpp" 6 | 7 | #include 8 | 9 | namespace msrv { 10 | 11 | class Response; 12 | 13 | class FileResponse; 14 | 15 | class CacheSupportFilter : public RequestFilter 16 | { 17 | public: 18 | CacheSupportFilter() = default; 19 | ~CacheSupportFilter() override = default; 20 | 21 | protected: 22 | void endRequest(Request* request) override; 23 | 24 | private: 25 | static std::string getEtag(Response* response); 26 | static uint64_t getHash(DataResponse* response); 27 | static uint64_t getHash(FileResponse* response); 28 | static std::string formatHash(uint64_t hash); 29 | static void setCacheHeaders(Response* response, const std::string& etag); 30 | }; 31 | 32 | } 33 | -------------------------------------------------------------------------------- /cpp/server/charset.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace msrv { 7 | 8 | #ifdef MSRV_OS_POSIX 9 | 10 | void setLocaleCharset(const char* charset = nullptr); 11 | std::string utf8ToLocale(const std::string& str); 12 | std::string localeToUtf8(const std::string& str); 13 | 14 | #endif 15 | 16 | #ifdef MSRV_OS_WINDOWS 17 | 18 | std::wstring utf8To16(const char* str, size_t size); 19 | 20 | inline std::wstring utf8To16(const char* str) { return utf8To16(str, ::strlen(str)); } 21 | inline std::wstring utf8To16(const std::string& str) { return utf8To16(str.data(), str.length()); } 22 | 23 | std::string utf16To8(const wchar_t* str, size_t size); 24 | 25 | inline std::string utf16To8(const wchar_t* str) { return utf16To8(str, ::wcslen(str)); } 26 | inline std::string utf16To8(const std::wstring& str) { return utf16To8(str.data(), str.length()); } 27 | 28 | #endif 29 | 30 | } 31 | -------------------------------------------------------------------------------- /cpp/server/charset_windows.cpp: -------------------------------------------------------------------------------- 1 | #include "charset.hpp" 2 | #include "system.hpp" 3 | #include "safe_windows.h" 4 | 5 | namespace msrv { 6 | 7 | namespace { 8 | 9 | size_t convert8To16(const char* inBuffer, size_t inSize, wchar_t* outBuffer, size_t outSize) 10 | { 11 | auto ret = ::MultiByteToWideChar( 12 | CP_UTF8, 13 | MB_ERR_INVALID_CHARS, 14 | inBuffer, 15 | static_cast(inSize), 16 | outBuffer, 17 | static_cast(outSize)); 18 | 19 | throwIfFailed("MultiByteToWideChar", ret != 0); 20 | return static_cast(ret); 21 | } 22 | 23 | size_t convert16To8(const wchar_t* inBuffer, size_t inSize, char* outBuffer, size_t outSize) 24 | { 25 | auto ret = ::WideCharToMultiByte( 26 | CP_UTF8, 27 | WC_ERR_INVALID_CHARS, 28 | inBuffer, 29 | static_cast(inSize), 30 | outBuffer, 31 | static_cast(outSize), 32 | nullptr, 33 | nullptr); 34 | 35 | throwIfFailed("WideCharToMultiByte", ret != 0); 36 | return static_cast(ret); 37 | } 38 | 39 | } 40 | 41 | std::wstring utf8To16(const char* str, size_t size) 42 | { 43 | std::wstring result; 44 | 45 | if (size == 0) 46 | return result; 47 | 48 | result.resize(convert8To16(str, size, nullptr, 0)); 49 | convert8To16(str, size, result.data(), result.length()); 50 | return result; 51 | } 52 | 53 | std::string utf16To8(const wchar_t* str, size_t size) 54 | { 55 | std::string result; 56 | 57 | if (size == 0) 58 | return result; 59 | 60 | result.resize(convert16To8(str, size, nullptr, 0)); 61 | convert16To8(str, size, result.data(), result.length()); 62 | return result; 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /cpp/server/chrono.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace msrv { 6 | 7 | using DurationMs = std::chrono::milliseconds; 8 | using TimePointMs = std::chrono::time_point; 9 | 10 | inline TimePointMs steadyTime() 11 | { 12 | return std::chrono::time_point_cast(std::chrono::steady_clock::now()); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /cpp/server/client_config_controller.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "controller.hpp" 4 | #include "settings.hpp" 5 | 6 | namespace msrv { 7 | 8 | class Player; 9 | class Router; 10 | 11 | class ClientConfigController : public ControllerBase 12 | { 13 | public: 14 | ClientConfigController(Request* request, SettingsDataPtr settings); 15 | ~ClientConfigController() = default; 16 | 17 | ResponsePtr getConfig(); 18 | void setConfig(); 19 | void removeConfig(); 20 | 21 | static void defineRoutes(Router* router, WorkQueue* workQueue, SettingsDataPtr settings); 22 | 23 | private: 24 | Path getFilePath(); 25 | 26 | void checkPermissions() 27 | { 28 | settings_->ensurePermissions(ApiPermissions::CHANGE_CLIENT_CONFIG); 29 | } 30 | 31 | SettingsDataPtr settings_; 32 | }; 33 | 34 | } 35 | -------------------------------------------------------------------------------- /cpp/server/compression_filter.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "defines.hpp" 4 | #include "request_filter.hpp" 5 | 6 | #include 7 | #include 8 | 9 | namespace msrv { 10 | 11 | class Request; 12 | 13 | class CompressionFilter : public RequestFilter 14 | { 15 | public: 16 | CompressionFilter(); 17 | virtual ~CompressionFilter(); 18 | 19 | protected: 20 | virtual void endRequest(Request* request) override; 21 | 22 | private: 23 | std::unordered_set contentTypes_; 24 | }; 25 | 26 | } 27 | -------------------------------------------------------------------------------- /cpp/server/content_type_map.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "defines.hpp" 4 | #include "unordered_map" 5 | #include "file_system.hpp" 6 | 7 | namespace msrv { 8 | 9 | class ContentTypeMap 10 | { 11 | public: 12 | ContentTypeMap(); 13 | ~ContentTypeMap(); 14 | 15 | const std::string& byFilePath(const Path& path) const; 16 | const std::string& byHeader(const std::vector& header) const; 17 | 18 | void add(const std::string& contentType, const std::string& ext); 19 | void add(const std::string& contentType, const std::string& ext, const std::string& ext2); 20 | 21 | private: 22 | std::unordered_map mapping_; 23 | std::string defaultType_; 24 | std::string jpegType_; 25 | std::string pngType_; 26 | std::string gifType_; 27 | std::string bmpType_; 28 | }; 29 | 30 | } 31 | -------------------------------------------------------------------------------- /cpp/server/core_types.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | namespace msrv { 12 | 13 | enum class Switch 14 | { 15 | // SW_ prefix to avoid clashes with frequently defined FALSE/TRUE constants, SW_TOGGLE for consistency 16 | SW_FALSE, 17 | SW_TRUE, 18 | SW_TOGGLE 19 | }; 20 | 21 | struct Range 22 | { 23 | Range() : offset(0), count(0) { } 24 | 25 | explicit Range(int32_t offsetVal, int32_t countVal = 1) 26 | : offset(offsetVal), count(countVal) 27 | { 28 | } 29 | 30 | int32_t offset; 31 | int32_t count; 32 | 33 | int32_t endOffset() const 34 | { 35 | return offset + count; 36 | } 37 | }; 38 | 39 | class InvalidRequestException : public std::runtime_error 40 | { 41 | public: 42 | InvalidRequestException() 43 | : std::runtime_error("invalid request") 44 | { 45 | } 46 | 47 | explicit InvalidRequestException(const std::string& str) 48 | : std::runtime_error(str) 49 | { 50 | } 51 | 52 | ~InvalidRequestException() = default; 53 | }; 54 | 55 | class OperationForbiddenException : public std::runtime_error 56 | { 57 | public: 58 | OperationForbiddenException() 59 | : std::runtime_error("operation is not allowed by current configuration") 60 | { 61 | } 62 | 63 | explicit OperationForbiddenException(const std::string& str) 64 | : std::runtime_error(str) 65 | { 66 | } 67 | 68 | ~OperationForbiddenException() = default; 69 | }; 70 | 71 | template 72 | struct MallocDeleter 73 | { 74 | void operator()(T* ptr) 75 | { 76 | ::free(ptr); 77 | } 78 | }; 79 | 80 | template 81 | using MallocPtr = std::unique_ptr>; 82 | 83 | } 84 | -------------------------------------------------------------------------------- /cpp/server/core_types_json.cpp: -------------------------------------------------------------------------------- 1 | #include "core_types_json.hpp" 2 | #include "core_types_parsers.hpp" 3 | 4 | #include 5 | 6 | namespace msrv { 7 | 8 | namespace { 9 | constexpr char INVALID_SWITCH_VALUE[] = "invalid switch value"; 10 | } 11 | 12 | void to_json(Json& json, const Switch& value) 13 | { 14 | switch (value) 15 | { 16 | case Switch::SW_FALSE: 17 | json = false; 18 | break; 19 | 20 | case Switch::SW_TRUE: 21 | json = true; 22 | break; 23 | 24 | case Switch::SW_TOGGLE: 25 | json = "toggle"; 26 | break; 27 | 28 | default: 29 | throw std::invalid_argument(INVALID_SWITCH_VALUE); 30 | } 31 | } 32 | 33 | void from_json(const Json& json, Switch& value) 34 | { 35 | if (json.is_boolean()) 36 | { 37 | value = json.get() ? Switch::SW_TRUE : Switch::SW_FALSE; 38 | return; 39 | } 40 | 41 | if (json.is_string()) 42 | { 43 | value = parseValue(json.get_ref()); 44 | return; 45 | } 46 | 47 | throw std::invalid_argument(INVALID_SWITCH_VALUE); 48 | } 49 | 50 | void to_json(Json& json, const Range& range) 51 | { 52 | json = formatString("%" PRId32 ":%" PRId32, range.offset, range.count); 53 | } 54 | 55 | void from_json(const Json& json, Range& value) 56 | { 57 | if (json.is_number()) 58 | { 59 | value = Range(json.get()); 60 | return; 61 | } 62 | 63 | if (json.is_string()) 64 | { 65 | value = parseValue(json.get_ref()); 66 | return; 67 | } 68 | 69 | throw std::invalid_argument("invalid range value"); 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /cpp/server/core_types_json.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "core_types.hpp" 4 | #include "json.hpp" 5 | 6 | namespace msrv { 7 | 8 | void to_json(Json& json, const Switch& value); 9 | void from_json(const Json& json, Switch& value); 10 | 11 | void to_json(Json& json, const Range& value); 12 | void from_json(const Json& json, Range& value); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /cpp/server/core_types_parsers.cpp: -------------------------------------------------------------------------------- 1 | #include "core_types_parsers.hpp" 2 | 3 | namespace msrv { 4 | 5 | bool ValueParser::tryParse(StringView str, Range* outVal) 6 | { 7 | int32_t offset; 8 | int32_t count; 9 | 10 | auto pos = str.find(':'); 11 | 12 | if (pos == StringView::npos) 13 | { 14 | if (!tryParseValue(str, &offset)) 15 | return false; 16 | 17 | if (offset < 0) 18 | return false; 19 | 20 | *outVal = Range(offset, 1); 21 | return true; 22 | } 23 | 24 | if (!tryParseValue(str.substr(0, pos), &offset)) 25 | return false; 26 | 27 | if (!tryParseValue(str.substr(pos + 1), &count)) 28 | return false; 29 | 30 | if (offset < 0 || count < 0) 31 | return false; 32 | 33 | *outVal = Range(offset, count); 34 | return true; 35 | } 36 | 37 | bool ValueParser::tryParse(StringView str, Switch* outVal) 38 | { 39 | switch (str.length()) 40 | { 41 | case 4: 42 | if (::memcmp(str.data(), "true", 4) == 0) 43 | { 44 | *outVal = Switch::SW_TRUE; 45 | return true; 46 | } 47 | 48 | return false; 49 | 50 | case 5: 51 | if (::memcmp(str.data(), "false", 5) == 0) 52 | { 53 | *outVal = Switch::SW_FALSE; 54 | return true; 55 | } 56 | 57 | return false; 58 | 59 | case 6: 60 | if (::memcmp(str.data(), "toggle", 6) == 0) 61 | { 62 | *outVal = Switch::SW_TOGGLE; 63 | return true; 64 | } 65 | 66 | return false; 67 | 68 | default: 69 | return false; 70 | } 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /cpp/server/core_types_parsers.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "core_types.hpp" 4 | #include "parsing.hpp" 5 | 6 | namespace msrv { 7 | 8 | template<> 9 | struct ValueParser 10 | { 11 | static bool tryParse(StringView str, Range* outVal); 12 | }; 13 | 14 | template<> 15 | struct ValueParser 16 | { 17 | static bool tryParse(StringView str, Switch* outVal); 18 | }; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /cpp/server/deadbeef/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | find_package(Deadbeef REQUIRED) 2 | 3 | include_directories( 4 | ${DEADBEEF_INCLUDE_DIRS} 5 | ) 6 | 7 | set( 8 | DEADBEEF_PLUGIN_SOURCES 9 | add_items_scope.cpp add_items_scope.hpp 10 | artwork_fetcher_v1.cpp artwork_fetcher_v2.cpp artwork_fetcher.hpp 11 | common.cpp common.hpp 12 | player.hpp 13 | player_control.cpp 14 | player_misc.cpp 15 | player_options.cpp player_options.hpp 16 | player_playlists.cpp 17 | playlist_mapping.cpp playlist_mapping.hpp 18 | plugin.cpp plugin.hpp 19 | utils.cpp utils.hpp 20 | ) 21 | 22 | add_library( 23 | deadbeef_plugin MODULE 24 | ${DEADBEEF_PLUGIN_SOURCES} 25 | $ 26 | ) 27 | 28 | target_link_libraries( 29 | deadbeef_plugin 30 | "${CORE_LIBRARIES}" 31 | ) 32 | 33 | configure_file(linker_script.in linker_script) 34 | 35 | set_target_properties( 36 | deadbeef_plugin PROPERTIES 37 | PREFIX "" 38 | OUTPUT_NAME "${DEADBEEF_PLUGIN_FILE}" 39 | LINK_FLAGS "-Wl,-version-script,${CMAKE_CURRENT_BINARY_DIR}/linker_script" 40 | ) 41 | 42 | add_dependencies(deadbeef_plugin ext_all) 43 | 44 | install( 45 | TARGETS deadbeef_plugin 46 | LIBRARY DESTINATION ${DEADBEEF_LIB_DIR} 47 | COMPONENT deadbeef_plugin 48 | ) 49 | 50 | if(ENABLE_TESTS) 51 | add_library( 52 | deadbeef_plugin_dummy_gui MODULE 53 | dummy_gui.c 54 | ) 55 | 56 | set_target_properties( 57 | deadbeef_plugin_dummy_gui PROPERTIES 58 | PREFIX "" 59 | OUTPUT_NAME "ddb_gui_dummy" 60 | ) 61 | 62 | add_dependencies(deadbeef_plugin_dummy_gui ext_all) 63 | 64 | add_library( 65 | deadbeef_plugin_nullout2 MODULE 66 | nullout2.c 67 | ) 68 | 69 | set_target_properties( 70 | deadbeef_plugin_nullout2 PROPERTIES 71 | PREFIX "" 72 | OUTPUT_NAME "nullout2" 73 | ) 74 | 75 | add_dependencies(deadbeef_plugin_nullout2 ext_all) 76 | endif() 77 | -------------------------------------------------------------------------------- /cpp/server/deadbeef/add_items_scope.cpp: -------------------------------------------------------------------------------- 1 | #include "add_items_scope.hpp" 2 | 3 | #include "../file_system.hpp" 4 | #include "../core_types.hpp" 5 | 6 | namespace msrv { 7 | namespace player_deadbeef { 8 | 9 | AddItemsScope::AddItemsScope(ddb_playlist_t* playlist, int visibility) 10 | : playlist_(playlist), visibility_(visibility), created_(false), aborted_(0) 11 | { 12 | assert(playlist); 13 | 14 | created_ = ddbApi->plt_add_files_begin(playlist, visibility) == 0; 15 | 16 | if (!created_) 17 | throw InvalidRequestException("player is already adding files"); 18 | } 19 | 20 | AddItemsScope::~AddItemsScope() 21 | { 22 | if (created_) 23 | ddbApi->plt_add_files_end(playlist_, visibility_); 24 | } 25 | 26 | bool AddItemsScope::add(const std::string& path) 27 | { 28 | Path nativePath = pathFromUtf8(path); 29 | 30 | return handleAdd(addDir(nativePath.c_str())) 31 | || handleAdd(addFile(nativePath.c_str())) 32 | || handleAdd(addPlaylist(nativePath.c_str())); 33 | } 34 | 35 | bool AddItemsScope::handleAdd(ddb_playItem_t* item) 36 | { 37 | if (aborted_) 38 | throw InvalidRequestException("add operation aborted"); 39 | 40 | if (!item) 41 | return false; 42 | 43 | ddbApi->pl_item_ref(item); 44 | lastItem_.reset(item); 45 | return true; 46 | } 47 | 48 | ddb_playItem_t* AddItemsScope::addDir(const char* path) 49 | { 50 | return ddbApi->plt_insert_dir2( 51 | visibility_, playlist_, lastItem_.get(), path, &aborted_, nullptr, nullptr); 52 | } 53 | 54 | ddb_playItem_t* AddItemsScope::addFile(const char* path) 55 | { 56 | return ddbApi->plt_insert_file2( 57 | visibility_, playlist_, lastItem_.get(), path, &aborted_, nullptr, nullptr); 58 | } 59 | 60 | ddb_playItem_t* AddItemsScope::addPlaylist(const char* path) 61 | { 62 | return ddbApi->plt_load2( 63 | visibility_, playlist_, lastItem_.get(), path, &aborted_, nullptr, nullptr); 64 | } 65 | 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /cpp/server/deadbeef/add_items_scope.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../defines.hpp" 4 | #include "utils.hpp" 5 | 6 | namespace msrv { 7 | namespace player_deadbeef { 8 | 9 | class AddItemsScope 10 | { 11 | public: 12 | AddItemsScope(ddb_playlist_t* playlist, int visibility); 13 | ~AddItemsScope(); 14 | 15 | void setLastItem(PlaylistItemPtr item) 16 | { 17 | lastItem_ = std::move(item); 18 | } 19 | 20 | bool add(const std::string& path); 21 | 22 | private: 23 | bool handleAdd(ddb_playItem_t* item); 24 | ddb_playItem_t* addDir(const char* path); 25 | ddb_playItem_t* addFile(const char* path); 26 | ddb_playItem_t* addPlaylist(const char* path); 27 | 28 | ddb_playlist_t* playlist_; 29 | int visibility_; 30 | bool created_; 31 | int aborted_; 32 | PlaylistItemPtr lastItem_; 33 | 34 | MSRV_NO_COPY_AND_ASSIGN(AddItemsScope); 35 | }; 36 | 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /cpp/server/deadbeef/artwork_fetcher.hpp: -------------------------------------------------------------------------------- 1 | #include "../player_api.hpp" 2 | 3 | #include "common.hpp" 4 | #include "utils.hpp" 5 | 6 | namespace msrv { 7 | namespace player_deadbeef { 8 | 9 | class ArtworkFetcher 10 | { 11 | public: 12 | virtual ~ArtworkFetcher() = default; 13 | 14 | virtual boost::unique_future fetchArtwork(PlaylistPtr playlist, PlaylistItemPtr item) = 0; 15 | 16 | static std::unique_ptr createV1(); 17 | static std::unique_ptr createV2(); 18 | }; 19 | 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /cpp/server/deadbeef/common.cpp: -------------------------------------------------------------------------------- 1 | #include "common.hpp" 2 | 3 | namespace msrv { 4 | namespace player_deadbeef { 5 | 6 | DB_functions_t* ddbApi; 7 | 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /cpp/server/deadbeef/common.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define DDB_API_LEVEL 10 4 | #define DDB_WARN_DEPRECATED 1 5 | 6 | #include 7 | 8 | #if (DB_API_VERSION_MAJOR != 1) || (DB_API_VERSION_MINOR < DDB_API_LEVEL) 9 | #error DB_API_VERSION should be at least 1.10 10 | #endif 11 | 12 | namespace msrv { 13 | namespace player_deadbeef { 14 | 15 | extern DB_functions_t* ddbApi; 16 | 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /cpp/server/deadbeef/dummy_gui.c: -------------------------------------------------------------------------------- 1 | #define DDB_API_LEVEL 8 2 | #define DDB_WARN_DEPRECATED 1 3 | 4 | #include 5 | 6 | static int dummy_callback() 7 | { 8 | return 0; 9 | } 10 | 11 | static DB_gui_t plugin_def = 12 | { 13 | .plugin = 14 | { 15 | .api_vmajor = 1, 16 | .api_vminor = DDB_API_LEVEL, 17 | .version_major = 1, 18 | .version_minor = 0, 19 | .type = DB_PLUGIN_GUI, 20 | .id = "dummy_gui", 21 | .name = "Dummy user interface", 22 | .descr = "Dummy user interface for headless environments", 23 | .copyright = "Public domain", 24 | .start = dummy_callback, 25 | .stop = dummy_callback, 26 | }, 27 | }; 28 | 29 | DB_plugin_t* ddb_gui_dummy_load(DB_functions_t* ddb) 30 | { 31 | return DB_PLUGIN(&plugin_def); 32 | } 33 | -------------------------------------------------------------------------------- /cpp/server/deadbeef/linker_script.in: -------------------------------------------------------------------------------- 1 | { 2 | global: @DEADBEEF_ENTRY_POINT@; 3 | local: *; 4 | }; 5 | -------------------------------------------------------------------------------- /cpp/server/deadbeef/player_options.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../player_api.hpp" 4 | #include "utils.hpp" 5 | 6 | namespace msrv::player_deadbeef { 7 | 8 | class LegacyPlaybackModeOption : public EnumPlayerOption 9 | { 10 | public: 11 | LegacyPlaybackModeOption(); 12 | 13 | int32_t getValue() const override; 14 | void setValue(int32_t value) override; 15 | 16 | private: 17 | mutable ConfigMutex configMutex_; 18 | 19 | void setModes(int order, int loop); 20 | }; 21 | 22 | class ShuffleOption : public EnumPlayerOption 23 | { 24 | public: 25 | ShuffleOption(); 26 | 27 | int32_t getValue() const override; 28 | void setValue(int32_t value) override; 29 | }; 30 | 31 | class RepeatOption : public EnumPlayerOption 32 | { 33 | public: 34 | RepeatOption(); 35 | 36 | int32_t getValue() const override; 37 | void setValue(int32_t value) override; 38 | }; 39 | 40 | class StopAfterCurrentTrackOption : public BoolPlayerOption 41 | { 42 | public: 43 | StopAfterCurrentTrackOption(); 44 | 45 | bool getValue() const override; 46 | void setValue(bool value) override; 47 | }; 48 | 49 | class StopAfterCurrentAlbumOption : public BoolPlayerOption 50 | { 51 | public: 52 | StopAfterCurrentAlbumOption(); 53 | 54 | bool getValue() const override; 55 | void setValue(bool value) override; 56 | }; 57 | 58 | } 59 | -------------------------------------------------------------------------------- /cpp/server/deadbeef/playlist_mapping.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../player_api.hpp" 4 | 5 | #include "utils.hpp" 6 | 7 | namespace msrv { 8 | namespace player_deadbeef { 9 | 10 | class PlaylistMapping 11 | { 12 | public: 13 | PlaylistMapping() 14 | : maxId_(0) 15 | { 16 | } 17 | 18 | ~PlaylistMapping() 19 | { 20 | } 21 | 22 | const char* getId(ddb_playlist_t* playlist); 23 | PlaylistPtr resolve(const PlaylistRef& plref); 24 | int resolveIndex(const PlaylistRef& plref); 25 | 26 | void ensureInitialized() 27 | { 28 | if (maxId_ == 0) 29 | initialize(); 30 | } 31 | 32 | private: 33 | void initialize(); 34 | 35 | int64_t maxId_; 36 | 37 | MSRV_NO_COPY_AND_ASSIGN(PlaylistMapping); 38 | }; 39 | 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /cpp/server/deadbeef/plugin.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../defines.hpp" 4 | #include "../settings.hpp" 5 | #include "../server_host.hpp" 6 | #include "../file_system.hpp" 7 | #include "project_info.hpp" 8 | 9 | #include "player.hpp" 10 | 11 | namespace msrv { 12 | namespace player_deadbeef { 13 | 14 | class Plugin 15 | { 16 | public: 17 | Plugin(); 18 | ~Plugin(); 19 | 20 | void connect() 21 | { 22 | player_.connect(); 23 | } 24 | 25 | void disconnect() 26 | { 27 | player_.disconnect(); 28 | } 29 | 30 | void handleMessage(uint32_t id, uintptr_t ctx, uint32_t p1, uint32_t p2); 31 | 32 | private: 33 | static ApiPermissions getPermissionsFromConfig(); 34 | 35 | static Path getProfileDir() 36 | { 37 | return {ddbApi->get_system_dir(DDB_SYS_DIR_CONFIG)}; 38 | } 39 | 40 | void handleConfigChanged(); 41 | void handlePluginsLoaded(); 42 | bool refreshSettings(); 43 | void reconfigure(); 44 | 45 | PlayerImpl player_; 46 | ServerHost host_; 47 | bool pluginsLoaded_ = false; 48 | 49 | int port_ = MSRV_DEFAULT_PORT; 50 | bool allowRemote_ = true; 51 | std::string musicDirs_; 52 | bool authRequired_ = false; 53 | std::string authUser_; 54 | std::string authPassword_; 55 | ApiPermissions permissions_ = ApiPermissions::ALL; 56 | 57 | MSRV_NO_COPY_AND_ASSIGN(Plugin); 58 | }; 59 | 60 | class PluginWrapper 61 | { 62 | public: 63 | static DB_plugin_t* load(DB_functions_t* api); 64 | 65 | private: 66 | static void initDef(); 67 | static int start(); 68 | static int stop(); 69 | static int connect(); 70 | static int disconnect(); 71 | static int handleMessage(uint32_t id, uintptr_t ctx, uint32_t p1, uint32_t p2); 72 | 73 | static DB_misc_t definition_; 74 | static DeadbeefLogger* logger_; 75 | static Plugin* instance_; 76 | static const char configDialog_[]; 77 | static char licenseText_[]; 78 | 79 | MSRV_NO_COPY_AND_ASSIGN(PluginWrapper); 80 | }; 81 | 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /cpp/server/deadbeef/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.hpp" 2 | 3 | #include "../core_types.hpp" 4 | #include "../string_utils.hpp" 5 | 6 | #include 7 | 8 | namespace msrv { 9 | namespace player_deadbeef { 10 | 11 | std::vector compileColumns( 12 | const std::vector& columns, bool throwOnError) 13 | { 14 | std::vector formatters; 15 | 16 | for (auto& column : columns) 17 | { 18 | TitleFormatPtr formatter(ddbApi->tf_compile(column.c_str())); 19 | 20 | if (!formatter) 21 | { 22 | if (throwOnError) 23 | throw InvalidRequestException("invalid format expression: " + column); 24 | 25 | return std::vector(); 26 | } 27 | 28 | formatters.emplace_back(std::move(formatter)); 29 | } 30 | 31 | return formatters; 32 | } 33 | 34 | std::vector evaluateColumns( 35 | ddb_playlist_t* playlist, 36 | ddb_playItem_t* item, 37 | const std::vector& formatters) 38 | { 39 | std::vector results; 40 | results.reserve(formatters.size()); 41 | 42 | ddb_tf_context_t context{}; 43 | context._size = sizeof(context); 44 | context.plt = playlist; 45 | context.it = item; 46 | 47 | int index = 0; 48 | char buffer[TITLE_FORMAT_BUFFER_SIZE]; 49 | 50 | for (auto& formatter : formatters) 51 | { 52 | int size = ddbApi->tf_eval(&context, formatter.get(), buffer, sizeof(buffer)); 53 | if (size < 0) 54 | throw std::runtime_error("Failed to evaluate expression at index " + toString(index)); 55 | 56 | results.emplace_back(buffer, size); 57 | index++; 58 | } 59 | 60 | return results; 61 | } 62 | 63 | void DeadbeefLogger::log(LogLevel, const char* fmt, va_list va) 64 | { 65 | std::string format = prefix_ + fmt + "\n"; 66 | 67 | ddbApi->vlog_detailed(plugin_, DDB_LOG_LAYER_INFO, format.c_str(), va); 68 | } 69 | 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /cpp/server/defines.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #if defined(__GNUC__) || defined(__clang__) 4 | #define MSRV_FORMAT_FUNC(fmt, args) \ 5 | __attribute__((format(printf, fmt, args))) 6 | #else 7 | #define MSRV_FORMAT_FUNC(fmt, args) 8 | #endif 9 | 10 | #define MSRV_STRINGIFY_(v) #v 11 | #define MSRV_STRINGIFY(v) MSRV_STRINGIFY_(v) 12 | 13 | #define MSRV_NO_COPY_AND_ASSIGN(type) \ 14 | type(const type&) = delete; \ 15 | type& operator=(const type&) = delete 16 | 17 | #define MSRV_ENUM_FLAGS(Type, Base) \ 18 | inline bool hasFlags(Type target, Type flags) \ 19 | { return (static_cast(target) & static_cast(flags)) == static_cast(flags); } \ 20 | inline Type setFlags(Type current, Type flags, bool set) \ 21 | { \ 22 | return static_cast(set \ 23 | ? (static_cast(current) | static_cast(flags)) \ 24 | : (static_cast(current) & ~static_cast(flags))); \ 25 | } \ 26 | inline Type operator|(Type first, Type second) \ 27 | { return static_cast(static_cast(first) | static_cast(second)); } \ 28 | \ 29 | inline Type operator|=(Type& first, Type second) \ 30 | { return (first = static_cast(static_cast(first) | static_cast(second))); } \ 31 | \ 32 | inline Type operator&(Type first, Type second) \ 33 | { return static_cast(static_cast(first) & static_cast(second)); } \ 34 | \ 35 | inline Type operator&=(Type& first, Type second) \ 36 | { return (first = static_cast(static_cast(first) & static_cast(second))); } 37 | -------------------------------------------------------------------------------- /cpp/server/env_info.hpp.in: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #cmakedefine HAVE_PTHREAD_SETNAME_NP 4 | #cmakedefine HAVE_PTHREAD_SET_NAME_NP 5 | -------------------------------------------------------------------------------- /cpp/server/file_system.cpp: -------------------------------------------------------------------------------- 1 | #include "file_system.hpp" 2 | 3 | #include 4 | 5 | namespace msrv { 6 | 7 | bool isSubpath(const Path& parentPath, const Path& childPath) 8 | { 9 | if (parentPath.empty() || childPath.empty() || !parentPath.is_absolute() || !childPath.is_absolute()) 10 | return false; 11 | 12 | const auto& parent = parentPath.native(); 13 | const auto& child = childPath.native(); 14 | 15 | if (!boost::starts_with(child, parent)) 16 | return false; 17 | 18 | // C:\foo and C:\foo 19 | if (child.length() == parent.length()) 20 | return true; 21 | 22 | // C:\foo and C:\foo\bar 23 | if (child[parent.length()] == Path::preferred_separator) 24 | return true; 25 | 26 | // C:\ and C:\foo 27 | return parent.back() == Path::preferred_separator 28 | && child[parent.length() - 1] == Path::preferred_separator; 29 | } 30 | 31 | namespace file_io { 32 | 33 | std::vector readToEnd(FileHandle::Type handle, int64_t maxBytes) 34 | { 35 | size_t bytesToRead = static_cast(queryInfo(handle).size); 36 | 37 | if (maxBytes >= 0) 38 | bytesToRead = std::min(bytesToRead, static_cast(maxBytes)); 39 | 40 | std::vector buffer; 41 | 42 | if (bytesToRead == 0) 43 | return buffer; 44 | 45 | buffer.resize(bytesToRead); 46 | 47 | auto bytesRead = read(handle, buffer.data(), bytesToRead); 48 | buffer.resize(bytesRead); 49 | return buffer; 50 | 51 | } 52 | 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /cpp/server/file_system.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "system.hpp" 4 | #include "charset.hpp" 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | namespace msrv { 14 | 15 | namespace fs = boost::filesystem; 16 | 17 | using Path = fs::path; 18 | 19 | #ifdef MSRV_OS_POSIX 20 | #define MSRV_PATH_LITERAL(str) Path(str) 21 | #endif 22 | 23 | #ifdef MSRV_OS_WINDOWS 24 | #define MSRV_PATH_LITERAL_(str) Path(L##str) 25 | #define MSRV_PATH_LITERAL(str) MSRV_PATH_LITERAL_(str) 26 | #endif 27 | 28 | enum class FileType 29 | { 30 | UNKNOWN, 31 | REGULAR, 32 | DIRECTORY, 33 | }; 34 | 35 | struct FileInfo 36 | { 37 | FileType type; 38 | int64_t size; 39 | int64_t timestamp; 40 | int64_t inode; 41 | }; 42 | 43 | inline std::string pathToUtf8(const Path& path) 44 | { 45 | #ifdef MSRV_OS_POSIX 46 | return localeToUtf8(path.native()); 47 | #endif 48 | 49 | #ifdef MSRV_OS_WINDOWS 50 | return utf16To8(path.native()); 51 | #endif 52 | } 53 | 54 | inline Path pathFromUtf8(const std::string& path) 55 | { 56 | #ifdef MSRV_OS_POSIX 57 | return Path(utf8ToLocale(path)); 58 | #endif 59 | 60 | #ifdef MSRV_OS_WINDOWS 61 | return Path(utf8To16(path)); 62 | #endif 63 | } 64 | 65 | bool isSubpath(const Path& parentPath, const Path& childPath); 66 | 67 | Path getModulePath(void* symbol); 68 | Path getUserConfigDir(); 69 | Path getEnvAsPath(const char* env); 70 | 71 | namespace file_io { 72 | 73 | FileInfo queryInfo(FileHandle::Type handle); 74 | FileInfo queryInfo(const Path& path); 75 | boost::optional tryQueryInfo(const Path& path); 76 | 77 | FileHandle open(const Path& path); 78 | void setPosition(FileHandle::Type handle, int64_t position); 79 | size_t read(FileHandle::Type handle, void* buffer, size_t bytes); 80 | std::vector readToEnd(FileHandle::Type handle, int64_t maxBytes = -1); 81 | 82 | void write(const Path& path, const void* buffer, size_t bytes); 83 | 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /cpp/server/fnv_hash.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | namespace msrv { 8 | 9 | // 64-bit Fowler/Noll/Vo FNV-1a hash 10 | // Based on public domain code from: 11 | // http://www.isthe.com/chongo/tech/comp/fnv/ 12 | 13 | class FnvHash 14 | { 15 | public: 16 | FnvHash() 17 | : value_(INIT) 18 | { 19 | } 20 | 21 | void addBytes(const void* buffer, size_t size) 22 | { 23 | const uint8_t* pos = reinterpret_cast(buffer); 24 | const uint8_t* end = pos + size; 25 | uint64_t value = value_; 26 | 27 | while (pos < end) 28 | { 29 | // xor the bottom with the current octet 30 | value ^= *pos++; 31 | 32 | // multiply by the 64 bit FNV magic prime mod 2^64 33 | value *= PRIME; 34 | } 35 | 36 | value_ = value; 37 | } 38 | 39 | template 40 | void addValue(T value) 41 | { 42 | static_assert(std::is_fundamental(), "fundamental type required"); 43 | addBytes(&value, sizeof(T)); 44 | } 45 | 46 | uint64_t value() const 47 | { 48 | return value_; 49 | } 50 | 51 | private: 52 | const uint64_t INIT = UINT64_C(0xcbf29ce484222325); 53 | const uint64_t PRIME = UINT64_C(0x100000001b3); 54 | 55 | uint64_t value_; 56 | }; 57 | 58 | } 59 | -------------------------------------------------------------------------------- /cpp/server/foobar2000/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | find_package(foosdk REQUIRED) 2 | 3 | include_directories( 4 | ${FOOSDK_INCLUDE_DIRS} 5 | ) 6 | 7 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /d2notypeopt") 8 | 9 | set( 10 | FOOBAR2000_PLUGIN_SOURCES 11 | common.hpp 12 | main_prefs_page.cpp main_prefs_page.hpp 13 | permissions_prefs_page.cpp permissions_prefs_page.hpp 14 | player.hpp 15 | player_control.cpp 16 | player_misc.cpp 17 | player_options.cpp player_options.hpp 18 | player_playlists.cpp 19 | playlist_mapping.cpp playlist_mapping.hpp 20 | plugin.cpp plugin.hpp 21 | prefs_page.cpp prefs_page.hpp 22 | resource.h resource.rc 23 | settings.cpp settings.hpp 24 | utils.cpp utils.hpp 25 | ) 26 | 27 | set( 28 | CORE_LIBRARIES_FOOBAR2000 29 | ${CORE_LIBRARIES} 30 | ) 31 | 32 | if(ZLIB_DLL_LIBRARIES) 33 | list(REMOVE_ITEM CORE_LIBRARIES_FOOBAR2000 "${ZLIB_LIBRARIES}") 34 | list(APPEND CORE_LIBRARIES_FOOBAR2000 "${ZLIB_DLL_LIBRARIES}") 35 | endif() 36 | 37 | add_library( 38 | foobar2000_plugin MODULE 39 | ${FOOBAR2000_PLUGIN_SOURCES} 40 | $ 41 | ) 42 | 43 | target_link_libraries( 44 | foobar2000_plugin 45 | "${CORE_LIBRARIES_FOOBAR2000};${FOOSDK_LIBRARIES};shlwapi.lib" 46 | ) 47 | 48 | set_target_properties( 49 | foobar2000_plugin PROPERTIES 50 | OUTPUT_NAME "${FOOBAR2000_PLUGIN_FILE}" 51 | ) 52 | 53 | install( 54 | TARGETS foobar2000_plugin 55 | RUNTIME DESTINATION . 56 | LIBRARY DESTINATION . 57 | COMPONENT foobar2000_plugin 58 | ) 59 | -------------------------------------------------------------------------------- /cpp/server/foobar2000/common.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "../safe_windows.h" 5 | -------------------------------------------------------------------------------- /cpp/server/foobar2000/permissions_prefs_page.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "common.hpp" 4 | #include "prefs_page.hpp" 5 | #include "utils.hpp" 6 | 7 | namespace msrv::player_foobar2000 { 8 | 9 | class PermissionsPrefsPageInstance : public PrefsPageInstance 10 | { 11 | public: 12 | PermissionsPrefsPageInstance(HWND parent, preferences_page_callback::ptr callback); 13 | ~PermissionsPrefsPageInstance() = default; 14 | 15 | void apply() override; 16 | void reset() override; 17 | 18 | protected: 19 | INT_PTR handleCommand(int control, int message) override; 20 | bool hasChanges() override; 21 | 22 | private: 23 | void initialize(); 24 | }; 25 | 26 | class PermissionsPrefsPage : public preferences_page_v3 27 | { 28 | public: 29 | PermissionsPrefsPage() = default; 30 | ~PermissionsPrefsPage() = default; 31 | 32 | const char* get_name() override 33 | { 34 | return "Permissions"; 35 | } 36 | 37 | GUID get_guid() override 38 | { 39 | return prefs_pages::permissions; 40 | } 41 | 42 | GUID get_parent_guid() override 43 | { 44 | return prefs_pages::main; 45 | } 46 | 47 | bool get_help_url(pfc::string_base& p_out) override 48 | { 49 | return false; 50 | } 51 | 52 | double get_sort_priority() override 53 | { 54 | return 0; 55 | } 56 | 57 | preferences_page_instance::ptr instantiate(HWND parent, preferences_page_callback::ptr callback) override 58 | { 59 | return preferences_page_instance::ptr(new service_impl_t(parent, callback)); 60 | } 61 | }; 62 | 63 | } 64 | -------------------------------------------------------------------------------- /cpp/server/foobar2000/player_options.cpp: -------------------------------------------------------------------------------- 1 | #include "player_options.hpp" 2 | 3 | namespace msrv::player_foobar2000 { 4 | 5 | PlaybackOrderOption::PlaybackOrderOption(playlist_manager_v4* playlistManager) 6 | : EnumPlayerOption("playbackOrder", "Playback order", getEnumNames(playlistManager)), 7 | playlistManager_(playlistManager) 8 | { 9 | } 10 | 11 | int32_t PlaybackOrderOption::getValue() const 12 | { 13 | return static_cast(playlistManager_->playback_order_get_active()); 14 | } 15 | 16 | void PlaybackOrderOption::setValue(int32_t value) 17 | { 18 | playlistManager_->playback_order_set_active(value); 19 | } 20 | 21 | std::vector PlaybackOrderOption::getEnumNames(playlist_manager_v4* playlistManager) 22 | { 23 | std::vector names; 24 | 25 | const auto count = playlistManager->playback_order_get_count(); 26 | 27 | names.reserve(count); 28 | 29 | for (t_size i = 0; i < count; i++) 30 | names.emplace_back(playlistManager->playback_order_get_name(i)); 31 | 32 | return names; 33 | } 34 | 35 | StopAfterCurrentTrackOption::StopAfterCurrentTrackOption(playback_control* playbackControl) 36 | : BoolPlayerOption("stopAfterCurrentTrack", "Stop after current track"), playbackControl_(playbackControl) 37 | { 38 | } 39 | 40 | bool StopAfterCurrentTrackOption::getValue() const 41 | { 42 | return playbackControl_->get_stop_after_current(); 43 | } 44 | 45 | void StopAfterCurrentTrackOption::setValue(bool value) 46 | { 47 | playbackControl_->set_stop_after_current(value); 48 | 49 | if (callback_) 50 | callback_(PlayerEvents::PLAYER_CHANGED); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /cpp/server/foobar2000/player_options.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "common.hpp" 4 | #include "../player_api.hpp" 5 | 6 | namespace msrv::player_foobar2000 { 7 | class PlaybackOrderOption : public EnumPlayerOption 8 | { 9 | public: 10 | explicit PlaybackOrderOption(playlist_manager_v4* playlistManager); 11 | int32_t getValue() const override; 12 | void setValue(int32_t value) override; 13 | 14 | private: 15 | playlist_manager_v4* playlistManager_; 16 | 17 | static std::vector getEnumNames(playlist_manager_v4* playlistManager); 18 | }; 19 | 20 | class StopAfterCurrentTrackOption : public BoolPlayerOption 21 | { 22 | public: 23 | explicit StopAfterCurrentTrackOption(playback_control* playbackControl); 24 | 25 | bool getValue() const override; 26 | void setValue(bool value) override; 27 | 28 | void setCallback(PlayerEventsCallback callback) 29 | { 30 | callback_ = std::move(callback); 31 | } 32 | 33 | private: 34 | playback_control* playbackControl_; 35 | PlayerEventsCallback callback_; 36 | }; 37 | 38 | } 39 | -------------------------------------------------------------------------------- /cpp/server/foobar2000/playlist_mapping.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../player_api.hpp" 4 | #include "common.hpp" 5 | 6 | namespace msrv { 7 | namespace player_foobar2000 { 8 | 9 | class PlaylistMapping 10 | { 11 | public: 12 | PlaylistMapping(); 13 | ~PlaylistMapping(); 14 | 15 | std::string getId(t_size index); 16 | 17 | t_size resolve(const PlaylistRef& plref); 18 | 19 | void ensureInitialized() 20 | { 21 | if (maxId_ == 0) 22 | initialize(); 23 | } 24 | 25 | private: 26 | bool readId(t_size index, pfc::array_t* buffer); 27 | void writeId(t_size index, const void* data, size_t size); 28 | void initialize(); 29 | 30 | service_ptr_t playlistManager_; 31 | int64_t maxId_; 32 | 33 | MSRV_NO_COPY_AND_ASSIGN(PlaylistMapping); 34 | }; 35 | 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /cpp/server/foobar2000/plugin.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "common.hpp" 4 | #include "player.hpp" 5 | #include "utils.hpp" 6 | 7 | #include "../settings.hpp" 8 | #include "../server_host.hpp" 9 | 10 | namespace msrv::player_foobar2000 { 11 | 12 | class Plugin 13 | { 14 | public: 15 | static Plugin* current() 16 | { 17 | return current_; 18 | } 19 | 20 | Plugin(); 21 | ~Plugin(); 22 | 23 | void reconfigure(); 24 | 25 | static Path getProfileDir(); 26 | 27 | private: 28 | static Plugin* current_; 29 | 30 | PlayerImpl player_; 31 | ServerHost host_; 32 | 33 | MSRV_NO_COPY_AND_ASSIGN(Plugin); 34 | }; 35 | 36 | } 37 | -------------------------------------------------------------------------------- /cpp/server/foobar2000/prefs_page.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "common.hpp" 4 | #include 5 | 6 | namespace msrv::player_foobar2000 { 7 | 8 | class PrefsPageInstance : public preferences_page_instance 9 | { 10 | public: 11 | PrefsPageInstance(const wchar_t* dialogTemplate, HWND parent, preferences_page_callback::ptr callback); 12 | ~PrefsPageInstance(); 13 | 14 | t_uint32 get_state() override; 15 | 16 | HWND get_wnd() override 17 | { 18 | return window_; 19 | } 20 | 21 | virtual void apply() 22 | { 23 | } 24 | 25 | virtual void reset() 26 | { 27 | } 28 | 29 | protected: 30 | HWND window() 31 | { 32 | return window_; 33 | } 34 | 35 | HWND parentWindow() 36 | { 37 | return parentWindow_; 38 | } 39 | 40 | void notifyChanged() 41 | { 42 | callback_->on_state_changed(); 43 | } 44 | 45 | virtual INT_PTR dialogProc(UINT message, WPARAM wparam, LPARAM lparam); 46 | 47 | virtual INT_PTR handleCommand(int control, int message) 48 | { 49 | return 0; 50 | } 51 | 52 | virtual INT_PTR handleNotify(NMHDR* data) 53 | { 54 | return 0; 55 | } 56 | 57 | virtual bool hasChanges() 58 | { 59 | return false; 60 | } 61 | 62 | private: 63 | static INT_PTR CALLBACK dialogProcWrapper(HWND window, UINT message, WPARAM wparam, LPARAM lparam); 64 | 65 | HWND window_ = nullptr; 66 | HWND parentWindow_ = nullptr; 67 | preferences_page_callback::ptr callback_; 68 | fb2k::CCoreDarkModeHooks darkModeHooks_; 69 | }; 70 | 71 | } 72 | -------------------------------------------------------------------------------- /cpp/server/foobar2000/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by resource.rc 4 | // 5 | #define IDD_MAIN_PREFS 7 6 | #define IDD_PERMISSIONS_PREFS 104 7 | #define IDC_PORT 1000 8 | #define IDC_ALLOW_REMOTE 1001 9 | #define IDC_AUTH_REQUIRED 1002 10 | #define IDC_AUTH_USER 1003 11 | #define IDC_AUTH_PASSWORD 1004 12 | #define IDC_MUSIC_DIRS 1005 13 | #define IDC_MUSIC_DIR_REMOVE 1007 14 | #define IDC_BUTTON2 1008 15 | #define IDC_MUSIC_DIR_ADD 1008 16 | #define IDC_AUTH_SHOW_PASSWORD 1009 17 | #define IDC_LINK_OPEN 1010 18 | #define IDC_LINK_DONATE 1011 19 | #define IDC_LINK_SOURCES 1012 20 | #define IDC_ALLOW_CHANGE_PLAYLISTS 1012 21 | #define IDC_LINK_3RD_PARTY_LICENSES 1013 22 | #define IDC_ALLOW_CHANGE_OUTPUT 1013 23 | #define IDC_LINK_API_DOCS 1014 24 | #define IDC_ALLOW_CHANGE_CLIENT_CONFIG 1014 25 | 26 | // Next default values for new objects 27 | // 28 | #ifdef APSTUDIO_INVOKED 29 | #ifndef APSTUDIO_READONLY_SYMBOLS 30 | #define _APS_NEXT_RESOURCE_VALUE 106 31 | #define _APS_NEXT_COMMAND_VALUE 40001 32 | #define _APS_NEXT_CONTROL_VALUE 1013 33 | #define _APS_NEXT_SYMED_VALUE 101 34 | #endif 35 | #endif 36 | -------------------------------------------------------------------------------- /cpp/server/foobar2000/settings.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "common.hpp" 4 | #include "../settings.hpp" 5 | 6 | #include 7 | #include 8 | 9 | namespace msrv::player_foobar2000::settings_store { 10 | 11 | extern cfg_int port; 12 | extern cfg_bool allowRemote; 13 | extern cfg_string musicDirs; 14 | extern cfg_bool authRequired; 15 | extern cfg_string authUser; 16 | extern cfg_string authPassword; 17 | extern cfg_bool allowChangePlaylists; 18 | extern cfg_bool allowChangeOutput; 19 | extern cfg_bool allowChangeClientConfig; 20 | 21 | ApiPermissions getPermissions(); 22 | 23 | std::vector getMusicDirs(); 24 | 25 | void setMusicDirs(const std::vector& dirs); 26 | 27 | } 28 | -------------------------------------------------------------------------------- /cpp/server/gzip.cpp: -------------------------------------------------------------------------------- 1 | #include "gzip.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace msrv { 9 | 10 | namespace { 11 | 12 | void throwIfZlibError(int res) 13 | { 14 | if (res != Z_OK) 15 | throw std::runtime_error(std::string("zlib error: ") + zError(res)); 16 | } 17 | 18 | struct DeflateStreamDeleter 19 | { 20 | public: 21 | void operator()(z_stream* stream) 22 | { 23 | ::deflateEnd(stream); 24 | } 25 | }; 26 | 27 | } 28 | 29 | bool gzipCompress(const void* data, size_t size, std::vector* output, size_t maxOutputSize) 30 | { 31 | ::z_stream stream; 32 | stream.zalloc = nullptr; 33 | stream.zfree = nullptr; 34 | 35 | int res = deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 + 16, 9, Z_DEFAULT_STRATEGY); 36 | throwIfZlibError(res); 37 | 38 | std::unique_ptr streamLifetime(&stream); 39 | 40 | if (maxOutputSize == 0) 41 | maxOutputSize = deflateBound(&stream, size); 42 | 43 | output->resize(maxOutputSize); 44 | 45 | stream.next_in = const_cast(reinterpret_cast(data)); 46 | stream.avail_in = static_cast(size); 47 | 48 | stream.next_out = output->data(); 49 | stream.avail_out = static_cast(output->size()); 50 | 51 | res = ::deflate(&stream, Z_FINISH); 52 | if (res == Z_STREAM_END) 53 | { 54 | output->resize(stream.total_out); 55 | return true; 56 | } 57 | else 58 | { 59 | throwIfZlibError(res); 60 | // Z_OK here means not enought space, give up compression 61 | return false; 62 | } 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /cpp/server/gzip.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | namespace msrv { 8 | 9 | bool gzipCompress(const void* data, size_t size, std::vector* output, size_t maxOutputSize = 0); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /cpp/server/http.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "defines.hpp" 4 | #include "string_utils.hpp" 5 | 6 | namespace msrv { 7 | 8 | using HttpKeyValueMap = AsciiLowerCaseMap; 9 | 10 | enum class HttpMethod 11 | { 12 | UNDEFINED, 13 | GET, 14 | POST, 15 | OPTIONS, 16 | COUNT, 17 | }; 18 | 19 | enum class HttpStatus 20 | { 21 | UNDEFINED = 0, 22 | S_200_OK = 200, 23 | S_202_ACCEPTED = 202, 24 | S_204_NO_CONTENT = 204, 25 | S_304_NOT_MODIFIED = 304, 26 | S_307_TEMP_REDIRECT = 307, 27 | S_308_PERM_REDIRECT = 308, 28 | S_400_BAD_REQUEST = 400, 29 | S_401_UNAUTHORIZED = 401, 30 | S_403_FORBIDDEN = 403, 31 | S_404_NOT_FOUND = 404, 32 | S_405_METHOD_NOT_ALLOWED = 405, 33 | S_500_SERVER_ERROR = 500, 34 | S_501_NOT_IMPLEMENTED = 501, 35 | }; 36 | 37 | struct HttpHeader 38 | { 39 | static const char CONTENT_TYPE[]; 40 | static const char CONTENT_LENGTH[]; 41 | static const char AUTHORIZATION[]; 42 | static const char WWW_AUTHENTICATE[]; 43 | static const char IF_NONE_MATCH[]; 44 | static const char ETAG[]; 45 | static const char CACHE_CONTROL[]; 46 | static const char ACCEPT_ENCODING[]; 47 | static const char CONTENT_ENCODING[]; 48 | static const char LOCATION[]; 49 | }; 50 | 51 | struct ContentType 52 | { 53 | static const char APPLICATION_OCTET_STREAM[]; 54 | static const char APPLICATION_JSON[]; 55 | static const char APPLICATION_JAVASCRIPT[]; 56 | static const char TEXT_PLAIN_UTF8[]; 57 | static const char TEXT_HTML_UTF8[]; 58 | static const char TEXT_CSS[]; 59 | static const char IMAGE_SVG[]; 60 | static const char IMAGE_JPEG[]; 61 | static const char IMAGE_PNG[]; 62 | static const char IMAGE_GIF[]; 63 | static const char IMAGE_BMP[]; 64 | }; 65 | 66 | std::string toString(HttpMethod method); 67 | std::string toString(HttpStatus status); 68 | 69 | inline bool isSuccessStatus(HttpStatus status) 70 | { 71 | int code = static_cast(status); 72 | return code >= 200 && code <= 299; 73 | } 74 | 75 | std::string urlDecode(StringView str); 76 | 77 | HttpKeyValueMap parseQueryString(StringView str); 78 | 79 | } 80 | -------------------------------------------------------------------------------- /cpp/server/json.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace msrv { 6 | 7 | using Json = nlohmann::json; 8 | 9 | inline std::string jsonDumpSafe(const Json& json) 10 | { 11 | return json.dump(-1, ' ', false, Json::error_handler_t::replace); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /cpp/server/log.cpp: -------------------------------------------------------------------------------- 1 | #include "log.hpp" 2 | #include "project_info.hpp" 3 | 4 | namespace msrv { 5 | 6 | Logger* Logger::current_; 7 | 8 | #ifndef NDEBUG 9 | 10 | void logDebug(const char* fmt, ...) 11 | { 12 | va_list va; 13 | va_start(va, fmt); 14 | 15 | if (auto logger = Logger::getCurrent()) 16 | logger->log(LogLevel::DEBUG, fmt, va); 17 | 18 | va_end(va); 19 | } 20 | 21 | #endif 22 | 23 | void logInfo(const char* fmt, ...) 24 | { 25 | va_list va; 26 | va_start(va, fmt); 27 | 28 | if (auto logger = Logger::getCurrent()) 29 | logger->log(LogLevel::INFO, fmt, va); 30 | 31 | va_end(va); 32 | } 33 | 34 | void logError(const char* fmt, ...) 35 | { 36 | va_list va; 37 | va_start(va, fmt); 38 | 39 | if (auto logger = Logger::getCurrent()) 40 | logger->log(LogLevel::ERROR, fmt, va); 41 | 42 | va_end(va); 43 | } 44 | 45 | StderrLogger::StderrLogger() 46 | : prefix_(MSRV_PROJECT_ID ": ") 47 | { 48 | } 49 | 50 | StderrLogger::~StderrLogger() 51 | { 52 | } 53 | 54 | void StderrLogger::log(LogLevel, const char* fmt, va_list va) 55 | { 56 | std::string format = prefix_ + fmt + "\n"; 57 | 58 | ::vfprintf(stderr, format.c_str(), va); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /cpp/server/log.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "defines.hpp" 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | namespace msrv { 10 | 11 | enum class LogLevel 12 | { 13 | DEBUG, 14 | INFO, 15 | ERROR 16 | }; 17 | 18 | class Logger 19 | { 20 | public: 21 | Logger() = default; 22 | virtual ~Logger() = default; 23 | virtual void log(LogLevel, const char*, va_list va) = 0; 24 | 25 | static Logger* getCurrent() 26 | { 27 | return current_; 28 | } 29 | 30 | static void setCurrent(Logger* logger) 31 | { 32 | current_ = logger; 33 | } 34 | 35 | private: 36 | static Logger* current_; 37 | 38 | MSRV_NO_COPY_AND_ASSIGN(Logger); 39 | }; 40 | 41 | class StderrLogger final : public Logger 42 | { 43 | public: 44 | StderrLogger(); 45 | virtual ~StderrLogger(); 46 | virtual void log(LogLevel level, const char* fmt, va_list va) override; 47 | 48 | private: 49 | std::string prefix_; 50 | 51 | MSRV_NO_COPY_AND_ASSIGN(StderrLogger); 52 | }; 53 | 54 | class LoggerScope 55 | { 56 | public: 57 | explicit LoggerScope(Logger* logger) 58 | : previous_(Logger::getCurrent()) 59 | { 60 | Logger::setCurrent(logger); 61 | } 62 | 63 | ~LoggerScope() 64 | { 65 | Logger::setCurrent(previous_); 66 | } 67 | 68 | private: 69 | Logger* previous_; 70 | 71 | MSRV_NO_COPY_AND_ASSIGN(LoggerScope); 72 | }; 73 | 74 | #ifdef NDEBUG 75 | inline void logDebug(const char*, ...) { } 76 | #else 77 | void logDebug(const char* fmt, ...) MSRV_FORMAT_FUNC(1, 2); 78 | #endif 79 | 80 | void logInfo(const char* fmt, ...) MSRV_FORMAT_FUNC(1, 2); 81 | 82 | void logError(const char* fmt, ...) MSRV_FORMAT_FUNC(1, 2); 83 | 84 | template 85 | bool tryCatchLog(Func&& func) 86 | { 87 | try 88 | { 89 | func(); 90 | return true; 91 | } 92 | catch (std::exception& ex) 93 | { 94 | logError("%s", ex.what()); 95 | return false; 96 | } 97 | catch (...) 98 | { 99 | logError("unknown error"); 100 | return false; 101 | } 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /cpp/server/outputs_controller.cpp: -------------------------------------------------------------------------------- 1 | #include "outputs_controller.hpp" 2 | #include "router.hpp" 3 | #include "player_api.hpp" 4 | #include "player_api_json.hpp" 5 | 6 | namespace msrv { 7 | 8 | OutputsController::OutputsController(Request* request, Player* player, SettingsDataPtr settings) 9 | : ControllerBase(request), player_(player), settings_(std::move(settings)) 10 | { 11 | } 12 | 13 | ResponsePtr OutputsController::getOutputs() 14 | { 15 | auto outputs = player_->getOutputs(); 16 | return Response::json({{"outputs", outputs}}); 17 | } 18 | 19 | void OutputsController::setOutputDevice() 20 | { 21 | settings_->ensurePermissions(ApiPermissions::CHANGE_OUTPUT); 22 | auto typeId = optionalParam("typeId", {}); 23 | auto deviceId = param("deviceId"); 24 | player_->setOutputDevice(typeId, deviceId); 25 | } 26 | 27 | void OutputsController::defineRoutes(Router* router, WorkQueue* workQueue, Player* player, SettingsDataPtr settings) 28 | { 29 | auto routes = router->defineRoutes(); 30 | routes.createWith([=](Request* r) { return new OutputsController(r, player, settings); }); 31 | routes.useWorkQueue(workQueue); 32 | routes.setPrefix("api/outputs"); 33 | routes.get("", &OutputsController::getOutputs); 34 | routes.post("active", &OutputsController::setOutputDevice); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /cpp/server/outputs_controller.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "controller.hpp" 4 | #include "settings.hpp" 5 | 6 | namespace msrv { 7 | 8 | class Router; 9 | 10 | class Player; 11 | 12 | class OutputsController : public ControllerBase 13 | { 14 | public: 15 | OutputsController(Request* request, Player* player, SettingsDataPtr settings); 16 | 17 | ResponsePtr getOutputs(); 18 | void setOutputDevice(); 19 | 20 | static void defineRoutes(Router* router, WorkQueue* workQueue, Player* player, SettingsDataPtr settings); 21 | 22 | private: 23 | Player* player_; 24 | SettingsDataPtr settings_; 25 | 26 | MSRV_NO_COPY_AND_ASSIGN(OutputsController); 27 | }; 28 | 29 | } 30 | -------------------------------------------------------------------------------- /cpp/server/parsing.cpp: -------------------------------------------------------------------------------- 1 | #include "parsing.hpp" 2 | 3 | namespace msrv { 4 | 5 | bool ValueParser::tryParse(StringView str, bool* outVal) 6 | { 7 | switch (str.length()) 8 | { 9 | case 4: 10 | if (::memcmp(str.data(), "true", 4) == 0) 11 | { 12 | *outVal = true; 13 | return true; 14 | } 15 | 16 | return false; 17 | 18 | case 5: 19 | if (::memcmp(str.data(), "false", 5) == 0) 20 | { 21 | *outVal = false; 22 | return true; 23 | } 24 | 25 | return false; 26 | 27 | default: 28 | return false; 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /cpp/server/play_queue_controller.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "controller.hpp" 4 | 5 | namespace msrv { 6 | 7 | class Router; 8 | 9 | class Player; 10 | 11 | class PlayQueueController : public ControllerBase 12 | { 13 | public: 14 | PlayQueueController(Request* request, Player* player); 15 | 16 | ResponsePtr getQueue(); 17 | void addToQueue(); 18 | void removeFromQueue(); 19 | void clearQueue(); 20 | 21 | static void defineRoutes(Router* router, WorkQueue* workQueue, Player* player); 22 | 23 | private: 24 | Player* player_; 25 | 26 | MSRV_NO_COPY_AND_ASSIGN(PlayQueueController); 27 | }; 28 | 29 | } 30 | -------------------------------------------------------------------------------- /cpp/server/player_api_json.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "player_api.hpp" 4 | #include "json.hpp" 5 | 6 | namespace msrv { 7 | 8 | void from_json(const Json& json, PlaylistRef& value); 9 | void to_json(Json& json, const PlaylistRef& value); 10 | 11 | void to_json(Json& json, const PlaybackState& value); 12 | void to_json(Json& json, const PlayerInfo& value); 13 | void to_json(Json& json, const VolumeInfo& value); 14 | void to_json(Json& json, const ActiveItemInfo& value); 15 | void to_json(Json& json, const PlayerState& value); 16 | void to_json(Json& json, const PlaylistInfo& value); 17 | void to_json(Json& json, const PlaylistItemInfo& value); 18 | void to_json(Json& json, const PlaylistItemsResult& value); 19 | void to_json(Json& json, const PlayQueueItemInfo& value); 20 | void to_json(Json& json, const OutputDeviceInfo& value); 21 | void to_json(Json& json, const OutputTypeInfo& value); 22 | void to_json(Json& json, const ActiveOutputInfo& value); 23 | void to_json(Json& json, const OutputsInfo& value); 24 | void to_json(Json& json, PlayerOption* const& value); 25 | 26 | } 27 | -------------------------------------------------------------------------------- /cpp/server/player_api_parsers.cpp: -------------------------------------------------------------------------------- 1 | #include "player_api_parsers.hpp" 2 | 3 | namespace msrv { 4 | 5 | bool ValueParser::tryParse(StringView str, PlaylistRef* outVal) 6 | { 7 | if (str.empty()) 8 | return false; 9 | 10 | int32_t index; 11 | 12 | if (tryParseValue(str, &index)) 13 | { 14 | if (index >= 0) 15 | { 16 | *outVal = PlaylistRef(index); 17 | return true; 18 | } 19 | else 20 | { 21 | return false; 22 | } 23 | } 24 | else 25 | { 26 | *outVal = PlaylistRef(str.to_string()); 27 | return true; 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /cpp/server/player_api_parsers.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "player_api.hpp" 4 | #include "parsing.hpp" 5 | 6 | namespace msrv { 7 | 8 | template<> 9 | struct ValueParser 10 | { 11 | static bool tryParse(StringView str, PlaylistRef* outVal); 12 | }; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /cpp/server/player_controller.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "defines.hpp" 4 | #include "controller.hpp" 5 | #include "settings.hpp" 6 | 7 | namespace msrv { 8 | 9 | class Player; 10 | 11 | class Router; 12 | 13 | struct SetOptionRequest; 14 | 15 | class PlayerController : public ControllerBase 16 | { 17 | public: 18 | PlayerController(Request* request, Player* player, SettingsDataPtr settings); 19 | 20 | ResponsePtr getState(); 21 | void setState(); 22 | 23 | void playItem(); 24 | void playCurrent(); 25 | void playNext(); 26 | void playPrevious(); 27 | void playRandom(); 28 | void stop(); 29 | void pause(); 30 | void togglePause(); 31 | void playOrPause(); 32 | void volumeUp(); 33 | void volumeDown(); 34 | 35 | static void defineRoutes(Router* router, WorkQueue* workQueue, Player* player, SettingsDataPtr settings); 36 | 37 | private: 38 | Player* player_; 39 | SettingsDataPtr settings_; 40 | 41 | void setOption(const SetOptionRequest& request); 42 | 43 | MSRV_NO_COPY_AND_ASSIGN(PlayerController); 44 | }; 45 | 46 | } 47 | -------------------------------------------------------------------------------- /cpp/server/player_events.cpp: -------------------------------------------------------------------------------- 1 | #include "player_events.hpp" 2 | 3 | #include 4 | 5 | namespace msrv { 6 | 7 | std::unique_ptr EventDispatcher::createListener(PlayerEvents eventMask) 8 | { 9 | std::unique_ptr listener(new EventListener(eventMask)); 10 | 11 | { 12 | std::lock_guard lock(mutex_); 13 | listeners_.insert(listener.get()); 14 | listener->owner_ = this; 15 | } 16 | 17 | return listener; 18 | } 19 | 20 | void EventDispatcher::dispatch(PlayerEvents events) 21 | { 22 | std::lock_guard lock(mutex_); 23 | 24 | for (auto listener : listeners_) 25 | { 26 | auto maskedEvents = listener->eventMask_ & events; 27 | 28 | if (maskedEvents != PlayerEvents::NONE) 29 | { 30 | listener->pendingEvents_.fetch_or(static_cast(maskedEvents)); 31 | } 32 | } 33 | } 34 | 35 | EventListener::EventListener(PlayerEvents eventMask) 36 | : owner_(nullptr), eventMask_(eventMask), pendingEvents_(static_cast(eventMask)) 37 | { 38 | } 39 | 40 | EventListener::~EventListener() 41 | { 42 | if (!owner_) 43 | return; 44 | 45 | std::lock_guard lock(owner_->mutex_); 46 | 47 | auto it = owner_->listeners_.find(this); 48 | assert(it != owner_->listeners_.end()); 49 | owner_->listeners_.erase(it); 50 | } 51 | 52 | PlayerEvents EventListener::readEvents() 53 | { 54 | return static_cast(pendingEvents_.exchange(0)); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /cpp/server/player_events.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "defines.hpp" 4 | #include "player_api.hpp" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | namespace msrv { 13 | 14 | class EventListener; 15 | 16 | class EventDispatcher 17 | { 18 | public: 19 | EventDispatcher() = default; 20 | 21 | std::unique_ptr createListener(PlayerEvents eventMask); 22 | void dispatch(PlayerEvents events); 23 | 24 | private: 25 | friend class EventListener; 26 | 27 | std::mutex mutex_; 28 | std::unordered_set listeners_; 29 | 30 | MSRV_NO_COPY_AND_ASSIGN(EventDispatcher); 31 | }; 32 | 33 | class EventListener 34 | { 35 | public: 36 | ~EventListener(); 37 | PlayerEvents readEvents(); 38 | 39 | private: 40 | friend class EventDispatcher; 41 | 42 | explicit EventListener(PlayerEvents eventMask); 43 | 44 | EventDispatcher* owner_; 45 | const PlayerEvents eventMask_; 46 | std::atomic_int pendingEvents_; 47 | 48 | MSRV_NO_COPY_AND_ASSIGN(EventListener); 49 | }; 50 | 51 | } 52 | -------------------------------------------------------------------------------- /cpp/server/playlists_controller.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "defines.hpp" 4 | #include "controller.hpp" 5 | #include "settings.hpp" 6 | 7 | namespace msrv { 8 | 9 | class Player; 10 | 11 | class Router; 12 | 13 | class PlaylistsController : public ControllerBase 14 | { 15 | public: 16 | PlaylistsController(Request* request, Player* player, SettingsDataPtr settings); 17 | ~PlaylistsController(); 18 | 19 | ResponsePtr getPlaylist(); 20 | ResponsePtr getPlaylists(); 21 | ResponsePtr getPlaylistItems(); 22 | 23 | ResponsePtr addPlaylist(); 24 | void removePlaylist(); 25 | void movePlaylist(); 26 | 27 | void updatePlaylist(); 28 | void updatePlaylists(); 29 | void clearPlaylist(); 30 | 31 | ResponsePtr addItems(); 32 | 33 | void moveItemsInPlaylist(); 34 | void copyItemsInPlaylist(); 35 | 36 | void moveItemsBetweenPlaylists(); 37 | void copyItemsBetweenPlaylists(); 38 | 39 | void removeItems(); 40 | 41 | void sortItems(); 42 | 43 | static void defineRoutes(Router* router, WorkQueue* workQueue, Player* player, SettingsDataPtr settings); 44 | 45 | private: 46 | std::string validateAndNormalizeItem(const std::string& item); 47 | 48 | void checkPermissions() 49 | { 50 | settings_->ensurePermissions(ApiPermissions::CHANGE_PLAYLISTS); 51 | } 52 | 53 | Player* player_; 54 | SettingsDataPtr settings_; 55 | 56 | MSRV_NO_COPY_AND_ASSIGN(PlaylistsController); 57 | }; 58 | 59 | } 60 | -------------------------------------------------------------------------------- /cpp/server/query_controller.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "defines.hpp" 4 | #include "controller.hpp" 5 | #include "player_events.hpp" 6 | #include "settings.hpp" 7 | 8 | #include 9 | 10 | namespace msrv { 11 | 12 | class Router; 13 | 14 | class QueryController : public ControllerBase 15 | { 16 | public: 17 | QueryController(Request* request, Player* player, EventDispatcher* dispatcher, SettingsDataPtr settings); 18 | ~QueryController(); 19 | 20 | ResponsePtr query(); 21 | ResponsePtr getEvents(); 22 | ResponsePtr getUpdates(); 23 | 24 | static void defineRoutes( 25 | Router* router, WorkQueue* workQueue, Player* player, EventDispatcher* dispatcher, SettingsDataPtr settings); 26 | 27 | private: 28 | PlayerEvents readEventMask(); 29 | void createQueries(PlayerEvents events); 30 | void listenForEvents(PlayerEvents events); 31 | 32 | static Json eventsToJson(PlayerEvents events); 33 | Json stateToJson(PlayerEvents events); 34 | 35 | Player* player_; 36 | EventDispatcher* dispatcher_; 37 | SettingsDataPtr settings_; 38 | 39 | std::unique_ptr listener_; 40 | ColumnsQueryPtr activeItemQuery_; 41 | PlaylistRef playlistRef_; 42 | Range playlistRange_; 43 | ColumnsQueryPtr playlistQuery_; 44 | ColumnsQueryPtr queueQuery_; 45 | }; 46 | 47 | } 48 | -------------------------------------------------------------------------------- /cpp/server/request.cpp: -------------------------------------------------------------------------------- 1 | #include "request.hpp" 2 | #include "response.hpp" 3 | #include "log.hpp" 4 | 5 | namespace msrv { 6 | 7 | namespace { 8 | 9 | class EmptyRequestHandler : public RequestHandler 10 | { 11 | public: 12 | std::unique_ptr execute() override 13 | { 14 | return Response::custom(HttpStatus::S_204_NO_CONTENT); 15 | } 16 | }; 17 | 18 | class EmptyRequestHandlerFactory : public RequestHandlerFactory 19 | { 20 | public: 21 | WorkQueue* workQueue() override 22 | { 23 | return nullptr; 24 | } 25 | 26 | RequestHandlerPtr createHandler(Request*) override 27 | { 28 | return std::make_unique(); 29 | } 30 | }; 31 | 32 | } 33 | 34 | Request::Request() 35 | : method(HttpMethod::GET), 36 | isProcessed_(false), 37 | isHandlerExecuted_(false) 38 | { 39 | } 40 | 41 | Request::Request(HttpMethod methodVal, std::string pathVal) 42 | : method(methodVal), 43 | path(std::move(pathVal)), 44 | isProcessed_(false), 45 | isHandlerExecuted_(false) 46 | { 47 | } 48 | 49 | Request::~Request() = default; 50 | 51 | void Request::setErrorResponse(std::string message, std::string param) 52 | { 53 | response = Response::error(HttpStatus::S_400_BAD_REQUEST, std::move(message), std::move(param)); 54 | } 55 | 56 | void Request::executeHandler() 57 | { 58 | assert(handler); 59 | assert(!isHandlerExecuted_); 60 | isHandlerExecuted_ = true; 61 | response = handler->execute(); 62 | } 63 | 64 | RequestHandlerFactory* RequestHandlerFactory::empty() 65 | { 66 | static EmptyRequestHandlerFactory factory; 67 | return &factory; 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /cpp/server/request_filter.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "defines.hpp" 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | namespace msrv { 10 | 11 | class Request; 12 | 13 | class RequestFilter; 14 | 15 | using RequestFilterPtr = std::unique_ptr; 16 | 17 | class RequestFilter 18 | { 19 | public: 20 | RequestFilter() = default; 21 | virtual ~RequestFilter() = default; 22 | 23 | virtual void beginRequest(Request*) 24 | { 25 | } 26 | 27 | virtual void endRequest(Request*) 28 | { 29 | } 30 | 31 | private: 32 | MSRV_NO_COPY_AND_ASSIGN(RequestFilter); 33 | }; 34 | 35 | class ExecuteHandlerFilter : public RequestFilter 36 | { 37 | public: 38 | ~ExecuteHandlerFilter() override = default; 39 | 40 | void beginRequest(Request* request) override; 41 | }; 42 | 43 | class RequestFilterChain 44 | { 45 | public: 46 | RequestFilterChain(); 47 | ~RequestFilterChain(); 48 | 49 | void add(RequestFilterPtr filter); 50 | void beginRequest(Request* request) const; 51 | void endRequest(Request* request) const; 52 | 53 | private: 54 | std::vector filters_; 55 | 56 | MSRV_NO_COPY_AND_ASSIGN(RequestFilterChain); 57 | }; 58 | 59 | } 60 | -------------------------------------------------------------------------------- /cpp/server/response_headers_filter.cpp: -------------------------------------------------------------------------------- 1 | #include "request.hpp" 2 | #include "response.hpp" 3 | #include "response_headers_filter.hpp" 4 | 5 | namespace msrv { 6 | 7 | ResponseHeadersFilter::ResponseHeadersFilter(SettingsDataPtr settings) 8 | : settings_(settings) 9 | { 10 | } 11 | 12 | ResponseHeadersFilter::~ResponseHeadersFilter() = default; 13 | 14 | void ResponseHeadersFilter::endRequest(Request* request) 15 | { 16 | if (request->response) 17 | request->response->addHeaders(settings_->responseHeaders); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /cpp/server/response_headers_filter.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "request_filter.hpp" 3 | #include "settings.hpp" 4 | 5 | namespace msrv { 6 | class ResponseHeadersFilter : public RequestFilter 7 | { 8 | public: 9 | explicit ResponseHeadersFilter(SettingsDataPtr settings); 10 | ~ResponseHeadersFilter() override; 11 | virtual void endRequest(Request* request) override; 12 | 13 | private: 14 | SettingsDataPtr settings_; 15 | }; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /cpp/server/response_sender.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "response.hpp" 4 | #include "server_core.hpp" 5 | 6 | namespace msrv { 7 | 8 | class RequestCore; 9 | 10 | class ResponseSender : private ResponseHandler 11 | { 12 | public: 13 | explicit ResponseSender(RequestCore* corereq) 14 | : requestCore_(corereq) 15 | { 16 | } 17 | 18 | ~ResponseSender() = default; 19 | 20 | void send(Response* response); 21 | void sendEvent(Json event); 22 | void sendEventStream(EventStreamResponse* response, Json event); 23 | 24 | private: 25 | static std::string eventToString(const Json& value); 26 | 27 | void initResponse(Response* response); 28 | 29 | virtual void handleResponse(SimpleResponse*) override; 30 | virtual void handleResponse(DataResponse*) override; 31 | virtual void handleResponse(FileResponse*) override; 32 | virtual void handleResponse(JsonResponse*) override; 33 | virtual void handleResponse(ErrorResponse*) override; 34 | virtual void handleResponse(EventStreamResponse*) override; 35 | virtual void handleResponse(AsyncResponse*) override; 36 | 37 | void setJsonBody(const Json& value); 38 | 39 | void setHeader(std::string key, std::string value) 40 | { 41 | responseCore_->headers[std::move(key)] = std::move(value); 42 | } 43 | 44 | RequestCore* requestCore_; 45 | ResponseCorePtr responseCore_; 46 | }; 47 | 48 | } 49 | -------------------------------------------------------------------------------- /cpp/server/safe_windows.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define WIN32_LEAN_AND_MEAN 4 | #include 5 | 6 | #undef ERROR 7 | #undef min 8 | #undef max 9 | -------------------------------------------------------------------------------- /cpp/server/server_host.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "defines.hpp" 4 | #include "content_type_map.hpp" 5 | #include "player_api.hpp" 6 | #include "player_events.hpp" 7 | #include "server_thread.hpp" 8 | #include "router.hpp" 9 | #include "settings.hpp" 10 | #include "work_queue.hpp" 11 | #include "request_filter.hpp" 12 | 13 | #include 14 | 15 | namespace msrv { 16 | 17 | class ServerHost 18 | { 19 | public: 20 | explicit ServerHost(Player* player); 21 | virtual ~ServerHost(); 22 | 23 | void reconfigure(SettingsDataPtr settings); 24 | 25 | private: 26 | void handlePlayerEvents(PlayerEvents events); 27 | 28 | Player* player_; 29 | 30 | EventDispatcher dispatcher_; 31 | ContentTypeMap contentTypes_; 32 | 33 | std::unique_ptr playerWorkQueue_; 34 | ThreadPoolWorkQueue utilityQueue_; 35 | std::unique_ptr serverThread_; 36 | 37 | MSRV_NO_COPY_AND_ASSIGN(ServerHost); 38 | }; 39 | 40 | } 41 | -------------------------------------------------------------------------------- /cpp/server/server_thread.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "server.hpp" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | namespace msrv { 12 | 13 | using ServerReadyCallback = std::function; 14 | 15 | class ServerThread 16 | { 17 | public: 18 | ServerThread(ServerReadyCallback readyCallback = ServerReadyCallback()); 19 | ~ServerThread(); 20 | 21 | void restart(ServerConfigPtr config) 22 | { 23 | sendCommand(Command::RESTART, std::move(config)); 24 | } 25 | 26 | void dispatchEvents(); 27 | 28 | private: 29 | enum class Command 30 | { 31 | NONE, 32 | RESTART, 33 | EXIT, 34 | }; 35 | 36 | Command readCommand(ServerConfigPtr* config); 37 | void run(); 38 | void runOnce(ServerConfigPtr config); 39 | void sendCommand(Command command, ServerConfigPtr nextConfig = ServerConfigPtr()); 40 | 41 | std::thread thread_; 42 | std::mutex mutex_; 43 | std::condition_variable commandNotify_; 44 | ServerPtr server_; 45 | Command command_; 46 | ServerConfigPtr nextConfig_; 47 | ServerReadyCallback readyCallback_; 48 | 49 | MSRV_NO_COPY_AND_ASSIGN(ServerThread); 50 | }; 51 | 52 | } 53 | -------------------------------------------------------------------------------- /cpp/server/static_controller.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "controller.hpp" 4 | #include "settings.hpp" 5 | 6 | namespace msrv { 7 | 8 | class Router; 9 | 10 | class ContentTypeMap; 11 | 12 | class WorkQueue; 13 | 14 | class StaticController : public ControllerBase 15 | { 16 | public: 17 | StaticController( 18 | Request* request, 19 | const Path& targetDir, 20 | const ContentTypeMap& contentTypes); 21 | ~StaticController(); 22 | 23 | ResponsePtr getFile(); 24 | 25 | static void defineRoutes( 26 | Router* router, 27 | WorkQueue* workQueue, 28 | SettingsDataPtr settings, 29 | const ContentTypeMap& contentTypes); 30 | 31 | static void defineRoutes( 32 | Router* router, 33 | WorkQueue* workQueue, 34 | const std::string& urlPrefix, 35 | const Path& targetDir, 36 | const ContentTypeMap& contentTypes); 37 | 38 | private: 39 | std::string getNormalizedPath(); 40 | ResponsePtr redirectToDirectory(); 41 | 42 | const Path& targetDir_; 43 | const ContentTypeMap& contentTypes_; 44 | }; 45 | 46 | } 47 | -------------------------------------------------------------------------------- /cpp/server/system.cpp: -------------------------------------------------------------------------------- 1 | #include "system.hpp" 2 | 3 | namespace msrv { 4 | 5 | std::string formatError(ErrorCode errorCode) 6 | { 7 | char buffer[256]; 8 | return std::string(formatError(errorCode, buffer, sizeof(buffer))); 9 | } 10 | 11 | std::string formatErrorFor(const char* func, ErrorCode errorCode) 12 | { 13 | return std::string(func) + " failed: " + formatError(errorCode); 14 | } 15 | 16 | void throwSystemError(const char* func, ErrorCode errorCode) 17 | { 18 | throw std::runtime_error(formatErrorFor(func, errorCode)); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /cpp/server/system_posix.cpp: -------------------------------------------------------------------------------- 1 | #include "system.hpp" 2 | 3 | #include 4 | #include 5 | 6 | namespace msrv { 7 | 8 | const char* formatError(ErrorCode errorCode, char* buffer, size_t size) noexcept 9 | { 10 | return ::strerror_r(errorCode, buffer, size); 11 | } 12 | 13 | void PosixHandleTraits::destroy(Type value) noexcept 14 | { 15 | ::close(value); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /cpp/server/system_windows.cpp: -------------------------------------------------------------------------------- 1 | #include "file_system.hpp" 2 | #include "safe_windows.h" 3 | #include 4 | 5 | namespace msrv { 6 | 7 | typedef HRESULT (WINAPI *SetThreadDescriptionFunc)(HANDLE thread, LPCWSTR name); 8 | 9 | void setThreadName(ThreadName name) 10 | { 11 | static auto setName = reinterpret_cast( 12 | GetProcAddress(LoadLibraryA("kernelbase.dll"), "SetThreadDescription")); 13 | 14 | if (setName) 15 | setName(GetCurrentThread(), name); 16 | } 17 | 18 | const char* formatError(ErrorCode errorCode, char* buffer, size_t size) noexcept 19 | { 20 | auto ret = ::FormatMessageA( 21 | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, 22 | nullptr, 23 | errorCode, 24 | MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), 25 | buffer, 26 | size, 27 | nullptr); 28 | 29 | if (ret == 0) 30 | return "Unknown error (failed to obtain error message)"; 31 | 32 | return buffer; 33 | } 34 | 35 | void WindowsHandleTraits::destroy(Type handle) noexcept 36 | { 37 | ::CloseHandle(handle); 38 | } 39 | 40 | ErrorCode lastSystemError() noexcept 41 | { 42 | return ::GetLastError(); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /cpp/server/tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | find_package(Catch REQUIRED) 2 | include_directories(${CATCH_INCLUDE_DIR}) 3 | 4 | set( 5 | RUNNER_SOURCES 6 | runner.cpp 7 | test_main.hpp 8 | base64_tests.cpp 9 | fnv_hash_tests.cpp 10 | parsing_tests.cpp 11 | router_tests.cpp 12 | server_tests.cpp 13 | string_utils_tests.cpp 14 | timers_tests.cpp 15 | ) 16 | 17 | set( 18 | ECHO_SERVER_SOURCES 19 | echo_server.cpp 20 | test_main.hpp 21 | ) 22 | 23 | add_executable( 24 | core_tests 25 | ${RUNNER_SOURCES} 26 | $ 27 | ) 28 | 29 | target_link_libraries( 30 | core_tests 31 | "${CORE_LIBRARIES}" 32 | ) 33 | 34 | add_executable( 35 | echo_server 36 | ${ECHO_SERVER_SOURCES} 37 | $ 38 | ) 39 | 40 | target_link_libraries( 41 | echo_server 42 | "${CORE_LIBRARIES}" 43 | ) 44 | -------------------------------------------------------------------------------- /cpp/server/tests/base64_tests.cpp: -------------------------------------------------------------------------------- 1 | #include "../base64.hpp" 2 | 3 | #include 4 | 5 | namespace msrv { 6 | namespace base64_tests { 7 | 8 | TEST_CASE("base64") 9 | { 10 | REQUIRE(base64Decode("Cg==") == "\n"); 11 | REQUIRE(base64Decode("WU4=") == "YN"); 12 | REQUIRE(base64Decode("SGVsbG8gV29ybGQh") == "Hello World!"); 13 | REQUIRE(base64Decode("abcd123,") == ""); 14 | } 15 | 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /cpp/server/tests/fnv_hash_tests.cpp: -------------------------------------------------------------------------------- 1 | #include "../fnv_hash.hpp" 2 | 3 | #include 4 | 5 | namespace msrv { 6 | namespace fnv_hash_tests { 7 | 8 | TEST_CASE("fnv_hash") 9 | { 10 | SECTION("addBytes") 11 | { 12 | FnvHash hash; 13 | const char input[] = "Hello World"; 14 | hash.addBytes(input, sizeof(input) - 1); 15 | REQUIRE(hash.value() == UINT64_C(0x3d58dee72d4e0c27)); 16 | } 17 | 18 | SECTION("multiple addBytes") 19 | { 20 | FnvHash hash; 21 | const char input1[] = "FNV "; 22 | const char input2[] = "Rocks"; 23 | const char input3[] = "!"; 24 | hash.addBytes(input1, sizeof(input1) - 1); 25 | hash.addBytes(input2, sizeof(input2) - 1); 26 | hash.addBytes(input3, sizeof(input3) - 1); 27 | REQUIRE(hash.value() == UINT64_C(0x738fbfb6ba6bdac6)); 28 | } 29 | 30 | SECTION("addValue") 31 | { 32 | FnvHash hash; 33 | hash.addValue(UINT32_C(0x58585858)); 34 | REQUIRE(hash.value() == UINT64_C(0xf1c9d95c7cd69f75)); 35 | } 36 | } 37 | 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /cpp/server/tests/parsing_tests.cpp: -------------------------------------------------------------------------------- 1 | #include "../parsing.hpp" 2 | 3 | #include 4 | 5 | namespace msrv { 6 | namespace base64_tests { 7 | 8 | TEST_CASE("parsing") 9 | { 10 | SECTION("parse int32") 11 | { 12 | int32_t result; 13 | REQUIRE(parseValue("123") == 123); 14 | REQUIRE(!tryParseValue("123t", &result)); 15 | } 16 | 17 | SECTION("parse bool") 18 | { 19 | bool result; 20 | REQUIRE(parseValue("true")); 21 | REQUIRE(!parseValue("false")); 22 | REQUIRE(!tryParseValue("fail", &result)); 23 | } 24 | 25 | SECTION("parse string list") 26 | { 27 | std::vector actual; 28 | std::vector expected; 29 | 30 | // strict parsing: everything is preserved with regards to escaping rules 31 | actual = parseValue>("\\\\hello\\, world,!"); 32 | expected = std::vector({"\\hello, world", "!"}); 33 | REQUIRE(actual == expected); 34 | 35 | // non strict parsing: entries are trimmed and empty ones are discarded 36 | actual = parseValueList("hello; world; ;;", ';'); 37 | expected = std::vector({"hello", "world"}); 38 | REQUIRE(actual == expected); 39 | } 40 | } 41 | 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /cpp/server/tests/runner.cpp: -------------------------------------------------------------------------------- 1 | #include "test_main.hpp" 2 | 3 | #define CATCH_CONFIG_RUNNER 4 | #include "catch.hpp" 5 | 6 | namespace msrv { 7 | 8 | int testMain(int argc, char** argv) 9 | { 10 | auto result = Catch::Session().run(argc, argv); 11 | return result ? EXIT_FAILURE : EXIT_SUCCESS; 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /cpp/server/tests/server_tests.cpp: -------------------------------------------------------------------------------- 1 | #include "../server_thread.hpp" 2 | #include "../router.hpp" 3 | #include "../request_filter.hpp" 4 | #include "../work_queue.hpp" 5 | #include "../settings.hpp" 6 | #include "project_info.hpp" 7 | 8 | #include 9 | #include 10 | 11 | namespace msrv { 12 | namespace server_tests { 13 | 14 | TEST_CASE("server") 15 | { 16 | boost::promise startedPromise; 17 | boost::unique_future started = startedPromise.get_future(); 18 | 19 | auto config = std::make_unique(MSRV_DEFAULT_TEST_PORT, false); 20 | config->filters.add(std::make_unique()); 21 | 22 | ServerThread server([&] { startedPromise.set_value(); }); 23 | server.restart(std::move(config)); 24 | 25 | for (int i = 0; i < 10 && !started.is_ready(); i++) 26 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 27 | 28 | REQUIRE(started.is_ready()); 29 | started.get(); 30 | } 31 | 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /cpp/server/tests/string_utils_tests.cpp: -------------------------------------------------------------------------------- 1 | #include "../string_utils.hpp" 2 | 3 | #include 4 | 5 | namespace msrv::string_utils_tests { 6 | 7 | TEST_CASE("AsciiLowerCaseEqual") 8 | { 9 | SECTION("works") 10 | { 11 | AsciiLowerCaseEqual equal; 12 | REQUIRE(equal("hello", "hello") == true); 13 | REQUIRE(equal("hello", "Hello") == true); 14 | REQUIRE(equal("hello", "hallo") == false); 15 | REQUIRE(equal("hello", "h") == false); 16 | } 17 | } 18 | 19 | TEST_CASE("AsciiLowerCaseHash") 20 | { 21 | SECTION("works") 22 | { 23 | AsciiLowerCaseHash hash; 24 | REQUIRE(hash("hello") == hash("hello")); 25 | REQUIRE(hash("hello") == hash("Hello")); 26 | REQUIRE(hash("hello") != hash("hallo")); 27 | REQUIRE(hash("hello") != hash("h")); 28 | } 29 | } 30 | 31 | TEST_CASE("AsciiLowerCaseMap") 32 | { 33 | SECTION("works") 34 | { 35 | AsciiLowerCaseMap map; 36 | 37 | map.emplace("test", 1); 38 | auto it = map.find("TEST"); 39 | REQUIRE(it != map.end()); 40 | REQUIRE(it->second == 1); 41 | 42 | map["Test"] = 2; 43 | it = map.find("TEST"); 44 | REQUIRE(it != map.end()); 45 | REQUIRE(it->second == 2); 46 | 47 | auto ret = map.try_emplace("tEst", 3); 48 | REQUIRE(ret.second == false); 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /cpp/server/tests/test_main.hpp: -------------------------------------------------------------------------------- 1 | #ifdef MSRV_OS_POSIX 2 | #include "../charset.hpp" 3 | #include 4 | 5 | #endif 6 | 7 | #ifdef MSRV_OS_WINDOWS 8 | #include "../safe_windows.h" 9 | #endif 10 | 11 | #include "../log.hpp" 12 | #include "project_info.hpp" 13 | 14 | namespace msrv { int testMain(int argc, char** argv); } 15 | 16 | int main(int argc, char* argv[]) 17 | { 18 | using namespace msrv; 19 | 20 | StderrLogger logger; 21 | LoggerScope loggerScope(&logger); 22 | 23 | #ifdef MSRV_OS_POSIX 24 | ::setlocale(LC_ALL, ""); 25 | ::setlocale(LC_NUMERIC, "C"); 26 | setLocaleCharset(); 27 | #endif 28 | 29 | return testMain(argc, argv); 30 | } 31 | -------------------------------------------------------------------------------- /js/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(CMAKE_FOLDER js) 2 | set(JS_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}") 3 | 4 | set(YARN_NAMES yarn yarnpkg) 5 | 6 | if(HOST_OS_WINDOWS) 7 | list(TRANSFORM YARN_NAMES APPEND .cmd) 8 | endif() 9 | 10 | find_program(YARN NAMES ${YARN_NAMES} REQUIRED) 11 | 12 | set( 13 | YARN_INTEGRITY 14 | ${CMAKE_CURRENT_SOURCE_DIR}/node_modules/.yarn-integrity 15 | ) 16 | 17 | set( 18 | PACKAGE_JSON_FILES 19 | ${CMAKE_CURRENT_SOURCE_DIR}/package.json 20 | ${CMAKE_CURRENT_SOURCE_DIR}/api_tests/package.json 21 | ${CMAKE_CURRENT_SOURCE_DIR}/client/package.json 22 | ${CMAKE_CURRENT_SOURCE_DIR}/webui/package.json 23 | ) 24 | 25 | add_custom_command( 26 | OUTPUT ${YARN_INTEGRITY} 27 | MAIN_DEPENDENCY ${CMAKE_CURRENT_SOURCE_DIR}/yarn.lock 28 | DEPENDS ${PACKAGE_JSON_FILES} 29 | COMMAND ${YARN} install --non-interactive 30 | COMMAND ${CMAKE_COMMAND} -E touch_nocreate ${YARN_INTEGRITY} 31 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 32 | VERBATIM 33 | ) 34 | 35 | add_custom_target(js_packages DEPENDS ${YARN_INTEGRITY}) 36 | 37 | if(ENABLE_TESTS) 38 | add_subdirectory(api_tests) 39 | endif() 40 | 41 | add_subdirectory(client) 42 | add_subdirectory(webui) 43 | -------------------------------------------------------------------------------- /js/api_tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(sources.cmake) 2 | 3 | add_custom_target( 4 | api_tests 5 | SOURCES ${API_TESTS_SOURCES} 6 | DEPENDS js_packages ${YARN_INTEGRITY} 7 | ) 8 | -------------------------------------------------------------------------------- /js/api_tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "beefweb-api-tests", 3 | "version": "0.11.0", 4 | "description": "Beefweb API tests", 5 | "main": "src/main.js", 6 | "type": "module", 7 | "repository": "https://github.com/hyperblast/beefweb", 8 | "author": "Hyperblast", 9 | "license": "MIT", 10 | "private": true, 11 | "scripts": { 12 | "test": "qunit src/main.js" 13 | }, 14 | "devDependencies": { 15 | "axios": "^0.21.2", 16 | "eventsource": "^2.0.2", 17 | "lodash": "^4.17.21", 18 | "mkdirp": "^1.0.4", 19 | "qunit": "^2.19.3", 20 | "rimraf": "^3.0.2", 21 | "tmp": "^0.2.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /js/api_tests/sources.cmake: -------------------------------------------------------------------------------- 1 | # Generated by js/update_sources.sh, do not edit directly 2 | set( 3 | API_TESTS_SOURCES 4 | package.json 5 | src/artwork_api_tests.js 6 | src/authentication_tests.js 7 | src/browser_api_tests.js 8 | src/client_config_api_tests.js 9 | src/deadbeef/player_controller.js 10 | src/deadbeef/test_context.js 11 | src/event_expectation.js 12 | src/foobar2000/player_controller.js 13 | src/foobar2000/test_context.js 14 | src/http_features_tests.js 15 | src/main.js 16 | src/outputs_api_tests.js 17 | src/permissions_tests.js 18 | src/play_queue_api_tests.js 19 | src/player_api_tests.js 20 | src/playlists_api_tests.js 21 | src/query_api_tests.js 22 | src/request_handler.js 23 | src/static_files_tests.js 24 | src/test_context.js 25 | src/test_env.js 26 | src/test_player_client.js 27 | src/utils.js 28 | ) 29 | -------------------------------------------------------------------------------- /js/api_tests/src/authentication_tests.js: -------------------------------------------------------------------------------- 1 | import q from 'qunit'; 2 | import lodash from 'lodash'; 3 | import { client, usePlayer } from './test_env.js'; 4 | 5 | const { startsWith } = lodash; 6 | 7 | const authUser = 'user1'; 8 | const authPassword = 'password1'; 9 | 10 | const pluginSettings = { 11 | authRequired: true, 12 | authUser, 13 | authPassword, 14 | }; 15 | 16 | const axiosConfig = { 17 | auth: { 18 | username: authUser, 19 | password: authPassword 20 | } 21 | }; 22 | 23 | function makeRequest(config) 24 | { 25 | const fullConfig = Object.assign({ 26 | auth: false, 27 | validateStatus: () => true, 28 | }, config); 29 | 30 | return client.handler.axios.get('api/player', fullConfig); 31 | } 32 | 33 | q.module('authentication', usePlayer({ pluginSettings, axiosConfig })); 34 | 35 | q.test('require auth', async assert => 36 | { 37 | const response = await makeRequest(); 38 | 39 | assert.equal(response.status, 401); 40 | assert.ok(response.data && response.data.error); 41 | assert.ok(startsWith(response.headers['www-authenticate'], 'Basic')); 42 | }); 43 | 44 | q.test('invalid auth', async assert => 45 | { 46 | const response = await makeRequest({ 47 | auth: { username: authUser, password: 'wrong' } 48 | }); 49 | 50 | assert.equal(response.status, 401); 51 | assert.ok(response.data && response.data.error); 52 | assert.ok(startsWith(response.headers['www-authenticate'], 'Basic')); 53 | }); 54 | 55 | q.test('valid auth', async assert => 56 | { 57 | const response = await makeRequest({ 58 | auth: { username: authUser, password: authPassword } 59 | }); 60 | 61 | assert.equal(response.status, 200); 62 | assert.ok(response.data && response.data.player); 63 | }); 64 | -------------------------------------------------------------------------------- /js/api_tests/src/client_config_api_tests.js: -------------------------------------------------------------------------------- 1 | import q from 'qunit'; 2 | import { client, usePlayer } from './test_env.js'; 3 | 4 | q.module('client config api', usePlayer()); 5 | 6 | q.test('client config', async assert => 7 | { 8 | const id = 'api_test_' + Math.round(1000000 * Math.random()); 9 | 10 | const config1 = await client.getClientConfig(id); 11 | assert.equal(config1, null); 12 | 13 | const value = { prop1: true, prop2: 'hello', prop3: { prop4: 123, prop5: [ 'item' ] } }; 14 | await client.setClientConfig(id, value); 15 | 16 | const config2 = await client.getClientConfig(id); 17 | assert.deepEqual(config2, value); 18 | 19 | await client.removeClientConfig(id); 20 | const config3 = await client.getClientConfig(id); 21 | assert.deepEqual(config3, null); 22 | }); 23 | -------------------------------------------------------------------------------- /js/api_tests/src/deadbeef/test_context.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { TestContextFactory } from '../test_context.js'; 3 | import PlayerController from './player_controller.js'; 4 | 5 | export class DeadbeefTestContextFactory extends TestContextFactory 6 | { 7 | createConfig() 8 | { 9 | const config = super.createConfig(); 10 | 11 | config.playerId = 'deadbeef'; 12 | 13 | const { BEEFWEB_TEST_DEADBEEF_VERSION } = process.env; 14 | const version = BEEFWEB_TEST_DEADBEEF_VERSION || 'v1.10'; 15 | 16 | config.playerDirBase = path.join(config.appsDir, 'deadbeef', version); 17 | 18 | config.pluginBuildDir = path.join( 19 | config.binaryDir, 20 | 'cpp', 21 | 'server', 22 | 'deadbeef'); 23 | 24 | config.pluginFiles = [ 25 | 'beefweb.so', 26 | 'ddb_gui_dummy.so', 27 | 'nullout2.so' 28 | ]; 29 | 30 | config.deadbeefSettings = { 31 | 'gui_plugin': 'dummy', 32 | 'output_plugin': 'Null output plugin v2', 33 | }; 34 | 35 | return config; 36 | } 37 | 38 | createOutputConfigs() 39 | { 40 | return { 41 | default: { typeId: 'nullout2', deviceId: 'default' }, 42 | alternate: [ 43 | { typeId: 'nullout', deviceId: 'default' }, 44 | { typeId: 'alsa', deviceId: 'null' }, 45 | ], 46 | } 47 | } 48 | 49 | createPlayer(config) 50 | { 51 | return new PlayerController(config); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /js/api_tests/src/foobar2000/test_context.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { TestContextFactory } from '../test_context.js'; 3 | import PlayerController from './player_controller.js'; 4 | 5 | export class Foobar2000TestContextFactory extends TestContextFactory 6 | { 7 | createConfig() 8 | { 9 | const config = super.createConfig(); 10 | 11 | config.playerId = 'foobar2000'; 12 | 13 | const { BEEFWEB_TEST_FOOBAR2000_VERSION } = process.env; 14 | config.playerVersion = BEEFWEB_TEST_FOOBAR2000_VERSION || 'v2.24-x64'; 15 | config.playerDir = path.join(config.appsDir, 'foobar2000', config.playerVersion); 16 | 17 | config.pluginBuildDir = path.join( 18 | config.binaryDir, 19 | 'cpp', 20 | 'server', 21 | 'foobar2000', 22 | config.buildType); 23 | 24 | config.pluginFile = 'foo_beefweb.dll'; 25 | 26 | return config; 27 | } 28 | 29 | createOutputConfigs() 30 | { 31 | return { 32 | default: { 33 | // Null Output 34 | typeId: 'output', 35 | deviceId: 'EEEB07DE-C2C8-44C2-985C-C85856D96DA1:5243F9AD-C84F-4723-8194-0788FC021BCC' 36 | }, 37 | 38 | alternate: [ 39 | { 40 | // Primary Sound Driver 41 | typeId: 'output', 42 | deviceId: 'D41D2423-FBB0-4635-B233-7054F79814AB:00000000-0000-0000-0000-000000000000' 43 | } 44 | ] 45 | } 46 | } 47 | 48 | createPlayer(config) 49 | { 50 | return new PlayerController(config); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /js/api_tests/src/http_features_tests.js: -------------------------------------------------------------------------------- 1 | import q from 'qunit'; 2 | import { client, tracks, usePlayer } from './test_env.js'; 3 | 4 | const expectedValue = 'Very Custom, Much Configurable, Wow'; 5 | 6 | const pluginSettings = { 7 | responseHeaders: { 8 | 'X-CustomHeader': expectedValue 9 | } 10 | }; 11 | 12 | q.module('http features', usePlayer({ pluginSettings })); 13 | 14 | q.test('custom headers', async assert => 15 | { 16 | const response = await client.handler.axios.get('/api/player'); 17 | 18 | assert.strictEqual(response.headers['x-customheader'], expectedValue); 19 | }); 20 | 21 | q.test('custom headers for async method', async assert => 22 | { 23 | const response = await client.handler.axios.post('/api/playlists/0/items/add', { items: [tracks.t1] }); 24 | 25 | assert.strictEqual(response.headers['x-customheader'], expectedValue); 26 | }); 27 | 28 | q.test('options method', async assert => 29 | { 30 | const response = await client.handler.axios.options('/api/player'); 31 | 32 | assert.strictEqual(response.headers['x-customheader'], expectedValue); 33 | }); 34 | -------------------------------------------------------------------------------- /js/api_tests/src/main.js: -------------------------------------------------------------------------------- 1 | import './artwork_api_tests.js'; 2 | import './authentication_tests.js'; 3 | import './browser_api_tests.js'; 4 | import './client_config_api_tests.js' 5 | import './http_features_tests.js'; 6 | import './outputs_api_tests.js'; 7 | import './permissions_tests.js'; 8 | import './player_api_tests.js'; 9 | import './playlists_api_tests.js'; 10 | import './play_queue_api_tests.js' 11 | import './query_api_tests.js'; 12 | import './static_files_tests.js'; 13 | -------------------------------------------------------------------------------- /js/api_tests/src/outputs_api_tests.js: -------------------------------------------------------------------------------- 1 | import q from 'qunit'; 2 | import { client, outputConfigs, usePlayer } from './test_env.js'; 3 | 4 | q.module('outputs api', usePlayer()); 5 | 6 | q.test('get outputs config', async assert => 7 | { 8 | const outputs = await client.getOutputs(); 9 | 10 | assert.ok(typeof outputs.supportsMultipleOutputTypes === 'boolean'); 11 | assert.deepEqual(outputs.active, outputConfigs.default); 12 | assert.ok(Array.isArray(outputs.types)); 13 | 14 | const type = outputs.types[0]; 15 | assert.ok(type.id); 16 | assert.ok(type.name); 17 | assert.ok(Array.isArray(type.devices)); 18 | 19 | const device = type.devices[0]; 20 | assert.ok(device.id); 21 | assert.ok(device.name); 22 | }); 23 | 24 | q.test('set output device', async assert => 25 | { 26 | for (const config of outputConfigs.alternate) 27 | { 28 | await client.setOutputDevice(config.typeId, config.deviceId); 29 | const output = (await client.getOutputs()).active; 30 | assert.equal(output.typeId, config.typeId); 31 | assert.equal(output.deviceId, config.deviceId); 32 | } 33 | }); 34 | -------------------------------------------------------------------------------- /js/api_tests/src/permissions_tests.js: -------------------------------------------------------------------------------- 1 | import q from 'qunit'; 2 | import { client, outputConfigs, usePlayer } from './test_env.js'; 3 | 4 | const pluginSettings = { 5 | permissions: { 6 | changePlaylists: false, 7 | changeOutput: false, 8 | changeClientConfig: false, 9 | } 10 | }; 11 | 12 | const resetOptions = { 13 | playlists: false, 14 | output: false 15 | }; 16 | 17 | const axiosConfig = { 18 | validateStatus: () => true, 19 | }; 20 | 21 | function post(path, data) 22 | { 23 | return client.handler.axios.post(path, data, axiosConfig); 24 | } 25 | 26 | q.module('permissions', usePlayer({ pluginSettings, resetOptions })); 27 | 28 | q.test('get permissions', async assert => 29 | { 30 | const state = await client.getPlayerState(); 31 | assert.deepEqual(state.permissions, pluginSettings.permissions); 32 | }); 33 | 34 | q.test('change playlist', async assert => 35 | { 36 | const response = await post('/api/playlists/add'); 37 | assert.equal(response.status, 403); 38 | }); 39 | 40 | q.test('change output', async assert => 41 | { 42 | const response = await post('/api/outputs/active', outputConfigs.alternate[0]); 43 | assert.equal(response.status, 403); 44 | }); 45 | 46 | q.test('change client config', async assert => 47 | { 48 | const response = await post('/api/clientconfig/perm_test', {}); 49 | assert.equal(response.status, 403); 50 | }); 51 | -------------------------------------------------------------------------------- /js/api_tests/src/test_env.js: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | import { Foobar2000TestContextFactory } from './foobar2000/test_context.js' 3 | import { DeadbeefTestContextFactory } from './deadbeef/test_context.js' 4 | 5 | function getContextFactory() 6 | { 7 | switch (os.type()) 8 | { 9 | case 'Windows_NT': 10 | return new Foobar2000TestContextFactory(); 11 | 12 | default: 13 | return new DeadbeefTestContextFactory(); 14 | } 15 | } 16 | 17 | export const context = getContextFactory().createContext(); 18 | export const tracks = context.tracks; 19 | export const config = context.config; 20 | export const client = context.client; 21 | export const outputConfigs = context.outputConfigs; 22 | 23 | export function usePlayer(options) 24 | { 25 | return { 26 | before: () => context.beginSuite(options), 27 | after: () => context.endSuite(), 28 | beforeEach: () => context.beginTest(), 29 | afterEach: () => context.endTest(), 30 | }; 31 | } -------------------------------------------------------------------------------- /js/api_tests/src/utils.js: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | 3 | export function sleep(timeout) 4 | { 5 | return new Promise(resolve => setTimeout(resolve, timeout)); 6 | } 7 | 8 | export async function waitUntil(check, delay = 200, retry = 10) 9 | { 10 | let ret; 11 | 12 | ret = await check(); 13 | 14 | if (ret) 15 | return ret; 16 | 17 | for (let i = 1; i < retry; i++) 18 | { 19 | await sleep(delay); 20 | 21 | ret = await check(); 22 | 23 | if (ret) 24 | return ret; 25 | } 26 | 27 | return null; 28 | } 29 | 30 | export function waitForExit(process, timeout = -1) 31 | { 32 | return new Promise(resolve => 33 | { 34 | if (timeout >= 0) 35 | setTimeout(() => resolve(false), timeout); 36 | 37 | process.on('exit', () => resolve(true)); 38 | }); 39 | } 40 | 41 | function getNormalizePathFunction() 42 | { 43 | switch (os.type()) 44 | { 45 | case 'Windows_NT': 46 | return p => p.toUpperCase(); 47 | default: 48 | return p => p; 49 | } 50 | } 51 | 52 | export const normalizePath = getNormalizePathFunction(); 53 | 54 | export function pathsEqual(p1, p2) 55 | { 56 | return normalizePath(p1) === normalizePath(p2); 57 | } 58 | 59 | export function pathCollectionsEqual(paths1, paths2, ignoreOrder = false) 60 | { 61 | if (paths1.length !== paths2.length) 62 | return false; 63 | 64 | const items1 = paths1.map(normalizePath); 65 | const items2 = paths2.map(normalizePath); 66 | 67 | if (ignoreOrder) 68 | { 69 | items1.sort(); 70 | items2.sort(); 71 | } 72 | 73 | let i = 0; 74 | for (let item of items1) 75 | { 76 | if (item !== items2[i++]) 77 | return false; 78 | } 79 | 80 | return true; 81 | } -------------------------------------------------------------------------------- /js/api_tests/tracks/cover-white.png.hidden: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperblast/beefweb/2bc397b09235a6a939a0af9a5927f2c644b52e56/js/api_tests/tracks/cover-white.png.hidden -------------------------------------------------------------------------------- /js/api_tests/tracks/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperblast/beefweb/2bc397b09235a6a939a0af9a5927f2c644b52e56/js/api_tests/tracks/cover.png -------------------------------------------------------------------------------- /js/api_tests/tracks/empty/no-music-files: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperblast/beefweb/2bc397b09235a6a939a0af9a5927f2c644b52e56/js/api_tests/tracks/empty/no-music-files -------------------------------------------------------------------------------- /js/api_tests/tracks/subdir/track2.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperblast/beefweb/2bc397b09235a6a939a0af9a5927f2c644b52e56/js/api_tests/tracks/subdir/track2.flac -------------------------------------------------------------------------------- /js/api_tests/tracks/subdir/track3.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperblast/beefweb/2bc397b09235a6a939a0af9a5927f2c644b52e56/js/api_tests/tracks/subdir/track3.flac -------------------------------------------------------------------------------- /js/api_tests/tracks/track1.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperblast/beefweb/2bc397b09235a6a939a0af9a5927f2c644b52e56/js/api_tests/tracks/track1.flac -------------------------------------------------------------------------------- /js/api_tests/tracks/track2.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperblast/beefweb/2bc397b09235a6a939a0af9a5927f2c644b52e56/js/api_tests/tracks/track2.flac -------------------------------------------------------------------------------- /js/api_tests/webroot/file.css: -------------------------------------------------------------------------------- 1 | file.css 2 | -------------------------------------------------------------------------------- /js/api_tests/webroot/file.htm: -------------------------------------------------------------------------------- 1 | file.htm 2 | -------------------------------------------------------------------------------- /js/api_tests/webroot/file.html: -------------------------------------------------------------------------------- 1 | file.html 2 | -------------------------------------------------------------------------------- /js/api_tests/webroot/file.jpeg: -------------------------------------------------------------------------------- 1 | file.jpeg 2 | -------------------------------------------------------------------------------- /js/api_tests/webroot/file.jpg: -------------------------------------------------------------------------------- 1 | file.jpg 2 | -------------------------------------------------------------------------------- /js/api_tests/webroot/file.js: -------------------------------------------------------------------------------- 1 | file.js 2 | -------------------------------------------------------------------------------- /js/api_tests/webroot/file.png: -------------------------------------------------------------------------------- 1 | file.png 2 | -------------------------------------------------------------------------------- /js/api_tests/webroot/file.svg: -------------------------------------------------------------------------------- 1 | file.svg 2 | -------------------------------------------------------------------------------- /js/api_tests/webroot/file.txt: -------------------------------------------------------------------------------- 1 | file.txt 2 | -------------------------------------------------------------------------------- /js/api_tests/webroot/index.html: -------------------------------------------------------------------------------- 1 | index.html 2 | -------------------------------------------------------------------------------- /js/api_tests/webroot/subdir/file.html: -------------------------------------------------------------------------------- 1 | subdir/file.html 2 | -------------------------------------------------------------------------------- /js/api_tests/webroot/subdir/index.html: -------------------------------------------------------------------------------- 1 | subdir/index.html 2 | -------------------------------------------------------------------------------- /js/client/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(sources.cmake) 2 | 3 | add_custom_target( 4 | js_client 5 | SOURCES ${JS_CLIENT_SOURCES} 6 | DEPENDS js_packages ${YARN_INTEGRITY} 7 | ) 8 | -------------------------------------------------------------------------------- /js/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "beefweb-client", 3 | "version": "0.11.0", 4 | "description": "Beefweb client library for browser and node", 5 | "main": "src/index.js", 6 | "type": "module", 7 | "repository": "https://github.com/hyperblast/beefweb", 8 | "author": "Hyperblast", 9 | "license": "MIT", 10 | "private": true, 11 | "scripts": { 12 | "build": "webpack-cli" 13 | }, 14 | "devDependencies": { 15 | "webpack": "^5.76.0", 16 | "webpack-cli": "^4.10.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /js/client/sources.cmake: -------------------------------------------------------------------------------- 1 | # Generated by js/update_sources.sh, do not edit directly 2 | set( 3 | JS_CLIENT_SOURCES 4 | package.json 5 | webpack.config.js 6 | src/enums.js 7 | src/index.js 8 | src/player_client.js 9 | src/utils.js 10 | ) 11 | -------------------------------------------------------------------------------- /js/client/src/enums.js: -------------------------------------------------------------------------------- 1 | export const ErrorType = Object.freeze({ 2 | none: 'none', 3 | unknown: 'unknown', 4 | internal: 'internal', 5 | invalidParam: 'invalidParam', 6 | invalidState: 'invalidState', 7 | notFound: 'notFound', 8 | accessDenied: 'accessDenied' 9 | }); 10 | 11 | export const SwitchParam = Object.freeze({ 12 | toggle: 'toggle' 13 | }); 14 | 15 | export const PlaybackState = Object.freeze({ 16 | stopped: 'stopped', 17 | playing: 'playing', 18 | paused: 'paused' 19 | }); 20 | -------------------------------------------------------------------------------- /js/client/src/index.js: -------------------------------------------------------------------------------- 1 | export { ErrorType, PlaybackState, SwitchParam } from './enums.js' 2 | export { formatQueryString, PlayerClientError } from './utils.js' 3 | export { default as PlayerClient } from './player_client.js' 4 | -------------------------------------------------------------------------------- /js/client/webpack.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { fileURLToPath } from 'url' 3 | import webpack from 'webpack' 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = path.dirname(__filename); 7 | 8 | function configCommon(config) 9 | { 10 | config.entry.push('./src/index.js'); 11 | 12 | config.output.path = path.join(__dirname, 'dist', 'umd'); 13 | config.output.libraryTarget = 'umd'; 14 | config.output.library = { 15 | amd: 'beefweb-client', 16 | commonjs: 'beefweb-client', 17 | root: 'Beefweb', 18 | }; 19 | } 20 | 21 | function configDebug(config) 22 | { 23 | config.mode = 'none'; 24 | config.output.filename = 'beefweb-client.dev.js' 25 | } 26 | 27 | function configRelease(config) 28 | { 29 | config.mode = 'production'; 30 | config.output.filename = 'beefweb-client.prod.js' 31 | } 32 | 33 | function makeTarget(configTarget) 34 | { 35 | const config = { 36 | entry: [], 37 | output: {}, 38 | plugins: [], 39 | module: { rules: [] }, 40 | resolve: { alias: {} }, 41 | node: {}, 42 | externals: {}, 43 | }; 44 | 45 | configCommon(config); 46 | configTarget(config); 47 | 48 | return config; 49 | } 50 | 51 | export default function() 52 | { 53 | return [ 54 | makeTarget(configDebug), 55 | makeTarget(configRelease), 56 | ]; 57 | }; 58 | -------------------------------------------------------------------------------- /js/config.mjs: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import { fileURLToPath } from 'url' 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = path.dirname(__filename); 7 | 8 | function toAbsolutePath(p) 9 | { 10 | return path.isAbsolute(p) ? p : path.join(path.dirname(__dirname), p); 11 | } 12 | 13 | export function getBinaryDir(buildType) 14 | { 15 | const binaryDir = process.env.BEEFWEB_BINARY_DIR; 16 | 17 | if (binaryDir) 18 | return toAbsolutePath(binaryDir); 19 | 20 | const binaryDirBase = process.env.BEEFWEB_BINARY_DIR_BASE || 'build'; 21 | 22 | return path.join(toAbsolutePath(binaryDirBase), buildType); 23 | } 24 | -------------------------------------------------------------------------------- /js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "workspaces": [ 4 | "api_tests", 5 | "client", 6 | "webui" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /js/update_sources.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Regenerate sources.cmake for each JS project 4 | 5 | set -e 6 | 7 | root_dir=`readlink -f "$(dirname $0)"` 8 | 9 | function echo_if_exists 10 | { 11 | if [ -f "$1" ]; then 12 | echo "$1" 13 | fi 14 | } 15 | 16 | function update 17 | { 18 | cd "$root_dir/$1" 19 | 20 | output=sources.cmake 21 | 22 | ( 23 | echo "# Generated by js/update_sources.sh, do not edit directly" 24 | echo "set(" 25 | echo "$2_SOURCES" 26 | echo "package.json" 27 | echo_if_exists .babelrc 28 | echo_if_exists webpack.config.js 29 | find src -type f | LC_ALL=C sort 30 | echo ")" 31 | ) > $output 32 | } 33 | 34 | update api_tests API_TESTS 35 | update client JS_CLIENT 36 | update webui WEBUI 37 | -------------------------------------------------------------------------------- /js/webui/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /js/webui/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(sources.cmake) 2 | include(../client/sources.cmake) 3 | 4 | list(TRANSFORM JS_CLIENT_SOURCES PREPEND "../client/") 5 | 6 | set( 7 | WEBUI_OUTPUT_FILES 8 | ${WEBUI_OUTPUT_DIR}/bundle.js 9 | ) 10 | 11 | set( 12 | WEBUI_BUILD_ARGS 13 | --env "outputDir=${WEBUI_OUTPUT_DIR}" 14 | --env "buildType=$" 15 | --env "$,tests,noTests>" 16 | ) 17 | 18 | if(CMAKE_GENERATOR MATCHES "Visual Studio") 19 | set(WEBUI_BUILD_ARGS ${WEBUI_BUILD_ARGS} --no-color) 20 | endif() 21 | 22 | add_custom_command( 23 | OUTPUT ${WEBUI_OUTPUT_FILES} 24 | DEPENDS ${WEBUI_SOURCES} ${JS_CLIENT_SOURCES} ${YARN_INTEGRITY} js_packages 25 | COMMAND ${YARN} build ${WEBUI_BUILD_ARGS} 26 | COMMAND ${CMAKE_COMMAND} -E touch_nocreate ${WEBUI_OUTPUT_FILES} 27 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 28 | VERBATIM 29 | ) 30 | 31 | add_custom_target( 32 | webui ALL 33 | DEPENDS ${WEBUI_OUTPUT_FILES} 34 | SOURCES ${WEBUI_SOURCES} 35 | ) 36 | 37 | set( 38 | LICENSES_FILE 39 | ${WEBUI_OUTPUT_DIR}/${WEBUI_LICENSES_FILE} 40 | ) 41 | 42 | add_custom_command( 43 | OUTPUT ${LICENSES_FILE} 44 | DEPENDS ${EXTLIB_LICENSES_FILE} ${YARN_INTEGRITY} js_packages 45 | COMMAND ${CMAKE_COMMAND} -E make_directory ${WEBUI_OUTPUT_DIR} 46 | COMMAND ${CMAKE_COMMAND} -E copy ${EXTLIB_LICENSES_FILE} ${LICENSES_FILE} 47 | COMMAND ${YARN} licenses generate-disclaimer --silent --prod --cwd ${CMAKE_CURRENT_SOURCE_DIR} >> ${LICENSES_FILE} 48 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 49 | VERBATIM 50 | ) 51 | 52 | add_custom_target( 53 | licenses_file ALL 54 | DEPENDS ${LICENSES_FILE} 55 | ) 56 | 57 | function(install_webui COMP DIR) 58 | install( 59 | DIRECTORY "${WEBUI_OUTPUT_DIR}/" 60 | DESTINATION "${DIR}/${WEBUI_ROOT}" 61 | COMPONENT ${COMP} 62 | PATTERN tests EXCLUDE 63 | PATTERN sandbox EXCLUDE 64 | ) 65 | endfunction() 66 | 67 | if(ENABLE_DEADBEEF) 68 | install_webui(deadbeef_plugin ${DEADBEEF_LIB_DIR}) 69 | endif() 70 | 71 | if(ENABLE_FOOBAR2000) 72 | install_webui(foobar2000_plugin .) 73 | endif() 74 | -------------------------------------------------------------------------------- /js/webui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "beefweb-webui", 3 | "version": "0.11.0", 4 | "description": "Beefweb user interface", 5 | "main": "src/index.js", 6 | "type": "module", 7 | "private": true, 8 | "repository": "https://github.com/hyperblast/beefweb", 9 | "author": "Hyperblast", 10 | "license": "MIT", 11 | "scripts": { 12 | "build": "webpack-cli", 13 | "watch": "webpack-cli --watch" 14 | }, 15 | "dependencies": { 16 | "detect-it": "^4.0.1", 17 | "lodash": "^4.17.21", 18 | "navigo": "^7.1.3", 19 | "normalize.css": "^8.0.1", 20 | "open-iconic": "^1.1.1", 21 | "prop-types": "^15.8.1", 22 | "react": "^17.0.2", 23 | "react-dom": "^17.0.2", 24 | "react-modal": "^3.16.1", 25 | "react-sortable-hoc": "^2.0.0", 26 | "wolfy87-eventemitter": "^5.2.9" 27 | }, 28 | "devDependencies": { 29 | "@babel/core": "^7.20.12", 30 | "@babel/preset-react": "^7.18.6", 31 | "babel-loader": "^8.3.0", 32 | "css-loader": "^6.7.3", 33 | "css-minimizer-webpack-plugin": "^3.4.1", 34 | "mini-css-extract-plugin": "^2.7.2", 35 | "file-loader": "^6.2.0", 36 | "html-webpack-plugin": "^5.5.0", 37 | "less": "^4.1.3", 38 | "less-loader": "^10.2.0", 39 | "terser-webpack-plugin": "5.3.6", 40 | "qunit": "^2.19.3", 41 | "style-loader": "^3.3.1", 42 | "url-loader": "^4.1.1", 43 | "webpack": "^5.76.0", 44 | "webpack-bundle-analyzer": "^4.7.0", 45 | "webpack-cli": "^4.10.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /js/webui/sources.cmake: -------------------------------------------------------------------------------- 1 | # Generated by js/update_sources.sh, do not edit directly 2 | set( 3 | WEBUI_SOURCES 4 | package.json 5 | .babelrc 6 | webpack.config.js 7 | src/about_box.js 8 | src/album_art_viewer.js 9 | src/app.js 10 | src/app_model.js 11 | src/columns.js 12 | src/columns_settings.js 13 | src/columns_settings_menu.js 14 | src/columns_settings_model.js 15 | src/control_bar.js 16 | src/css_settings_controller.js 17 | src/data_source.js 18 | src/data_table.js 19 | src/defaults_settings.js 20 | src/dialogs.js 21 | src/dom_utils.js 22 | src/dropdown.js 23 | src/elements.js 24 | src/file_browser.js 25 | src/file_browser_header.js 26 | src/file_browser_model.js 27 | src/general_settings.js 28 | src/index.html 29 | src/index.js 30 | src/loader.gif 31 | src/media_size_controller.js 32 | src/media_theme_controller.js 33 | src/model_binding.js 34 | src/navigation_model.js 35 | src/notification_group.js 36 | src/notification_model.js 37 | src/output_settings.js 38 | src/output_settings_model.js 39 | src/play_queue_model.js 40 | src/playback_control.js 41 | src/playback_info_bar.js 42 | src/player_features.js 43 | src/player_model.js 44 | src/playlist_content.js 45 | src/playlist_menu.js 46 | src/playlist_model.js 47 | src/playlist_switcher.js 48 | src/position_control.js 49 | src/request_handler.js 50 | src/sandbox/index.js 51 | src/scroll_manager.js 52 | src/service_context.js 53 | src/setting_editor.js 54 | src/settings_content.js 55 | src/settings_header.js 56 | src/settings_model.js 57 | src/settings_model_base.js 58 | src/settings_store.js 59 | src/status_bar.js 60 | src/style.less 61 | src/tests/index.html 62 | src/tests/index.js 63 | src/timer.js 64 | src/touch_mode_controller.js 65 | src/urls.js 66 | src/utils.js 67 | src/view_switcher.js 68 | src/volume_control.js 69 | src/window_controller.js 70 | ) 71 | -------------------------------------------------------------------------------- /js/webui/src/album_art_viewer.js: -------------------------------------------------------------------------------- 1 | import ModelBinding from './model_binding.js'; 2 | import ServiceContext from './service_context.js'; 3 | import React from 'react'; 4 | import { PlaybackState } from 'beefweb-client'; 5 | import { bindHandlers } from './utils.js'; 6 | import { Icon } from './elements.js'; 7 | 8 | class AlbumArtViewer_ extends React.PureComponent 9 | { 10 | static contextType = ServiceContext; 11 | 12 | constructor(props, context) 13 | { 14 | super(props, context); 15 | 16 | this.state = this.getStateFromModel(); 17 | this.state.errorFilePath = ''; 18 | 19 | bindHandlers(this); 20 | } 21 | 22 | getStateFromModel() 23 | { 24 | const { playbackState, activeItem } = this.context.playerModel; 25 | 26 | return { 27 | isPlaying: playbackState !== PlaybackState.stopped, 28 | filePath: activeItem.columns[2] || '', 29 | }; 30 | } 31 | 32 | handleImageError() 33 | { 34 | this.setState({ errorFilePath: this.state.filePath }); 35 | } 36 | 37 | render() 38 | { 39 | const { isPlaying, filePath, errorFilePath } = this.state; 40 | const hasAlbumArt = isPlaying && filePath !== errorFilePath; 41 | 42 | if (!hasAlbumArt) 43 | { 44 | return ( 45 |
46 | 47 |
48 | ); 49 | } 50 | 51 | const url = `/api/artwork/current?f=${encodeURIComponent(filePath)}`; 52 | 53 | return ( 54 |
55 | Loading album art... 56 |
57 | ); 58 | } 59 | } 60 | 61 | export default ModelBinding(AlbumArtViewer_, { 62 | playerModel: 'change' 63 | }); 64 | -------------------------------------------------------------------------------- /js/webui/src/app_model.js: -------------------------------------------------------------------------------- 1 | import DataSource from './data_source.js' 2 | import PlayerModel from './player_model.js' 3 | import PlaylistModel from './playlist_model.js' 4 | import FileBrowserModel from './file_browser_model.js' 5 | import SettingsModel from './settings_model.js' 6 | import NotificationModel from './notification_model.js' 7 | import ScrollManager from './scroll_manager.js' 8 | import NavigationModel from './navigation_model.js'; 9 | import ColumnsSettingsModel from './columns_settings_model.js'; 10 | import PlayQueueModel from './play_queue_model.js'; 11 | import OutputSettingsModel from './output_settings_model.js'; 12 | 13 | export default class AppModel 14 | { 15 | constructor(client, settingsStore) 16 | { 17 | this.client = client; 18 | this.dataSource = new DataSource(client); 19 | this.settingsModel = new SettingsModel(settingsStore, client); 20 | this.columnsSettingsModel = new ColumnsSettingsModel(this.settingsModel); 21 | this.playerModel = new PlayerModel(client, this.dataSource, this.settingsModel); 22 | this.playlistModel = new PlaylistModel(client, this.dataSource, this.settingsModel); 23 | this.playQueueModel = new PlayQueueModel(client, this.dataSource); 24 | this.outputSettingsModel = new OutputSettingsModel(client, this.dataSource); 25 | this.fileBrowserModel = new FileBrowserModel(client); 26 | this.notificationModel = new NotificationModel(); 27 | this.navigationModel = new NavigationModel(); 28 | this.scrollManager = new ScrollManager(); 29 | 30 | Object.freeze(this); 31 | } 32 | 33 | load() 34 | { 35 | return this.settingsModel.initialize().then(() => { 36 | this.notificationModel.load(); 37 | this.columnsSettingsModel.load(); 38 | }); 39 | } 40 | 41 | start() 42 | { 43 | this.playerModel.start(); 44 | this.playlistModel.start(); 45 | this.playQueueModel.start(); 46 | this.outputSettingsModel.start(); 47 | this.dataSource.start(); 48 | this.fileBrowserModel.reload(); 49 | this.notificationModel.start(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /js/webui/src/control_bar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PlaybackControl from './playback_control.js' 3 | import PositionControl from './position_control.js' 4 | import { VolumeControl, VolumeControlButton } from './volume_control.js' 5 | import { ViewSwitcher, ViewSwitcherButton } from './view_switcher.js' 6 | import ServiceContext from "./service_context.js"; 7 | import ModelBinding from "./model_binding.js"; 8 | import { MediaSize } from "./settings_model.js"; 9 | 10 | class ControlBar_ extends React.PureComponent 11 | { 12 | static contextType = ServiceContext; 13 | 14 | constructor(props, context) 15 | { 16 | super(props, context); 17 | 18 | this.state = this.getStateFromModel(); 19 | } 20 | 21 | getStateFromModel() 22 | { 23 | return { 24 | inlineMode: this.context.settingsModel.mediaSizeUp(MediaSize.medium) 25 | }; 26 | } 27 | 28 | render() 29 | { 30 | const { inlineMode } = this.state; 31 | 32 | return ( 33 |
34 | 35 | 36 | { 37 | inlineMode 38 | ? ( 39 | <> 40 | 41 | 42 | ) 43 | : ( 44 |
45 | 46 | 47 |
48 | ) 49 | } 50 |
51 | ); 52 | } 53 | } 54 | 55 | export const ControlBar = ModelBinding(ControlBar_, { settingsModel: 'mediaSizeChange' }); 56 | -------------------------------------------------------------------------------- /js/webui/src/css_settings_controller.js: -------------------------------------------------------------------------------- 1 | import { MediaSize } from './settings_model.js' 2 | 3 | const settingClassPrefix = 'st-'; 4 | 5 | function makeSettingClass(name, value) 6 | { 7 | name = name.toLowerCase(); 8 | 9 | if (value === true) 10 | return `${settingClassPrefix}${name}`; 11 | 12 | if (value === false) 13 | return `${settingClassPrefix}no-${name}`; 14 | 15 | value = String(value).toLowerCase(); 16 | 17 | return `${settingClassPrefix}${name}-${value}`; 18 | } 19 | 20 | export default class CssSettingsController 21 | { 22 | constructor(settingsModel) 23 | { 24 | this.settingsModel = settingsModel; 25 | } 26 | 27 | start() 28 | { 29 | this.settingsModel.on('change', this.update.bind(this)); 30 | this.update(); 31 | } 32 | 33 | update() 34 | { 35 | const rootElement = document.documentElement; 36 | 37 | const classNames = rootElement.className 38 | .split(' ') 39 | .filter(i => i !== '' && !i.startsWith(settingClassPrefix)); 40 | 41 | this.addSettingClasses(classNames); 42 | this.addMediaSizeClasses(classNames); 43 | 44 | rootElement.className = classNames.join(' '); 45 | } 46 | 47 | addSettingClasses(classNames) 48 | { 49 | const { values, metadata } = this.settingsModel; 50 | 51 | for (let key of Object.keys(values)) 52 | { 53 | const value = values[key]; 54 | 55 | if (metadata[key].cssVisible && value !== undefined) 56 | classNames.push(makeSettingClass(key, value)); 57 | } 58 | } 59 | 60 | addMediaSizeClasses(classNames) 61 | { 62 | for (let size of Object.keys(MediaSize)) 63 | { 64 | if (this.settingsModel.mediaSizeUp(size)) 65 | classNames.push(`${settingClassPrefix}mediasize-${size}-up`); 66 | 67 | if (this.settingsModel.mediaSizeDown(size)) 68 | classNames.push(`${settingClassPrefix}mediasize-${size}-down`); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /js/webui/src/dom_utils.js: -------------------------------------------------------------------------------- 1 | import { once } from './utils.js' 2 | 3 | export const getScrollBarSize = once(() => 4 | { 5 | // Based on https://github.com/sonicdoe/measure-scrollbar/blob/master/index.js 6 | 7 | const div = document.createElement('div'); 8 | 9 | div.style.width = '100px'; 10 | div.style.height = '100px'; 11 | div.style.overflow = 'scroll'; 12 | div.style.position = 'absolute'; 13 | div.style.top = '-9999px'; 14 | 15 | document.body.appendChild(div); 16 | const size = div.offsetWidth - div.clientWidth; 17 | document.body.removeChild(div); 18 | 19 | return size; 20 | }); 21 | 22 | let nextElementId = 0; 23 | 24 | export function generateElementId(prefix) 25 | { 26 | const id = nextElementId++; 27 | 28 | return `${prefix}-id${id}`; 29 | } 30 | 31 | export function addStyleSheet(value) 32 | { 33 | const element = document.createElement('style'); 34 | element.type = 'text/css'; 35 | element.innerText = value; 36 | document.head.appendChild(element); 37 | return element; 38 | } 39 | 40 | export function makeClassName(classes) 41 | { 42 | const parts = []; 43 | 44 | if (Array.isArray(classes)) 45 | { 46 | for (let cls of classes) 47 | { 48 | if (cls) 49 | parts.push(cls); 50 | } 51 | } 52 | else 53 | { 54 | for (let cls of Object.keys(classes)) 55 | { 56 | if (classes[cls]) 57 | parts.push(cls); 58 | } 59 | } 60 | 61 | return parts.join(' '); 62 | } 63 | 64 | export function getFontSize() 65 | { 66 | return parseFloat(getComputedStyle(document.documentElement).fontSize); 67 | } 68 | -------------------------------------------------------------------------------- /js/webui/src/general_settings.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import SettingEditor from './setting_editor.js'; 3 | import ModelBinding from './model_binding.js'; 4 | import ServiceContext from './service_context.js'; 5 | import { MediaSize } from './settings_model.js'; 6 | 7 | class GeneralSettings extends React.PureComponent 8 | { 9 | static contextType = ServiceContext; 10 | 11 | constructor(props, context) 12 | { 13 | super(props, context); 14 | 15 | this.state = this.getStateFromModel(); 16 | } 17 | 18 | getStateFromModel() 19 | { 20 | const { showPlaybackInfo } = this.context.settingsModel; 21 | 22 | return { 23 | showPlaybackInfo, 24 | showFullWidth: this.context.settingsModel.mediaSizeUp(MediaSize.large), 25 | }; 26 | } 27 | 28 | render() 29 | { 30 | const { showPlaybackInfo, showFullWidth } = this.state; 31 | 32 | return ( 33 |
34 | 35 | 36 | 37 | { 38 | showFullWidth ? : null 39 | } 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | ); 49 | } 50 | } 51 | 52 | export default ModelBinding(GeneralSettings, { 53 | settingsModel: ['showPlaybackInfoChange', 'mediaSizeChange'], 54 | }); 55 | -------------------------------------------------------------------------------- /js/webui/src/index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | Loading... 9 | 10 | 11 |
12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /js/webui/src/loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperblast/beefweb/2bc397b09235a6a939a0af9a5927f2c644b52e56/js/webui/src/loader.gif -------------------------------------------------------------------------------- /js/webui/src/media_theme_controller.js: -------------------------------------------------------------------------------- 1 | import { UITheme } from "./settings_model.js"; 2 | 3 | export default class MediaThemeController 4 | { 5 | constructor(settingsModel) 6 | { 7 | this.settingsModel = settingsModel; 8 | this.darkThemeQuery = window.matchMedia('(prefers-color-scheme: dark)'); 9 | this.update = this.update.bind(this); 10 | } 11 | 12 | start() 13 | { 14 | this.settingsModel.on('uiThemePreferenceChange', this.update); 15 | this.darkThemeQuery.addEventListener('change', this.update); 16 | this.update(); 17 | } 18 | 19 | update() 20 | { 21 | this.settingsModel.uiTheme = this.settingsModel.uiThemePreference === UITheme.system 22 | ? (this.darkThemeQuery.matches ? UITheme.dark : UITheme.light) 23 | : this.settingsModel.uiThemePreference; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /js/webui/src/navigation_model.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'wolfy87-eventemitter' 2 | 3 | export const View = Object.freeze({ 4 | playlist: 'playlist', 5 | fileBrowser: 'fileBrowser', 6 | albumArt: 'albumArt', 7 | settings: 'settings', 8 | notFound: 'notFound' 9 | }); 10 | 11 | export const SettingsView = Object.freeze({ 12 | general: 'general', 13 | columns: 'columns', 14 | output: 'output', 15 | defaults: 'defaults', 16 | about: 'about', 17 | }); 18 | 19 | export default class NavigationModel extends EventEmitter 20 | { 21 | constructor() 22 | { 23 | super(); 24 | 25 | this.view = View.playlist; 26 | this.settingsView = SettingsView.general; 27 | 28 | this.defineEvent('viewChange'); 29 | this.defineEvent('settingsViewChange'); 30 | } 31 | 32 | setView(view) 33 | { 34 | if (view === this.view) 35 | return; 36 | 37 | this.view = view; 38 | this.emit('viewChange'); 39 | } 40 | 41 | setSettingsView(view) 42 | { 43 | if (view === this.settingsView) 44 | return; 45 | 46 | this.settingsView = view; 47 | this.emit('settingsViewChange'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /js/webui/src/notification_model.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom' 3 | import NotificationGroup from './notification_group.js' 4 | import { bindHandlers } from './utils.js'; 5 | import Timer from './timer.js'; 6 | 7 | const maxNotifications = 5; 8 | const timeToLive = 3000; 9 | 10 | export default class NotificationModel 11 | { 12 | constructor() 13 | { 14 | this.items = []; 15 | 16 | bindHandlers(this); 17 | 18 | this.timer = new Timer(this.handleTimeout, timeToLive / 2); 19 | } 20 | 21 | notifyAddItem(path) 22 | { 23 | this.notify('Adding track:', path); 24 | } 25 | 26 | notifyAddDirectory(path) 27 | { 28 | this.notify('Adding directory:', path); 29 | } 30 | 31 | notify(title, message) 32 | { 33 | if (this.items.length >= maxNotifications) 34 | this.items.splice(0, 1); 35 | 36 | this.items.push({ title, message, timeout: Date.now() + timeToLive }); 37 | this.update(); 38 | } 39 | 40 | load() 41 | { 42 | this.container = document.getElementById('notification-container'); 43 | } 44 | 45 | start() 46 | { 47 | this.timer.restart(); 48 | } 49 | 50 | handleCloseQuery(index) 51 | { 52 | this.items.splice(index, 1); 53 | this.update(); 54 | } 55 | 56 | handleTimeout(delta, now) 57 | { 58 | const isAlive = i => i.timeout >= now; 59 | 60 | if (this.items.every(isAlive)) 61 | return; 62 | 63 | this.items = this.items.filter(isAlive); 64 | this.update(); 65 | } 66 | 67 | update() 68 | { 69 | const box = ( 70 | 73 | ); 74 | 75 | ReactDom.render(box, this.container); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /js/webui/src/playback_info_bar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ModelBinding from './model_binding.js'; 3 | import { PlaybackState } from 'beefweb-client'; 4 | import ServiceContext from './service_context.js'; 5 | 6 | class PlaybackInfoBar_ extends React.PureComponent 7 | { 8 | static contextType = ServiceContext; 9 | 10 | constructor(props, context) 11 | { 12 | super(props, context); 13 | 14 | this.state = this.getStateFromModel(); 15 | } 16 | 17 | getStateFromModel() 18 | { 19 | const { playerModel } = this.context; 20 | 21 | const title = playerModel.playbackState !== PlaybackState.stopped 22 | ? (playerModel.activeItem.columns[1] || '') 23 | : playerModel.info.title; 24 | 25 | return { title }; 26 | } 27 | 28 | render() 29 | { 30 | const { title } = this.state; 31 | 32 | return
{title}
; 33 | } 34 | } 35 | 36 | const PlaybackInfoBar = ModelBinding(PlaybackInfoBar_, { 37 | playerModel: 'change', 38 | settingsModel: 'change' 39 | }); 40 | 41 | export default PlaybackInfoBar; 42 | -------------------------------------------------------------------------------- /js/webui/src/player_features.js: -------------------------------------------------------------------------------- 1 | export const defaultPlayerFeatures = Object.freeze({ 2 | showTotalTime: false, 3 | prependToQueue: false, 4 | }); 5 | 6 | function defineFeatures(obj) 7 | { 8 | return Object.freeze(Object.assign({}, defaultPlayerFeatures, obj)); 9 | } 10 | 11 | const featuresByPlayer = { 12 | deadbeef: defineFeatures({ 13 | showTotalTime: true, 14 | prependToQueue: true, 15 | }), 16 | foobar2000: defineFeatures({ 17 | }) 18 | }; 19 | 20 | export function getPlayerFeatures(name) 21 | { 22 | return featuresByPlayer[name] || defaultPlayerFeatures; 23 | } 24 | -------------------------------------------------------------------------------- /js/webui/src/position_control.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { clamp } from 'lodash' 3 | import { formatTime } from './utils.js' 4 | import ModelBinding from './model_binding.js'; 5 | import ServiceContext from "./service_context.js"; 6 | 7 | class PositionControl extends React.PureComponent 8 | { 9 | static contextType = ServiceContext; 10 | 11 | constructor(props, context) 12 | { 13 | super(props, context); 14 | 15 | this.state = this.getStateFromModel(); 16 | this.handleClick = this.handleClick.bind(this); 17 | } 18 | 19 | getStateFromModel() 20 | { 21 | const { position, duration } = this.context.playerModel.activeItem; 22 | 23 | return { duration, position }; 24 | } 25 | 26 | handleClick(e) 27 | { 28 | if (e.button !== 0) 29 | return; 30 | 31 | const rect = e.target.getBoundingClientRect(); 32 | const positionPercent = (e.clientX - rect.left) / rect.width; 33 | const newPosition = this.state.duration * positionPercent; 34 | 35 | if (newPosition >= 0) 36 | this.context.playerModel.setPosition(newPosition); 37 | } 38 | 39 | render() 40 | { 41 | const { position, duration } = this.state; 42 | let positionPercent = '0%'; 43 | let timeInfo = ''; 44 | 45 | if (position >= 0 && duration > 0) 46 | { 47 | positionPercent = '' + clamp(100 * position / duration, 0, 100) + '%'; 48 | timeInfo = formatTime(position) + ' / ' + formatTime(duration); 49 | } 50 | 51 | return ( 52 |
53 |
54 |
55 |
{timeInfo}
56 |
57 |
58 | ); 59 | } 60 | } 61 | 62 | export default ModelBinding(PositionControl, { playerModel: 'change' }); 63 | -------------------------------------------------------------------------------- /js/webui/src/scroll_manager.js: -------------------------------------------------------------------------------- 1 | const defaultPosition = Object.freeze({ offset: 0 }); 2 | 3 | export default class ScrollManager 4 | { 5 | constructor() 6 | { 7 | this.components = {}; 8 | this.positions = {}; 9 | } 10 | 11 | registerComponent(key, component) 12 | { 13 | this.components[key] = component; 14 | } 15 | 16 | unregisterComponent(key) 17 | { 18 | delete this.components[key]; 19 | } 20 | 21 | getPosition(key) 22 | { 23 | return this.positions[key] || defaultPosition; 24 | } 25 | 26 | savePosition(key, offset) 27 | { 28 | this.positions[key] = Object.freeze({ offset }); 29 | } 30 | 31 | scrollToItem(key, offsetItem) 32 | { 33 | const component = this.components[key]; 34 | const position = Object.freeze({ offsetItem }); 35 | 36 | if (component) 37 | component.scrollTo(position); 38 | else 39 | this.positions[key] = position; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /js/webui/src/service_context.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const ServiceContext = React.createContext(Object.freeze({})); 4 | 5 | export default ServiceContext; 6 | -------------------------------------------------------------------------------- /js/webui/src/settings_content.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SettingsView } from './navigation_model.js'; 3 | import ModelBinding from './model_binding.js'; 4 | import DefaultsSettings from './defaults_settings.js' 5 | import GeneralSettings from './general_settings.js'; 6 | import ColumnsSettings from './columns_settings.js'; 7 | import ServiceContext from './service_context.js'; 8 | import AboutBox from './about_box.js'; 9 | import OutputSettings from './output_settings.js'; 10 | 11 | const settingsViews = { 12 | [SettingsView.general]: GeneralSettings, 13 | [SettingsView.columns]: ColumnsSettings, 14 | [SettingsView.output]: OutputSettings, 15 | [SettingsView.defaults]: DefaultsSettings, 16 | [SettingsView.about]: AboutBox, 17 | }; 18 | 19 | class SettingsContent extends React.PureComponent 20 | { 21 | static contextType = ServiceContext; 22 | 23 | constructor(props, context) 24 | { 25 | super(props, context); 26 | 27 | this.state = this.getStateFromModel(); 28 | } 29 | 30 | getStateFromModel() 31 | { 32 | const { settingsView } = this.context.navigationModel; 33 | return { settingsView }; 34 | } 35 | 36 | render() 37 | { 38 | const { settingsView } = this.state; 39 | 40 | const View = settingsViews[settingsView]; 41 | 42 | return ( 43 |
44 |
45 | { View ? : null } 46 |
47 |
48 | ) 49 | } 50 | } 51 | 52 | export default ModelBinding(SettingsContent, { 53 | navigationModel: 'settingsViewChange' 54 | }); 55 | -------------------------------------------------------------------------------- /js/webui/src/settings_store.js: -------------------------------------------------------------------------------- 1 | export default class SettingsStore 2 | { 3 | on(eventName, callback) 4 | { 5 | if (eventName === 'refresh') 6 | window.addEventListener('storage', callback); 7 | } 8 | 9 | off(eventName, callback) 10 | { 11 | if (eventName === 'refresh') 12 | window.removeEventListener('storage', callback); 13 | } 14 | 15 | getItem(key) 16 | { 17 | return localStorage.getItem(key); 18 | } 19 | 20 | setItem(key, value) 21 | { 22 | localStorage.setItem(key, value); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /js/webui/src/status_bar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { PlaybackState } from 'beefweb-client' 3 | import { formatTime } from './utils.js' 4 | import ModelBinding from './model_binding.js'; 5 | import ServiceContext from './service_context.js'; 6 | import urls from './urls.js'; 7 | 8 | const stateToName = Object.freeze({ 9 | [PlaybackState.playing]: 'Playing', 10 | [PlaybackState.paused]: 'Paused', 11 | [PlaybackState.stopped]: 'Stopped' 12 | }); 13 | 14 | class StatusBar extends React.PureComponent 15 | { 16 | static contextType = ServiceContext; 17 | 18 | constructor(props, context) 19 | { 20 | super(props, context); 21 | 22 | this.state = this.getStateFromModel(); 23 | } 24 | 25 | getStateFromModel() 26 | { 27 | const { playerModel, playlistModel } = this.context; 28 | 29 | const totalTime = playlistModel.currentPlaylist 30 | ? playlistModel.currentPlaylist.totalTime 31 | : 0; 32 | 33 | return { 34 | playbackState: playerModel.playbackState, 35 | totalCount: playlistModel.playlistItems.totalCount, 36 | totalTime, 37 | showTotalTime: playerModel.features.showTotalTime, 38 | }; 39 | } 40 | 41 | getStatusLine() 42 | { 43 | const { playbackState, totalCount, totalTime, showTotalTime } = this.state; 44 | 45 | const items = [ 46 | stateToName[playbackState], 47 | `${totalCount} track(s)`, 48 | ]; 49 | 50 | if (showTotalTime) 51 | items.push(`${formatTime(totalTime, true)} total playtime`); 52 | 53 | return items.join(' | '); 54 | } 55 | 56 | render() 57 | { 58 | return ( 59 | 64 | ); 65 | } 66 | } 67 | 68 | export default ModelBinding(StatusBar, { 69 | playerModel: 'change', 70 | playlistModel: ['playlistsChange', 'itemsChange'] 71 | }); 72 | -------------------------------------------------------------------------------- /js/webui/src/tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tests 6 | 7 | 8 |
9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /js/webui/src/tests/index.js: -------------------------------------------------------------------------------- 1 | import q from 'qunit' 2 | 3 | q.test( "hello test", function( assert ) { 4 | assert.ok( 1 == "1", "Passed!" ); 5 | }); 6 | 7 | q.start(); 8 | -------------------------------------------------------------------------------- /js/webui/src/timer.js: -------------------------------------------------------------------------------- 1 | export default class Timer 2 | { 3 | constructor(callback, period) 4 | { 5 | this.period = period; 6 | this.callback = callback; 7 | this.intervalId = null; 8 | 9 | this.update = this.update.bind(this); 10 | } 11 | 12 | restart() 13 | { 14 | this.stop(); 15 | this.lastTick = Date.now(); 16 | this.intervalId = setInterval(this.update, this.period); 17 | } 18 | 19 | stop() 20 | { 21 | if (this.intervalId) 22 | { 23 | clearInterval(this.intervalId); 24 | this.intervalId = null; 25 | } 26 | } 27 | 28 | update() 29 | { 30 | const now = Date.now(); 31 | const delta = now - this.lastTick; 32 | this.lastTick = now; 33 | this.callback(delta, now); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /js/webui/src/touch_mode_controller.js: -------------------------------------------------------------------------------- 1 | import { primaryInput } from 'detect-it' 2 | import EventEmitter from 'wolfy87-eventemitter' 3 | import SettingsModel, { InputMode } from './settings_model.js' 4 | 5 | export default class TouchModeController 6 | { 7 | constructor(settingsModel) 8 | { 9 | this.settingsModel = settingsModel; 10 | } 11 | 12 | start() 13 | { 14 | this.settingsModel.on('inputModeChange', this.update.bind(this)); 15 | this.update(); 16 | } 17 | 18 | update() 19 | { 20 | this.settingsModel.touchMode = this.isTouchMode(); 21 | } 22 | 23 | isTouchMode() 24 | { 25 | const { inputMode } = this.settingsModel; 26 | 27 | if (inputMode === InputMode.forceMouse) 28 | return false; 29 | 30 | if (inputMode === InputMode.forceTouch) 31 | return true; 32 | 33 | return primaryInput === 'touch'; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /js/webui/src/urls.js: -------------------------------------------------------------------------------- 1 | const urls = Object.freeze({ 2 | viewCurrentPlaylist: '#/playlists', 3 | 4 | viewPlaylist(id) 5 | { 6 | return `#/playlists/${encodeURIComponent(id)}`; 7 | }, 8 | 9 | browseCurrentPath: '#/files', 10 | 11 | browsePath(path) 12 | { 13 | return `#/files/!${encodeURIComponent(path)}`; 14 | }, 15 | 16 | viewCurrentSettings: '#/settings', 17 | 18 | viewAlbumArt: '#/album-art', 19 | 20 | viewSettings(view) 21 | { 22 | return `#/settings/${view}`; 23 | }, 24 | 25 | nowPlaying: '#/now-playing', 26 | }); 27 | 28 | export default urls; 29 | 30 | export function getPathFromUrl(url) 31 | { 32 | const index = url.indexOf('!'); 33 | 34 | if (index < 0) 35 | return null; 36 | 37 | return decodeURIComponent(url.substring(index + 1)); 38 | } 39 | -------------------------------------------------------------------------------- /js/webui/src/window_controller.js: -------------------------------------------------------------------------------- 1 | import iconPlay from 'open-iconic/png/media-play-4x.png' 2 | import iconPause from 'open-iconic/png/media-pause-4x.png' 3 | import iconStop from 'open-iconic/png/media-stop-4x.png' 4 | import { PlaybackState } from 'beefweb-client' 5 | 6 | const stateToIcon = { 7 | [PlaybackState.playing]: iconPlay, 8 | [PlaybackState.paused]: iconPause, 9 | [PlaybackState.stopped]: iconStop 10 | }; 11 | 12 | function setIcon(icon) 13 | { 14 | let iconElement = document.getElementById('player-state-icon'); 15 | 16 | if (iconElement) 17 | { 18 | iconElement.href = icon; 19 | return; 20 | } 21 | 22 | iconElement = document.createElement('link'); 23 | iconElement.id = 'player-state-icon'; 24 | iconElement.rel = 'shortcut icon'; 25 | iconElement.href = icon; 26 | 27 | document.head.appendChild(iconElement); 28 | } 29 | 30 | function stateEqual(x, y) 31 | { 32 | return x.title === y.title && x.playbackState === y.playbackState; 33 | } 34 | 35 | export default class WindowController 36 | { 37 | constructor(playerModel) 38 | { 39 | this.playerModel = playerModel; 40 | this.handleUpdate = this.handleUpdate.bind(this); 41 | } 42 | 43 | start() 44 | { 45 | this.playerModel.on('change', this.handleUpdate); 46 | 47 | this.state = { 48 | title: '', 49 | playbackState: '' 50 | }; 51 | 52 | this.handleUpdate(); 53 | } 54 | 55 | getStateFromModel() 56 | { 57 | const model = this.playerModel; 58 | const playbackState = model.playbackState; 59 | 60 | const title = playbackState !== PlaybackState.stopped 61 | ? model.activeItem.columns[0] + ' - ' + model.info.title 62 | : model.info.title; 63 | 64 | return { title, playbackState }; 65 | } 66 | 67 | handleUpdate() 68 | { 69 | const state = this.getStateFromModel(); 70 | 71 | if (stateEqual(this.state, state)) 72 | return; 73 | 74 | this.state = state; 75 | 76 | window.document.title = state.title; 77 | setIcon(stateToIcon[state.playbackState]); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /scripts/dev_install.cmd: -------------------------------------------------------------------------------- 1 | @setlocal 2 | 3 | @if [%1] == [] goto :usage 4 | @if [%1] == [-?] goto :usage 5 | @if [%1] == [--help] goto :usage 6 | 7 | set BUILD_TYPE=%1 8 | set PROFILE_DIR=%APPDATA%\foobar2000-v2 9 | set COMPONENT_DIR=%PROFILE_DIR%\user-components-x64\foo_beefweb 10 | set CONFIG_DIR=%PROFILE_DIR%\beefweb 11 | 12 | mkdir "%COMPONENT_DIR%" 13 | mkdir "%CONFIG_DIR%" 14 | 15 | cd "%~dp0..\build\%BUILD_TYPE%\cpp\server" 16 | 17 | @if errorlevel 1 ( 18 | @echo Build directory does not exist, aborting 19 | @goto :end 20 | ) 21 | 22 | copy /Y foobar2000\%BUILD_TYPE%\*.* "%COMPONENT_DIR%" 23 | copy /Y %BUILD_TYPE%\config.json "%CONFIG_DIR%" 24 | 25 | @goto :end 26 | 27 | :usage 28 | @echo Copy binaries and config file from current build directory to foobar2000 profile 29 | @echo. 30 | @echo Usage: 31 | @echo %~nx0 ^ 32 | @echo. 33 | @echo Build types: 34 | @echo Debug, Release, MinSizeRel, RelWithDebInfo 35 | @echo. 36 | 37 | :end 38 | -------------------------------------------------------------------------------- /scripts/dev_install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | cd "$(dirname $0)/.." 6 | 7 | plugin_file=beefweb.so 8 | webui_root=beefweb.root 9 | 10 | function relink 11 | { 12 | if [ -L "$1" ]; then 13 | echo "removing existing link '$1'" 14 | rm "$1" 15 | elif [ -e "$1" ]; then 16 | echo "target '$1' exists and is not a symbolic link, aborting" 17 | exit 1 18 | fi 19 | 20 | echo "setting up link '$1' -> '$2'" 21 | ln -s "$2" "$1" 22 | } 23 | 24 | function install 25 | { 26 | plugin_dir="$HOME/.local/lib/deadbeef" 27 | server_build_dir="$(pwd)/build/$1/cpp/server/deadbeef" 28 | webui_build_dir="$(pwd)/build/$1/js/webui/output" 29 | 30 | if [ ! -e "$plugin_dir" ]; then 31 | mkdir -p "$plugin_dir" 32 | fi 33 | 34 | relink "$plugin_dir/$plugin_file" "$server_build_dir/$plugin_file" 35 | relink "$plugin_dir/$webui_root" "$webui_build_dir" 36 | } 37 | 38 | usage="Add symlinks from ~/.local/lib/deadbeef to binaries in current build directory 39 | 40 | Usage: 41 | $(basename $0) 42 | 43 | Build types: 44 | Debug, Release, MinSizeRel, RelWithDebInfo 45 | " 46 | 47 | case "$1" in 48 | Debug|Release|MinSizeRel|RelWithDebInfo) 49 | install "$1" 50 | ;; 51 | 52 | 53 | ""|-?|--help) 54 | echo "$usage" 55 | ;; 56 | 57 | *) 58 | echo "invalid build type: $1, try --help" 59 | exit 1 60 | ;; 61 | esac 62 | -------------------------------------------------------------------------------- /scripts/install/deadbeef.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | version="$1" 6 | 7 | case "$version" in 8 | 'v1.8') 9 | url='https://sourceforge.net/projects/deadbeef/files/travis/linux/1.8.8/deadbeef-static_1.8.8-1_x86_64.tar.bz2' 10 | hash='2e65b41ac39ddbc08e60ef25695a7a9a928f4342e038c1cdf3bb8b234f419b9b' 11 | ;; 12 | 13 | 'v1.9') 14 | url='https://sourceforge.net/projects/deadbeef/files/travis/linux/1.9.6/deadbeef-static_1.9.6-1_x86_64.tar.bz2' 15 | hash='aa17741053f63a7fceace003bf269bd4c4c9e55e42ee14286d9fbf34fbc8e014' 16 | ;; 17 | 18 | 'v1.10') 19 | url='https://sourceforge.net/projects/deadbeef/files/travis/linux/1.10.0/deadbeef-static_1.10.0-1_x86_64.tar.bz2' 20 | hash='e6d89edfaa17a2634bffdb9050ab096f14e39ac71ea6183cd48cae136a5f1a97' 21 | ;; 22 | 23 | *) 24 | echo "usage: $(basename $0) " 25 | exit 1 26 | ;; 27 | esac 28 | 29 | "$(dirname $0)/install.sh" "deadbeef/$version" "$url" "$hash" 30 | -------------------------------------------------------------------------------- /scripts/install/foobar2000.cmd: -------------------------------------------------------------------------------- 1 | @setlocal 2 | 3 | @if "%1" == "v1.6" ( 4 | @set pkg_file=foobar2000_v1.6.17.exe 5 | @goto :install 6 | ) 7 | 8 | @if "%1" == "v2.0" ( 9 | @set pkg_file=foobar2000_v2.0.exe 10 | @goto :install 11 | ) 12 | 13 | @if "%1" == "v2.0-x64" ( 14 | @set pkg_file=foobar2000-x64_v2.0.exe 15 | @goto :install 16 | ) 17 | 18 | @if "%1" == "v2.1" ( 19 | @set pkg_file=foobar2000_v2.1.6.exe 20 | @goto :install 21 | ) 22 | 23 | @if "%1" == "v2.1-x64" ( 24 | @set pkg_file=foobar2000-x64_v2.1.6.exe 25 | @goto :install 26 | ) 27 | 28 | @if "%1" == "v2.24" ( 29 | @set pkg_file=foobar2000_v2.24.2.exe 30 | @goto :install 31 | ) 32 | 33 | @if "%1" == "v2.24-x64" ( 34 | @set pkg_file=foobar2000-x64_v2.24.2.exe 35 | @goto :install 36 | ) 37 | 38 | @echo Usage: %~nx0 version 39 | @echo Supported versions: v1.6 v2.0 v2.0-x64 v2.1 v2.1-x64 v2.24 v2.24-x64 40 | @cmd /c exit 1 41 | @goto :end 42 | 43 | :install 44 | 45 | set target_dir=apps\foobar2000\%1 46 | 47 | cd "%~dp0..\.." 48 | 49 | if exist %target_dir%\. ( 50 | rmdir /s /q %target_dir% 51 | @if errorlevel 1 goto :end 52 | ) 53 | 54 | mkdir %target_dir% 55 | @if errorlevel 1 goto :end 56 | 57 | cd %target_dir% 58 | @if errorlevel 1 goto :end 59 | 60 | curl --silent --fail --show-error --location -o %pkg_file% ^ 61 | "https://hyperblast.org/files/foobar2000/%pkg_file%" 62 | @if errorlevel 1 goto :end 63 | 64 | echo.>portable_mode_enabled 65 | @if errorlevel 1 goto :end 66 | 67 | %pkg_file% /S /D=%cd% 68 | @if errorlevel 1 goto :end 69 | 70 | del %pkg_file% 71 | @if errorlevel 1 goto :end 72 | 73 | xcopy /E /F /Y "%~dp0foobar2000\%1" . 74 | @if errorlevel 1 goto :end 75 | 76 | :end 77 | -------------------------------------------------------------------------------- /scripts/install/foobar2000/v1.6/configuration/Core.cfg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperblast/beefweb/2bc397b09235a6a939a0af9a5927f2c644b52e56/scripts/install/foobar2000/v1.6/configuration/Core.cfg -------------------------------------------------------------------------------- /scripts/install/foobar2000/v1.6/theme.fth: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperblast/beefweb/2bc397b09235a6a939a0af9a5927f2c644b52e56/scripts/install/foobar2000/v1.6/theme.fth -------------------------------------------------------------------------------- /scripts/install/foobar2000/v2.0-x64/profile/config.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperblast/beefweb/2bc397b09235a6a939a0af9a5927f2c644b52e56/scripts/install/foobar2000/v2.0-x64/profile/config.sqlite -------------------------------------------------------------------------------- /scripts/install/foobar2000/v2.0-x64/profile/theme.fth: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperblast/beefweb/2bc397b09235a6a939a0af9a5927f2c644b52e56/scripts/install/foobar2000/v2.0-x64/profile/theme.fth -------------------------------------------------------------------------------- /scripts/install/foobar2000/v2.0/profile/config.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperblast/beefweb/2bc397b09235a6a939a0af9a5927f2c644b52e56/scripts/install/foobar2000/v2.0/profile/config.sqlite -------------------------------------------------------------------------------- /scripts/install/foobar2000/v2.0/profile/theme.fth: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperblast/beefweb/2bc397b09235a6a939a0af9a5927f2c644b52e56/scripts/install/foobar2000/v2.0/profile/theme.fth -------------------------------------------------------------------------------- /scripts/install/foobar2000/v2.1-x64/profile/config.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperblast/beefweb/2bc397b09235a6a939a0af9a5927f2c644b52e56/scripts/install/foobar2000/v2.1-x64/profile/config.sqlite -------------------------------------------------------------------------------- /scripts/install/foobar2000/v2.1-x64/profile/theme.fth: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperblast/beefweb/2bc397b09235a6a939a0af9a5927f2c644b52e56/scripts/install/foobar2000/v2.1-x64/profile/theme.fth -------------------------------------------------------------------------------- /scripts/install/foobar2000/v2.1/profile/config.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperblast/beefweb/2bc397b09235a6a939a0af9a5927f2c644b52e56/scripts/install/foobar2000/v2.1/profile/config.sqlite -------------------------------------------------------------------------------- /scripts/install/foobar2000/v2.1/profile/theme.fth: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperblast/beefweb/2bc397b09235a6a939a0af9a5927f2c644b52e56/scripts/install/foobar2000/v2.1/profile/theme.fth -------------------------------------------------------------------------------- /scripts/install/foobar2000/v2.24-x64/profile/config.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperblast/beefweb/2bc397b09235a6a939a0af9a5927f2c644b52e56/scripts/install/foobar2000/v2.24-x64/profile/config.sqlite -------------------------------------------------------------------------------- /scripts/install/foobar2000/v2.24-x64/profile/theme.fth: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperblast/beefweb/2bc397b09235a6a939a0af9a5927f2c644b52e56/scripts/install/foobar2000/v2.24-x64/profile/theme.fth -------------------------------------------------------------------------------- /scripts/install/foobar2000/v2.24/profile/config.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperblast/beefweb/2bc397b09235a6a939a0af9a5927f2c644b52e56/scripts/install/foobar2000/v2.24/profile/config.sqlite -------------------------------------------------------------------------------- /scripts/install/foobar2000/v2.24/profile/theme.fth: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperblast/beefweb/2bc397b09235a6a939a0af9a5927f2c644b52e56/scripts/install/foobar2000/v2.24/profile/theme.fth -------------------------------------------------------------------------------- /scripts/install/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | name="$1" 6 | url="$2" 7 | hash="$3" 8 | 9 | if [ -z "$name" ] || [ -z "$url" ] || [ -z "$hash" ]; then 10 | echo "usage: $(basename $0) " 11 | exit 1 12 | fi 13 | 14 | cd "$(dirname $0)/../.." 15 | 16 | file="${url##*/}" 17 | install_dir="apps/$name" 18 | 19 | rm -rf "$install_dir" 20 | mkdir -p "$install_dir" 21 | cd "$install_dir" 22 | 23 | curl --silent --fail --show-error --location -o "$file" "$url" 24 | echo "$hash *$file" | sha256sum -c 25 | tar xf "$file" --strip-components=1 26 | rm "$file" 27 | -------------------------------------------------------------------------------- /scripts/install/patch.cmd: -------------------------------------------------------------------------------- 1 | setlocal 2 | 3 | set target_dir=apps\patch 4 | set pkg_file=patch.zip 5 | 6 | cd "%~dp0..\.." 7 | 8 | if exist %target_dir%. ( 9 | rmdir /s /q %target_dir% 10 | @if errorlevel 1 goto :end 11 | ) 12 | 13 | mkdir %target_dir% 14 | @if errorlevel 1 goto :end 15 | 16 | cd %target_dir% 17 | @if errorlevel 1 goto :end 18 | 19 | curl --silent --fail --show-error --location -o %pkg_file% ^ 20 | "https://hyperblast.org/files/patch/%pkg_file%" 21 | @if errorlevel 1 goto :end 22 | 23 | 7z x %pkg_file% 24 | @if errorlevel 1 goto :end 25 | 26 | del %pkg_file% 27 | @if errorlevel 1 goto :end 28 | 29 | :end 30 | -------------------------------------------------------------------------------- /scripts/print_versions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | if [ -z "$CXX" ]; then 6 | CXX=c++ 7 | fi 8 | 9 | $CXX --version | head -n1 10 | cmake --version | head -n1 11 | echo -n 'node ' && node --version 12 | echo -n 'yarn ' && yarn --version 13 | -------------------------------------------------------------------------------- /version.sh: -------------------------------------------------------------------------------- 1 | version=0.11 2 | final=0 3 | --------------------------------------------------------------------------------