├── .clang-format ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .travis.yml ├── CMakeLists.txt ├── DEVELOPMENT.md ├── Dockerfile ├── LICENSE ├── README.md ├── cmake └── FindLibMPDClient.cmake ├── contrib ├── init.debian ├── ympd.default ├── ympd.freebsd ├── ympd.service └── ympd.spec ├── htdocs ├── assets │ └── favicon.ico ├── css │ ├── bootstrap-theme.css │ ├── bootstrap.css │ └── mpd.css ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ └── glyphicons-halflings-regular.woff ├── index.html └── js │ ├── bootstrap-notify.js │ ├── bootstrap-slider.js │ ├── bootstrap.js │ ├── bootstrap.min.js │ ├── jquery-1.10.2.js │ ├── jquery-1.10.2.min.js │ ├── jquery-ui-sortable.min.js │ ├── jquery.cookie.js │ ├── modernizr-custom.js │ ├── mpd.js │ └── sammy.js ├── src ├── config.h.in ├── http_server.c ├── http_server.h ├── json_encode.c ├── json_encode.h ├── mongoose.c ├── mongoose.h ├── mpd_client.c ├── mpd_client.h └── ympd.c ├── tools ├── lint.sh ├── mkdata.c └── mkdata.pl └── ympd.1 /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | # BasedOnStyle: Google 4 | AccessModifierOffset: -1 5 | AlignAfterOpenBracket: Align 6 | AlignConsecutiveMacros: false 7 | AlignConsecutiveAssignments: false 8 | AlignConsecutiveDeclarations: false 9 | AlignEscapedNewlines: Left 10 | AlignOperands: true 11 | AlignTrailingComments: true 12 | AllowAllArgumentsOnNextLine: true 13 | AllowAllConstructorInitializersOnNextLine: true 14 | AllowAllParametersOfDeclarationOnNextLine: true 15 | AllowShortBlocksOnASingleLine: Never 16 | AllowShortCaseLabelsOnASingleLine: false 17 | AllowShortFunctionsOnASingleLine: None 18 | AllowShortLambdasOnASingleLine: All 19 | AllowShortIfStatementsOnASingleLine: Never 20 | AllowShortLoopsOnASingleLine: true 21 | AlwaysBreakAfterDefinitionReturnType: None 22 | AlwaysBreakAfterReturnType: None 23 | AlwaysBreakBeforeMultilineStrings: true 24 | AlwaysBreakTemplateDeclarations: Yes 25 | BinPackArguments: true 26 | BinPackParameters: true 27 | BraceWrapping: 28 | AfterCaseLabel: false 29 | AfterClass: false 30 | AfterControlStatement: true 31 | AfterEnum: false 32 | AfterFunction: false 33 | AfterNamespace: false 34 | AfterObjCDeclaration: false 35 | AfterStruct: false 36 | AfterUnion: false 37 | AfterExternBlock: false 38 | BeforeCatch: false 39 | BeforeElse: false 40 | IndentBraces: false 41 | SplitEmptyFunction: true 42 | SplitEmptyRecord: true 43 | SplitEmptyNamespace: true 44 | BreakBeforeBinaryOperators: None 45 | BreakBeforeBraces: Attach 46 | BreakBeforeInheritanceComma: false 47 | BreakInheritanceList: BeforeColon 48 | BreakBeforeTernaryOperators: true 49 | BreakConstructorInitializersBeforeComma: false 50 | BreakConstructorInitializers: BeforeColon 51 | BreakAfterJavaFieldAnnotations: false 52 | BreakStringLiterals: true 53 | ColumnLimit: 100 54 | CommentPragmas: '^ IWYU pragma:' 55 | CompactNamespaces: false 56 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 57 | ConstructorInitializerIndentWidth: 4 58 | ContinuationIndentWidth: 4 59 | Cpp11BracedListStyle: true 60 | DeriveLineEnding: true 61 | DerivePointerAlignment: true 62 | DisableFormat: false 63 | ExperimentalAutoDetectBinPacking: false 64 | FixNamespaceComments: true 65 | ForEachMacros: 66 | - foreach 67 | - Q_FOREACH 68 | - BOOST_FOREACH 69 | IncludeBlocks: Regroup 70 | IncludeCategories: 71 | - Regex: '^' 72 | Priority: 2 73 | SortPriority: 0 74 | - Regex: '^<.*\.h>' 75 | Priority: 1 76 | SortPriority: 0 77 | - Regex: '^<.*' 78 | Priority: 2 79 | SortPriority: 0 80 | - Regex: '.*' 81 | Priority: 3 82 | SortPriority: 0 83 | IncludeIsMainRegex: '([-_](test|unittest))?$' 84 | IncludeIsMainSourceRegex: '' 85 | IndentCaseLabels: true 86 | IndentGotoLabels: true 87 | IndentPPDirectives: None 88 | IndentWidth: 4 89 | IndentWrappedFunctionNames: false 90 | JavaScriptQuotes: Leave 91 | JavaScriptWrapImports: true 92 | KeepEmptyLinesAtTheStartOfBlocks: false 93 | MacroBlockBegin: '' 94 | MacroBlockEnd: '' 95 | MaxEmptyLinesToKeep: 1 96 | NamespaceIndentation: None 97 | ObjCBinPackProtocolList: Never 98 | ObjCBlockIndentWidth: 2 99 | ObjCSpaceAfterProperty: false 100 | ObjCSpaceBeforeProtocolList: true 101 | PenaltyBreakAssignment: 2 102 | PenaltyBreakBeforeFirstCallParameter: 1 103 | PenaltyBreakComment: 300 104 | PenaltyBreakFirstLessLess: 120 105 | PenaltyBreakString: 1000 106 | PenaltyBreakTemplateDeclaration: 10 107 | PenaltyExcessCharacter: 1000000 108 | PenaltyReturnTypeOnItsOwnLine: 200 109 | PointerAlignment: Left 110 | RawStringFormats: 111 | - Language: Cpp 112 | Delimiters: 113 | - cc 114 | - CC 115 | - cpp 116 | - Cpp 117 | - CPP 118 | - 'c++' 119 | - 'C++' 120 | CanonicalDelimiter: '' 121 | BasedOnStyle: google 122 | - Language: TextProto 123 | Delimiters: 124 | - pb 125 | - PB 126 | - proto 127 | - PROTO 128 | EnclosingFunctions: 129 | - EqualsProto 130 | - EquivToProto 131 | - PARSE_PARTIAL_TEXT_PROTO 132 | - PARSE_TEST_PROTO 133 | - PARSE_TEXT_PROTO 134 | - ParseTextOrDie 135 | - ParseTextProtoOrDie 136 | CanonicalDelimiter: '' 137 | BasedOnStyle: google 138 | ReflowComments: true 139 | SortIncludes: true 140 | SortUsingDeclarations: true 141 | SpaceAfterCStyleCast: false 142 | SpaceAfterLogicalNot: false 143 | SpaceAfterTemplateKeyword: true 144 | SpaceBeforeAssignmentOperators: true 145 | SpaceBeforeCpp11BracedList: false 146 | SpaceBeforeCtorInitializerColon: true 147 | SpaceBeforeInheritanceColon: true 148 | SpaceBeforeParens: ControlStatements 149 | SpaceBeforeRangeBasedForLoopColon: true 150 | SpaceInEmptyBlock: false 151 | SpaceInEmptyParentheses: false 152 | SpacesBeforeTrailingComments: 2 153 | SpacesInAngles: false 154 | SpacesInConditionalStatement: false 155 | SpacesInContainerLiterals: true 156 | SpacesInCStyleCastParentheses: false 157 | SpacesInParentheses: false 158 | SpacesInSquareBrackets: false 159 | SpaceBeforeSquareBrackets: false 160 | Standard: Auto 161 | StatementMacros: 162 | - Q_UNUSED 163 | - QT_REQUIRE_VERSION 164 | TabWidth: 8 165 | UseCRLF: false 166 | UseTab: Never 167 | --- 168 | 169 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build Artifacts 2 | build/ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Used by Prettier to ignore certain files and folders completely. 2 | # See https://prettier.io/docs/en/ignore.html for details. 3 | 4 | # Ignore build and system artifacts 5 | build/ 6 | cmake/ 7 | contrib/ 8 | 9 | # Ignore all 3rd party libraries 10 | **/bootstrap* 11 | **/jquery* 12 | **/modernizr* 13 | **/sammy* 14 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "semi": true, 5 | "singleQuote": true, 6 | "overrides": [ 7 | { 8 | "files": ["*.html"], 9 | "options": { 10 | "tabWidth": 2 11 | } 12 | }, 13 | { 14 | "files": ["*.css"], 15 | "options": { 16 | "tabWidth": 2 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: c 2 | 3 | sudo: required 4 | dist: trusty 5 | 6 | compiler: 7 | - gcc 8 | - clang 9 | 10 | before_install: 11 | - sudo apt-get -qq update 12 | - sudo apt-get install -y libmpdclient-dev cmake 13 | - mkdir build 14 | - cd build 15 | - cmake -D CMAKE_BUILD_TYPE=DEBUG .. 16 | 17 | script: make 18 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.6) 2 | 3 | project (ympd C) 4 | set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${PROJECT_SOURCE_DIR}/cmake/") 5 | set(CPACK_PACKAGE_VERSION_MAJOR "1") 6 | set(CPACK_PACKAGE_VERSION_MINOR "2") 7 | set(CPACK_PACKAGE_VERSION_PATCH "3") 8 | if(CMAKE_BUILD_TYPE MATCHES RELEASE) 9 | set(ASSETS_PATH "${CMAKE_INSTALL_PREFIX}/share/${PROJECT_NAME}/htdocs") 10 | else() 11 | set(ASSETS_PATH "${PROJECT_SOURCE_DIR}/htdocs") 12 | endif() 13 | 14 | option(WITH_MPD_HOST_CHANGE "Let users of the web frontend change the MPD Host" ON) 15 | option(WITH_DYNAMIC_ASSETS "Serve assets dynamically (e.g for development/packaging)" OFF) 16 | option(WITH_IPV6 "enable IPv6 support" ON) 17 | option(WITH_SSL "enable SSL support" ON) 18 | 19 | find_package(LibMPDClient REQUIRED) 20 | find_package(Threads REQUIRED) 21 | 22 | configure_file(src/config.h.in ${PROJECT_BINARY_DIR}/config.h) 23 | include_directories(${PROJECT_BINARY_DIR} ${PROJECT_SOURCE_DIR} ${LIBMPDCLIENT_INCLUDE_DIR}) 24 | 25 | include(CheckCSourceCompiles) 26 | 27 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=gnu99 -Wall") 28 | set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -ggdb -pedantic") 29 | if(WITH_IPV6) 30 | set_property(DIRECTORY APPEND PROPERTY COMPILE_DEFINITIONS NS_ENABLE_IPV6) 31 | endif() 32 | if(WITH_SSL) 33 | find_package(OpenSSL REQUIRED) 34 | include_directories(${OPENSSL_INCLUDE_DIR}) 35 | # list(APPEND LIB_LIST ${OPENSSL_LIBRARIES}) 36 | set_property(DIRECTORY APPEND PROPERTY COMPILE_DEFINITIONS NS_ENABLE_SSL) 37 | endif() 38 | 39 | file(GLOB RESOURCES 40 | RELATIVE ${PROJECT_SOURCE_DIR} 41 | htdocs/js/* 42 | htdocs/assets/* 43 | htdocs/css/*.css 44 | htdocs/fonts/* 45 | htdocs/index.html 46 | htdocs/player.html 47 | ) 48 | 49 | set(SOURCES 50 | src/ympd.c 51 | src/mpd_client.c 52 | src/mongoose.c 53 | src/json_encode.c 54 | ) 55 | 56 | if(NOT WITH_DYNAMIC_ASSETS) 57 | if(CMAKE_CROSSCOMPILING) 58 | set(MKDATA_EXE ${PROJECT_SOURCE_DIR}/tools/mkdata.pl) 59 | else() 60 | set(MKDATA_EXE $) 61 | set(MKDATA_TARGET mkdata) 62 | add_executable(mkdata tools/mkdata.c) 63 | endif() 64 | 65 | add_custom_command(OUTPUT ${PROJECT_BINARY_DIR}/assets.c 66 | COMMAND ${MKDATA_EXE} ${RESOURCES} > ${PROJECT_BINARY_DIR}/assets.c 67 | WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} 68 | DEPENDS ${RESOURCES} ${MKDATA_TARGET} 69 | ) 70 | list(APPEND SOURCES src/http_server.c assets.c) 71 | endif() 72 | 73 | add_executable(ympd ${SOURCES}) 74 | target_link_libraries(ympd ${LIBMPDCLIENT_LIBRARY} ${CMAKE_THREAD_LIBS_INIT} ${OPENSSL_LIBRARIES}) 75 | 76 | install(TARGETS ympd DESTINATION bin) 77 | install(FILES ympd.1 DESTINATION ${CMAKE_INSTALL_PREFIX}/share/man/man1) 78 | if(WITH_DYNAMIC_ASSETS) 79 | install(DIRECTORY htdocs DESTINATION share/${PROJECT_NAME}) 80 | endif() 81 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Development Notes 2 | 3 | ## Code Formatting 4 | 5 | The project has been formatted with [prettier.io](https://prettier.io/) for the HTML, JavaScript, CSS, and Markdown files. See the configuration file [.prettierrc.json](./.prettierrc.json) and the ignore file [.prettierignore](./.prettierignore) for details. If `prettier` is installed globally, there's no need to provide the various `npm`-type dependencies in the project. Various editors may provide plugins that can use this configuration without having to install `npm` and `prettier` manually. 6 | 7 | Manual Usage: 8 | 9 | ```bash 10 | > npx prettier --write . 11 | ``` 12 | 13 | The C source and header files have been formatted with `clang-format`. There's no easy way to manually execute the formatter on all of the C files at the same time. The clang format is based off of the 'Google' style with ajdustments to make the changes not as disruptive. See [.clang-format](./.clang-format) file for the formatting rules. Various editors should be able to automatically format the source on save. 14 | 15 | The only files formatted are the non-third party library files. 16 | 17 | Manual Usage: 18 | 19 | ```bash 20 | > clang-format -i -style=file 21 | ``` 22 | 23 | Manually formatted files: 24 | 25 | - http_server.c 26 | - http_server.h 27 | - json_encode.h 28 | - mpd_client.c 29 | - mpd_client.h 30 | - ympd.c 31 | 32 | For help with the rules, see [Clang Format Configurator](https://zed0.co.uk/clang-format-configurator/) for an interactive tool and [ClangFormat](https://clang.llvm.org/docs/ClangFormat.html) for the rules reference. 33 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.5 2 | WORKDIR /app/build 3 | COPY . /app 4 | RUN apk add --no-cache g++ make cmake libmpdclient-dev openssl-dev 5 | RUN cmake .. 6 | RUN make 7 | 8 | FROM alpine:3.5 9 | RUN apk add --no-cache libmpdclient openssl 10 | EXPOSE 8080 11 | COPY --from=0 /app/build/ympd /usr/bin/ympd 12 | COPY --from=0 /app/build/mkdata /usr/bin/mkdata 13 | CMD ympd -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc. 5 | 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Library General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License 307 | along with this program; if not, write to the Free Software 308 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 309 | 310 | 311 | Also add information on how to contact you by electronic and paper mail. 312 | 313 | If the program is interactive, make it output a short notice like this 314 | when it starts in an interactive mode: 315 | 316 | Gnomovision version 69, Copyright (C) year name of author 317 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 318 | This is free software, and you are welcome to redistribute it 319 | under certain conditions; type `show c' for details. 320 | 321 | The hypothetical commands `show w' and `show c' should show the appropriate 322 | parts of the General Public License. Of course, the commands you use may 323 | be called something other than `show w' and `show c'; they could even be 324 | mouse-clicks or menu items--whatever suits your program. 325 | 326 | You should also get your employer (if you work as a programmer) or your 327 | school, if any, to sign a "copyright disclaimer" for the program, if 328 | necessary. Here is a sample; alter the names: 329 | 330 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 331 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 332 | 333 | , 1 April 1989 334 | Ty Coon, President of Vice 335 | 336 | This General Public License does not permit incorporating your program into 337 | proprietary programs. If your program is a subroutine library, you may 338 | consider it more useful to permit linking proprietary applications with the 339 | library. If this is what you want to do, use the GNU Library General 340 | Public License instead of this License. 341 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/notandy/ympd.svg)](https://travis-ci.org/notandy/ympd) 2 | ympd 3 | ==== 4 | 5 | Standalone MPD Web GUI written in C, utilizing Websockets and Bootstrap/JS 6 | 7 | http://www.ympd.org 8 | 9 | ![ScreenShot](http://www.ympd.org/assets/ympd_github.png) 10 | 11 | ## Dependencies 12 | 13 | - libmpdclient 2: http://www.musicpd.org/libs/libmpdclient/ 14 | - cmake 2.6: http://cmake.org/ 15 | - OpenSSL: https://www.openssl.org/ 16 | 17 | ## Unix Build Instructions 18 | 19 | 1. install dependencies. cmake, libmpdclient (dev), and OpenSSL (dev) are available from all major distributions. 20 | 2. create build directory `cd /path/to/src; mkdir build; cd build` 21 | 3. create makefile `cmake .. -DCMAKE_INSTALL_PREFIX:PATH=/usr` 22 | 4. build `make` 23 | 5. install `sudo make install` or just run with `./ympd` 24 | 25 | ## Run flags 26 | 27 | ``` 28 | Usage: ./ympd [OPTION]... 29 | 30 | -D, --digest path to htdigest file for authorization 31 | (realm ympd) [no authorization] 32 | -h, --host connect to mpd at host [localhost] 33 | -p, --port connect to mpd at port [6600] 34 | -w, --webport [ip:] listen interface/port for webserver [8080] 35 | -u, --user drop priviliges to user after socket bind 36 | -V, --version get version 37 | --help this help 38 | ``` 39 | 40 | ## SSL Support 41 | 42 | To run ympd with SSL support: 43 | 44 | - create a certificate (key and cert in the same file), example: 45 | 46 | ``` 47 | # openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 1000 -nodes 48 | # cat key.pem cert.pem > ssl.pem 49 | ``` 50 | 51 | - tell ympd to use a webport using SSL and where to find the certificate: 52 | 53 | ``` 54 | # ./ympd -w "ssl://8081:/path/to/ssl.pem" 55 | ``` 56 | 57 | ## Copyright 58 | 59 | 2013-2014 60 | -------------------------------------------------------------------------------- /cmake/FindLibMPDClient.cmake: -------------------------------------------------------------------------------- 1 | # - Try to find LibMPDClient 2 | # Once done, this will define 3 | # 4 | # LIBMPDCLIENT_FOUND - System has LibMPDClient 5 | # LIBMPDCLIENT_INCLUDE_DIRS - The LibMPDClient include directories 6 | # LIBMPDCLIENT_LIBRARIES - The libraries needed to use LibMPDClient 7 | # LIBMPDCLIENT_DEFINITIONS - Compiler switches required for using LibMPDClient 8 | 9 | find_package(PkgConfig) 10 | pkg_check_modules(PC_LIBMPDCLIENT QUIET libmpdclient) 11 | set(LIBMPDCLIENT_DEFINITIONS ${PC_LIBMPDCLIENT_CFLAGS_OTHER}) 12 | 13 | find_path(LIBMPDCLIENT_INCLUDE_DIR 14 | NAMES mpd/player.h 15 | HINTS ${PC_LIBMPDCLIENT_INCLUDEDIR} ${PC_LIBMPDCLIENT_INCLUDE_DIRS} 16 | ) 17 | 18 | find_library(LIBMPDCLIENT_LIBRARY 19 | NAMES mpdclient 20 | HINTS ${PC_LIBMPDCLIENT_LIBDIR} ${PC_LIBMPDCLIENT_LIBRARY_DIRS} 21 | ) 22 | 23 | set(LIBMPDCLIENT_LIBRARIES ${LIBMPDCLIENT_LIBRARY}) 24 | set(LIBMPDCLIENT_INCLUDE_DIRS ${LIBMPDCLIENT_INCLUDE_DIR}) 25 | 26 | include(FindPackageHandleStandardArgs) 27 | find_package_handle_standard_args(LibMPDClient DEFAULT_MSG 28 | LIBMPDCLIENT_LIBRARY LIBMPDCLIENT_INCLUDE_DIR 29 | ) 30 | 31 | mark_as_advanced(LIBMPDCLIENT_LIBRARY LIBMPDCLIENT_INCLUDE_DIR) 32 | 33 | -------------------------------------------------------------------------------- /contrib/init.debian: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ### BEGIN INIT INFO 3 | # Provides: ympd 4 | # Required-Start: $remote_fs mpd 5 | # Required-Stop: $remote_fs mpd 6 | # Default-Start: 2 3 4 5 7 | # Default-Stop: 0 1 6 8 | # Short-Description: Daemonized version of ympd. 9 | # Description: Enable service provided by ympd. 10 | ### END INIT INFO 11 | #Author: Andrew Karpow 12 | 13 | . /lib/lsb/init-functions 14 | 15 | PATH=/sbin:/usr/sbin:/bin:/usr/bin 16 | DESC="ympd Daemon" 17 | NAME=ympd 18 | DAEMON=/usr/bin/$NAME 19 | PIDFILE=/var/run/$NAME.pid 20 | SCRIPTNAME=/etc/init.d/$NAME 21 | LOG_OUT=/var/log/$NAME.out 22 | LOG_ERR=/var/log/$NAME.err 23 | YMPD_USER=nobody 24 | MPD_HOST=localhost 25 | MPD_PORT=6600 26 | WEB_PORT=8080 27 | #DIGEST=--digest /path/to/htdigest 28 | #LOCALPORT=8080 29 | 30 | 31 | # Exit if the package is not installed 32 | [ -x "$DAEMON" ] || exit 0 33 | 34 | # Read configuration variable file if it is present 35 | [ -r /etc/default/$NAME ] && . /etc/default/$NAME 36 | 37 | # Load the VERBOSE setting and other rcS variables 38 | [ -f /etc/default/rcS ] && . /etc/default/rcS 39 | 40 | DAEMON_OPT="--user $YMPD_USER --mpdpass '$MPD_PASSWORD' --webport $WEB_PORT --host $MPD_HOST --port $MPD_PORT $DIGEST $LOCALPORT" 41 | 42 | do_start() 43 | { 44 | start-stop-daemon --start --background --quiet --pidfile $PIDFILE --make-pidfile \ 45 | --exec $DAEMON --test > /dev/null || return 1 46 | start-stop-daemon --start --background --quiet --pidfile $PIDFILE --make-pidfile \ 47 | --exec $DAEMON -- $DAEMON_OPT || return 2 48 | } 49 | 50 | do_stop() 51 | { 52 | start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME 53 | RETVAL="$?" 54 | 55 | [ "$RETVAL" = 2 ] && return 2 56 | rm -f $PIDFILE 57 | return "$RETVAL" 58 | } 59 | 60 | case "$1" in 61 | start) 62 | [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" 63 | do_start 64 | case "$?" in 65 | 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 66 | 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; 67 | esac 68 | ;; 69 | stop) 70 | [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" 71 | do_stop 72 | case "$?" in 73 | 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 74 | 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; 75 | esac 76 | ;; 77 | status) 78 | status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $? 79 | ;; 80 | restart|force-reload) 81 | # 82 | # If the "reload" option is implemented then remove the 83 | # 'force-reload' alias 84 | # 85 | log_daemon_msg "Restarting $DESC" "$NAME" 86 | do_stop 87 | case "$?" in 88 | 0|1) 89 | do_start 90 | case "$?" in 91 | 0) log_end_msg 0 ;; 92 | 1) log_end_msg 1 ;; # Old process is still running 93 | *) log_end_msg 1 ;; # Failed to start 94 | esac 95 | ;; 96 | *) 97 | # Failed to stop 98 | log_end_msg 1 99 | ;; 100 | esac 101 | ;; 102 | *) 103 | echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 104 | exit 3 105 | ;; 106 | esac 107 | 108 | -------------------------------------------------------------------------------- /contrib/ympd.default: -------------------------------------------------------------------------------- 1 | MPD_HOST=localhost 2 | MPD_PORT=6600 3 | MPD_PASSWORD= 4 | WEB_PORT=8080 5 | #DIGEST=--digest /path/to/htdigest 6 | #LOCALPORT=--localport 8080 7 | -------------------------------------------------------------------------------- /contrib/ympd.freebsd: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # PROVIDE: ympd 4 | # REQUIRE: DAEMON musicpd 5 | # KEYWORD: shutdown 6 | 7 | # Add the following line to /etc/rc.conf to enable ympd: 8 | # 9 | # ympd_enable="YES" 10 | 11 | . /etc/rc.subr 12 | 13 | name="ympd" 14 | rcvar="${name}_enable" 15 | command="/usr/local/bin/${name}" 16 | pidfile="/var/run/${name}.pid" 17 | start_cmd="ympd_start" 18 | 19 | load_rc_config "${name}" 20 | : ${ympd_enable:="NO"} 21 | 22 | ympd_start() 23 | { 24 | check_startmsgs && echo "Starting ${name}." 25 | /usr/sbin/daemon -f -p "${pidfile}" "${command}" "${rc_flags}" 26 | } 27 | 28 | run_rc_command "$1" 29 | -------------------------------------------------------------------------------- /contrib/ympd.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=ympd server daemon 3 | Requires=network.target local-fs.target 4 | 5 | [Service] 6 | User=nobody 7 | DynamicUser=yes 8 | MountAPIVFS=yes 9 | RemoveIPC=yes 10 | CapabilityBoundingSet= 11 | LockPersonality=yes 12 | PrivateUsers=yes 13 | PrivateTmp=yes 14 | PrivateDevices=yes 15 | ProtectSystem=strict 16 | NoNewPrivileges=yes 17 | MemoryDenyWriteExecute=yes 18 | RestrictRealtime=yes 19 | RestrictNamespaces=yes 20 | RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 21 | ProtectKernelTunables=yes 22 | ProtectKernelModules=yes 23 | ProtectControlGroups=yes 24 | ProtectHome=yes 25 | 26 | Environment=MPD_HOST=localhost 27 | Environment=MPD_PORT=6600 28 | Environment=MPD_PASSWORD= 29 | Environment=WEB_PORT=8080 30 | Environment=YMPD_USER=nobody 31 | Environment=DIGEST= 32 | Environment=LOCALPORT= 33 | EnvironmentFile=/etc/default/ympd 34 | ExecStart=/usr/bin/ympd --user $USER --webport $WEB_PORT --host $MPD_HOST --port $MPD_PORT $DIGEST $LOCALPORT 35 | Type=simple 36 | 37 | [Install] 38 | WantedBy=multi-user.target 39 | -------------------------------------------------------------------------------- /contrib/ympd.spec: -------------------------------------------------------------------------------- 1 | # 2 | # spec file for package ympd 3 | # 4 | # Copyright (c) 2014 Markus S. 5 | # 6 | # This file is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 2 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | Name: ympd 20 | Version: 1.2.2 21 | Release: 0%{?dist} 22 | Summary: ympd is a lightweight MPD (Music Player Daemon) web client 23 | Group: Applications/Multimedia 24 | License: GPL 25 | URL: http://www.ympd.org/ 26 | 27 | # For this spec file to work, the ympd sources must be located in a directory 28 | # named ympd-1.2.2 (with "1.2.2" being the version number defined above). 29 | # If the sources are compressed in another format than ZIP, change the 30 | # file extension accordingly. 31 | Source0: %{name}-%{version}.zip 32 | 33 | # Package names only verified with Fedora. 34 | # Should the packages in your distro be named dirrerently, 35 | # see http://en.opensuse.org/openSUSE:Build_Service_cross_distribution_howto 36 | # %if 0%{?fedora} 37 | BuildRequires: cmake 38 | BuildRequires: unzip 39 | BuildRequires: libmpdclient-devel 40 | Requires: libmpdclient 41 | # %endif 42 | 43 | %description 44 | ympd is a lightweight MPD (Music Player Daemon) web client that runs without 45 | a dedicated webserver or interpreters like PHP, NodeJS or Ruby. 46 | It's tuned for minimal resource usage and requires only very litte dependencies. 47 | 48 | 49 | %prep 50 | %setup -q 51 | 52 | %build 53 | mkdir build 54 | pushd build 55 | %cmake .. -DCMAKE_INSTALL_PREFIX_PATH=%{_prefix} 56 | make PREFIX=%{_prefix} %{?_smp_mflags} 57 | popd 58 | 59 | %install 60 | pushd build 61 | %{make_install} 62 | popd 63 | 64 | %files 65 | %defattr(-,root,root,-) 66 | %doc LICENSE README.md 67 | %{_bindir}/%{name} 68 | %{_mandir}/man[^3]/* 69 | 70 | 71 | %changelog 72 | -------------------------------------------------------------------------------- /htdocs/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuperBFG7/ympd/297691ab5b5422957f9e469bb5514add9c4cd036/htdocs/assets/favicon.ico -------------------------------------------------------------------------------- /htdocs/css/bootstrap-theme.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.1.0 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | 7 | .btn-default, 8 | .btn-primary, 9 | .btn-success, 10 | .btn-info, 11 | .btn-warning, 12 | .btn-danger { 13 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .2); 14 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); 15 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); 16 | } 17 | .btn-default:active, 18 | .btn-primary:active, 19 | .btn-success:active, 20 | .btn-info:active, 21 | .btn-warning:active, 22 | .btn-danger:active, 23 | .btn-default.active, 24 | .btn-primary.active, 25 | .btn-success.active, 26 | .btn-info.active, 27 | .btn-warning.active, 28 | .btn-danger.active { 29 | -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); 30 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); 31 | } 32 | .btn:active, 33 | .btn.active { 34 | background-image: none; 35 | } 36 | .btn-default { 37 | text-shadow: 0 1px 0 #fff; 38 | background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%); 39 | background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%); 40 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0); 41 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 42 | background-repeat: repeat-x; 43 | border-color: #dbdbdb; 44 | border-color: #ccc; 45 | } 46 | .btn-default:hover, 47 | .btn-default:focus { 48 | background-color: #e0e0e0; 49 | background-position: 0 -15px; 50 | } 51 | .btn-default:active, 52 | .btn-default.active { 53 | background-color: #e0e0e0; 54 | border-color: #dbdbdb; 55 | } 56 | .btn-primary { 57 | background-image: -webkit-linear-gradient(top, #428bca 0%, #2d6ca2 100%); 58 | background-image: linear-gradient(to bottom, #428bca 0%, #2d6ca2 100%); 59 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0); 60 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 61 | background-repeat: repeat-x; 62 | border-color: #2b669a; 63 | } 64 | .btn-primary:hover, 65 | .btn-primary:focus { 66 | background-color: #2d6ca2; 67 | background-position: 0 -15px; 68 | } 69 | .btn-primary:active, 70 | .btn-primary.active { 71 | background-color: #2d6ca2; 72 | border-color: #2b669a; 73 | } 74 | .btn-success { 75 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%); 76 | background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%); 77 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0); 78 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 79 | background-repeat: repeat-x; 80 | border-color: #3e8f3e; 81 | } 82 | .btn-success:hover, 83 | .btn-success:focus { 84 | background-color: #419641; 85 | background-position: 0 -15px; 86 | } 87 | .btn-success:active, 88 | .btn-success.active { 89 | background-color: #419641; 90 | border-color: #3e8f3e; 91 | } 92 | .btn-info { 93 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); 94 | background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%); 95 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0); 96 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 97 | background-repeat: repeat-x; 98 | border-color: #28a4c9; 99 | } 100 | .btn-info:hover, 101 | .btn-info:focus { 102 | background-color: #2aabd2; 103 | background-position: 0 -15px; 104 | } 105 | .btn-info:active, 106 | .btn-info.active { 107 | background-color: #2aabd2; 108 | border-color: #28a4c9; 109 | } 110 | .btn-warning { 111 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); 112 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%); 113 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0); 114 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 115 | background-repeat: repeat-x; 116 | border-color: #e38d13; 117 | } 118 | .btn-warning:hover, 119 | .btn-warning:focus { 120 | background-color: #eb9316; 121 | background-position: 0 -15px; 122 | } 123 | .btn-warning:active, 124 | .btn-warning.active { 125 | background-color: #eb9316; 126 | border-color: #e38d13; 127 | } 128 | .btn-danger { 129 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%); 130 | background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%); 131 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0); 132 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 133 | background-repeat: repeat-x; 134 | border-color: #b92c28; 135 | } 136 | .btn-danger:hover, 137 | .btn-danger:focus { 138 | background-color: #c12e2a; 139 | background-position: 0 -15px; 140 | } 141 | .btn-danger:active, 142 | .btn-danger.active { 143 | background-color: #c12e2a; 144 | border-color: #b92c28; 145 | } 146 | .thumbnail, 147 | .img-thumbnail { 148 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 149 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 150 | } 151 | .dropdown-menu > li > a:hover, 152 | .dropdown-menu > li > a:focus { 153 | background-color: #e8e8e8; 154 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 155 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 156 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 157 | background-repeat: repeat-x; 158 | } 159 | .dropdown-menu > .active > a, 160 | .dropdown-menu > .active > a:hover, 161 | .dropdown-menu > .active > a:focus { 162 | background-color: #357ebd; 163 | background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%); 164 | background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%); 165 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0); 166 | background-repeat: repeat-x; 167 | } 168 | .navbar-default { 169 | background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%); 170 | background-image: linear-gradient(to bottom, #fff 0%, #f8f8f8 100%); 171 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0); 172 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 173 | background-repeat: repeat-x; 174 | border-radius: 4px; 175 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); 176 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); 177 | } 178 | .navbar-default .navbar-nav > .active > a { 179 | background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f3f3f3 100%); 180 | background-image: linear-gradient(to bottom, #ebebeb 0%, #f3f3f3 100%); 181 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0); 182 | background-repeat: repeat-x; 183 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); 184 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); 185 | } 186 | .navbar-brand, 187 | .navbar-nav > li > a { 188 | text-shadow: 0 1px 0 rgba(255, 255, 255, .25); 189 | } 190 | .navbar-inverse { 191 | background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%); 192 | background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%); 193 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0); 194 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 195 | background-repeat: repeat-x; 196 | } 197 | .navbar-inverse .navbar-nav > .active > a { 198 | background-image: -webkit-linear-gradient(top, #222 0%, #282828 100%); 199 | background-image: linear-gradient(to bottom, #222 0%, #282828 100%); 200 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0); 201 | background-repeat: repeat-x; 202 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); 203 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); 204 | } 205 | .navbar-inverse .navbar-brand, 206 | .navbar-inverse .navbar-nav > li > a { 207 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .25); 208 | } 209 | .navbar-static-top, 210 | .navbar-fixed-top, 211 | .navbar-fixed-bottom { 212 | border-radius: 0; 213 | } 214 | .alert { 215 | text-shadow: 0 1px 0 rgba(255, 255, 255, .2); 216 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); 217 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); 218 | } 219 | .alert-success { 220 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); 221 | background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%); 222 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0); 223 | background-repeat: repeat-x; 224 | border-color: #b2dba1; 225 | } 226 | .alert-info { 227 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%); 228 | background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%); 229 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0); 230 | background-repeat: repeat-x; 231 | border-color: #9acfea; 232 | } 233 | .alert-warning { 234 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); 235 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%); 236 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0); 237 | background-repeat: repeat-x; 238 | border-color: #f5e79e; 239 | } 240 | .alert-danger { 241 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); 242 | background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%); 243 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0); 244 | background-repeat: repeat-x; 245 | border-color: #dca7a7; 246 | } 247 | .progress { 248 | background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); 249 | background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%); 250 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0); 251 | background-repeat: repeat-x; 252 | } 253 | .progress-bar { 254 | background-image: -webkit-linear-gradient(top, #428bca 0%, #3071a9 100%); 255 | background-image: linear-gradient(to bottom, #428bca 0%, #3071a9 100%); 256 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0); 257 | background-repeat: repeat-x; 258 | } 259 | .progress-bar-success { 260 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%); 261 | background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%); 262 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0); 263 | background-repeat: repeat-x; 264 | } 265 | .progress-bar-info { 266 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); 267 | background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%); 268 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0); 269 | background-repeat: repeat-x; 270 | } 271 | .progress-bar-warning { 272 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); 273 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%); 274 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0); 275 | background-repeat: repeat-x; 276 | } 277 | .progress-bar-danger { 278 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%); 279 | background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%); 280 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0); 281 | background-repeat: repeat-x; 282 | } 283 | .list-group { 284 | border-radius: 4px; 285 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 286 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 287 | } 288 | .list-group-item.active, 289 | .list-group-item.active:hover, 290 | .list-group-item.active:focus { 291 | text-shadow: 0 -1px 0 #3071a9; 292 | background-image: -webkit-linear-gradient(top, #428bca 0%, #3278b3 100%); 293 | background-image: linear-gradient(to bottom, #428bca 0%, #3278b3 100%); 294 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0); 295 | background-repeat: repeat-x; 296 | border-color: #3278b3; 297 | } 298 | .panel { 299 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05); 300 | box-shadow: 0 1px 2px rgba(0, 0, 0, .05); 301 | } 302 | .panel-default > .panel-heading { 303 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 304 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 305 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 306 | background-repeat: repeat-x; 307 | } 308 | .panel-primary > .panel-heading { 309 | background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%); 310 | background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%); 311 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0); 312 | background-repeat: repeat-x; 313 | } 314 | .panel-success > .panel-heading { 315 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); 316 | background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%); 317 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0); 318 | background-repeat: repeat-x; 319 | } 320 | .panel-info > .panel-heading { 321 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); 322 | background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%); 323 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0); 324 | background-repeat: repeat-x; 325 | } 326 | .panel-warning > .panel-heading { 327 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); 328 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%); 329 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0); 330 | background-repeat: repeat-x; 331 | } 332 | .panel-danger > .panel-heading { 333 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%); 334 | background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%); 335 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0); 336 | background-repeat: repeat-x; 337 | } 338 | .well { 339 | background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); 340 | background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%); 341 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0); 342 | background-repeat: repeat-x; 343 | border-color: #dcdcdc; 344 | -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); 345 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); 346 | } 347 | /*# sourceMappingURL=bootstrap-theme.css.map */ 348 | -------------------------------------------------------------------------------- /htdocs/css/mpd.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | padding-bottom: 50px; 4 | } 5 | 6 | .starter-template { 7 | padding: 40px 15px; 8 | } 9 | 10 | #volumeslider { 11 | width: 150px; 12 | float: left; 13 | } 14 | 15 | #volumeslider .progress { 16 | margin-bottom: 0; 17 | } 18 | 19 | button { 20 | overflow: hidden; 21 | } 22 | 23 | #volume-icon { 24 | float: left; 25 | margin-right: 10px; 26 | margin-top: 2px; 27 | } 28 | 29 | #volume-number { 30 | float: right; 31 | margin-top: 2px; 32 | margin-left: 10px; 33 | } 34 | 35 | #love { 36 | float: right; 37 | } 38 | 39 | #love > button > span { 40 | color: red; 41 | } 42 | 43 | #breadcrump { 44 | display: block; 45 | overflow: auto; 46 | white-space: nowrap; 47 | } 48 | 49 | #breadcrump > li > a { 50 | cursor: pointer; 51 | } 52 | 53 | #counter { 54 | font-size: 24px; 55 | margin-top: -6px; 56 | margin-left: 10px; 57 | min-width: 50px; 58 | } 59 | 60 | #search { 61 | margin-right: -10px; 62 | } 63 | 64 | .btn-group-hover { 65 | opacity: 20%; 66 | } 67 | 68 | .btn:active, 69 | .btn.active { 70 | background-image: none; 71 | outline: 0; 72 | -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); 73 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); 74 | color: #428bca; 75 | background-color: #fdfdfd; 76 | border-color: #adadad; 77 | } 78 | 79 | @media (max-width: 1199px) { 80 | #btn-responsive-block > .btn { 81 | padding: 6px 12px; 82 | font-size: 14px; 83 | border-radius: 4px; 84 | } 85 | } 86 | 87 | h1 { 88 | display: block; 89 | 90 | overflow: hidden; 91 | text-overflow: ellipsis; 92 | white-space: nowrap; 93 | } 94 | 95 | td:nth-child(4), 96 | th:nth-child(4) { 97 | /* This *has* to be placed before 98 | any t[dh]:nth-last-child(2) for 99 | the override to work. */ 100 | min-width: 50%; 101 | } 102 | 103 | td:nth-last-child(2), 104 | th:nth-last-child(2) { 105 | text-align: right; 106 | width: 4em; 107 | } 108 | 109 | #salamisandwich td:nth-child(4) span { 110 | font-size: 90%; 111 | 112 | display: block; 113 | } 114 | 115 | td:nth-child(2), 116 | td:nth-child(3) { 117 | min-width: 25%; 118 | max-width: 10em; 119 | 120 | overflow: hidden; 121 | text-overflow: ellipsis; 122 | white-space: nowrap; 123 | } 124 | 125 | @media only screen and (max-width: 600px) { 126 | td:nth-child(2), 127 | td:nth-child(3) { 128 | min-width: 0; 129 | max-width: 0; 130 | } 131 | td:nth-child(4), 132 | th:nth-child(4) { 133 | min-width: 10%; 134 | white-space: normal; 135 | } 136 | } 137 | 138 | tbody { 139 | cursor: pointer; 140 | } 141 | 142 | td:last-child, 143 | td:first-child { 144 | width: 30px; 145 | } 146 | 147 | .notifications { 148 | position: fixed; 149 | z-index: 9999; 150 | } 151 | 152 | /* Positioning */ 153 | .notifications.top-right { 154 | right: 10px; 155 | top: 60px; 156 | } 157 | 158 | /* Notification Element */ 159 | .notifications > div { 160 | position: relative; 161 | z-index: 9999; 162 | margin: 5px 0px; 163 | } 164 | 165 | button { 166 | overflow: hidden; 167 | } 168 | 169 | #trashmode span:last-child { 170 | display: inline-block; 171 | text-align: left; 172 | width: 2.8em; 173 | } 174 | 175 | #filter > a.active { 176 | font-weight: bold; 177 | pointer-events: none; 178 | cursor: default; 179 | text-decoration: none; 180 | color: black; 181 | } 182 | 183 | @media screen and (min-width: 992px) { 184 | .sticky { 185 | position: sticky; 186 | top: 55px; 187 | z-index: 99; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /htdocs/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuperBFG7/ympd/297691ab5b5422957f9e469bb5514add9c4cd036/htdocs/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /htdocs/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuperBFG7/ympd/297691ab5b5422957f9e469bb5514add9c4cd036/htdocs/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /htdocs/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuperBFG7/ympd/297691ab5b5422957f9e469bb5514add9c4cd036/htdocs/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /htdocs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | ympd 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 26 | 27 | 28 | 29 | 131 | 132 |
133 |
134 |
135 |
136 | 137 |
138 | 139 |
140 |

141 | 146 | 147 | 148 | 156 |

157 |

158 | 159 | 160 |

161 |

  

162 | 163 |
164 |
165 |
166 | 167 | 168 |
169 |
170 | Queue 171 | 172 |
173 | 174 | 175 |
176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 |
#ArtistAlbumTitleLength
191 |
192 | 193 | 197 |
198 | 199 | 200 |
201 |
202 |
206 | 209 | 212 | 215 | 218 | 221 |
222 |
226 | 227 |
232 | 236 | 245 | 254 |
255 | 256 |
260 | 267 | 273 | Save Queue 274 | 275 |
276 |
277 |
278 | 279 |
280 | 281 |
282 | 283 | 284 | 285 | 442 | 443 | 444 | 445 | 497 | 498 | 499 | 551 | 552 | 553 | 584 | 585 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | -------------------------------------------------------------------------------- /htdocs/js/bootstrap-notify.js: -------------------------------------------------------------------------------- 1 | /** 2 | * bootstrap-notify.js v1.0 3 | * -- 4 | * Copyright 2012 Goodybag, Inc. 5 | * -- 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | (function ($) { 20 | var Notification = function (element, options) { 21 | // Element collection 22 | this.$element = $(element); 23 | this.$note = $('
'); 24 | this.options = $.extend(true, {}, $.fn.notify.defaults, options); 25 | 26 | // Setup from options 27 | if(this.options.transition) 28 | if(this.options.transition == 'fade') 29 | this.$note.addClass('in').addClass(this.options.transition); 30 | else this.$note.addClass(this.options.transition); 31 | else this.$note.addClass('fade').addClass('in'); 32 | 33 | if(this.options.type) 34 | this.$note.addClass('alert-' + this.options.type); 35 | else this.$note.addClass('alert-success'); 36 | 37 | if(!this.options.message && this.$element.data("message") !== '') // dom text 38 | this.$note.html(this.$element.data("message")); 39 | else 40 | if(typeof this.options.message === 'object') 41 | if(this.options.message.html) 42 | this.$note.html(this.options.message.html); 43 | else if(this.options.message.text) 44 | this.$note.text(this.options.message.text); 45 | else 46 | this.$note.html(this.options.message); 47 | 48 | if(this.options.closable) 49 | var link = $(' ×'); 50 | $(link).on('click', $.proxy(onClose, this)); 51 | this.$note.prepend(link); 52 | 53 | return this; 54 | }; 55 | 56 | var onClose = function() { 57 | this.options.onClose(); 58 | $(this.$note).remove(); 59 | this.options.onClosed(); 60 | return false; 61 | }; 62 | 63 | Notification.prototype.show = function () { 64 | if(this.options.fadeOut.enabled) 65 | this.$note.delay(this.options.fadeOut.delay || 3000).fadeOut('slow', $.proxy(onClose, this)); 66 | 67 | this.$element.append(this.$note); 68 | this.$note.alert(); 69 | }; 70 | 71 | Notification.prototype.hide = function () { 72 | if(this.options.fadeOut.enabled) 73 | this.$note.delay(this.options.fadeOut.delay || 3000).fadeOut('slow', $.proxy(onClose, this)); 74 | else onClose.call(this); 75 | }; 76 | 77 | $.fn.notify = function (options) { 78 | return new Notification(this, options); 79 | }; 80 | 81 | $.fn.notify.defaults = { 82 | type: 'success', 83 | closable: true, 84 | transition: 'fade', 85 | fadeOut: { 86 | enabled: true, 87 | delay: 3000 88 | }, 89 | message: null, 90 | onClose: function () {}, 91 | onClosed: function () {} 92 | } 93 | })(window.jQuery); 94 | -------------------------------------------------------------------------------- /htdocs/js/bootstrap-slider.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | 3 | function slider(options){ 4 | if(typeof options === 'number'){ 5 | options = $.extend( 6 | { 7 | origVal:options 8 | }, 9 | defaults, 10 | { 11 | val:(( options < 0 ) ? 0 : ( (options > 100 ) ? 100 : options)) 12 | } 13 | ); 14 | } 15 | else if (options === "get"){ 16 | var vals = []; 17 | 18 | $(this).each(function() { 19 | vals.push($(this).data("sliderValue")); 20 | }); 21 | return vals; 22 | } 23 | else if(typeof options === 'object'){ 24 | options = $.extend({origVal:options.val,origBarColor:options.barColor},defaults,options); 25 | } 26 | 27 | return $(this).each (function() { 28 | var self=$(this); 29 | 30 | if(self.hasClass("slider-wrapper-jq")){ 31 | if(self.data("dragSlider") === "true") 32 | return; 33 | if(typeof options.origVal !== "undefined") 34 | self.slider._setValue.call(self,options.val,null,true); 35 | if(typeof options.origBarColor !== "undefined") 36 | self.find('.progress-bar').css("background-color",options.barColor); 37 | return; 38 | } 39 | 40 | self.addClass("slider-wrapper-jq") 41 | .append($("
") 42 | .append("
") 44 | .append("
")); 45 | 46 | self.find('.progress').on('mousedown', function(evt){ 47 | self.data("dragSlider","true") 48 | .data("startPoint",evt.pageX) 49 | .data("endPoint",evt.pageX); 50 | 51 | if(!$(evt.target).hasClass("btn")){ 52 | self.slider._setWidthFromEvent.call(self,evt.pageX,null,true); 53 | } 54 | else{ 55 | self.data("btnTarget","true"); 56 | } 57 | 58 | evt.preventDefault(); 59 | evt.stopPropagation(); 60 | }); 61 | 62 | $(window).on('mouseup', function(evt){ 63 | if(self.data("dragSlider")==="true"){ 64 | if(!(self.data("btnTarget") === "true" && self.data("startPoint") === self.data("endPoint") )){ 65 | self.slider._setWidthFromEvent.call(self,evt.pageX); 66 | } 67 | 68 | self.removeData("dragSlider") 69 | .removeData("btnTarget") 70 | .removeData("startPoint") 71 | .removeData("endPoint"); 72 | } 73 | }).on('mousemove',function(evt){ 74 | if(self.data("dragSlider")==="true"){ 75 | self.slider._setWidthFromEvent.call(self,evt.pageX,null,true); 76 | self.data("endPoint",evt.pageX); 77 | evt.preventDefault(); 78 | } 79 | }); 80 | 81 | self.slider._setValue.call(self,options.val); 82 | }); 83 | 84 | } 85 | 86 | var defaults={ 87 | val:50, 88 | barColor:"#428bca" 89 | }, 90 | _setWidthFromEvent = function(pageX,reqVals,supress) { 91 | if(!reqVals){ 92 | reqVals = this.slider._getRequiredValues.call(this); 93 | } 94 | else{ 95 | reqVals = null; 96 | } 97 | 98 | var width = pageX - reqVals.pbar.offset().left, 99 | perc = ((100.0*width) / (reqVals.progw)); 100 | 101 | return this.slider._setValue.call(this,perc,reqVals,supress); 102 | }, 103 | _setValue = function (val,reqVals,supress) { 104 | if(!reqVals){ 105 | reqVals = this.slider._getRequiredValues.call(this); 106 | } 107 | 108 | val = ((val<0)?0:((val>100)?100:val)); 109 | var adjVal= ((val*(100-reqVals.pbutp)/100) + (reqVals.pbutp/2)); 110 | 111 | this.data("sliderValue",val); 112 | reqVals.pbar.css({width:adjVal+"%"}); 113 | this.find('div.btn').css('left',adjVal+"%"); 114 | 115 | if(supress !== true){ 116 | this.trigger("slider.newValue",{val:Math.round(val)}); 117 | } 118 | 119 | return val; 120 | }, 121 | _getRequiredValues = function(){ 122 | var pbar=this.find('.progress-bar'), 123 | progw=this.children('.progress').get(0).clientWidth, 124 | pbutp=((this.find('div.btn').get(0).clientWidth*100.0)/progw); 125 | 126 | return { 127 | pbar:pbar, 128 | progw:progw, 129 | pbutp:pbutp 130 | }; 131 | }; 132 | 133 | $.fn.slider = slider; 134 | $.fn.slider.defaults = defaults; 135 | $.fn.slider._getRequiredValues = _getRequiredValues ; 136 | $.fn.slider._setWidthFromEvent = _setWidthFromEvent; 137 | $.fn.slider._setValue = _setValue; 138 | 139 | })(jQuery); 140 | -------------------------------------------------------------------------------- /htdocs/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.1.0 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one(a.support.transition.end,function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b()})}(jQuery),+function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function c(){f.trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one(a.support.transition.end,c).emulateTransitionEnd(150):c())};var d=a.fn.alert;a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("bs.alert");e||d.data("bs.alert",e=new c(this)),"string"==typeof b&&e[b].call(d)})},a.fn.alert.Constructor=c,a.fn.alert.noConflict=function(){return a.fn.alert=d,this},a(document).on("click.bs.alert.data-api",b,c.prototype.close)}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.isLoading=!1};b.DEFAULTS={loadingText:"loading..."},b.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",f.resetText||d.data("resetText",d[e]()),d[e](f[b]||this.options[b]),setTimeout(a.proxy(function(){"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},b.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}a&&this.$element.toggleClass("active")};var c=a.fn.button;a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof c&&c;e||d.data("bs.button",e=new b(this,f)),"toggle"==c?e.toggle():c&&e.setState(c)})},a.fn.button.Constructor=b,a.fn.button.noConflict=function(){return a.fn.button=c,this},a(document).on("click.bs.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle"),b.preventDefault()})}(jQuery),+function(a){"use strict";var b=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},b.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},b.prototype.getActiveIndex=function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},b.prototype.to=function(b){var c=this,d=this.getActiveIndex();return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},b.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},b.prototype.next=function(){return this.sliding?void 0:this.slide("next")},b.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},b.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}if(e.hasClass("active"))return this.sliding=!1;var j=a.Event("slide.bs.carousel",{relatedTarget:e[0],direction:g});return this.$element.trigger(j),j.isDefaultPrevented()?void 0:(this.sliding=!0,f&&this.pause(),this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid.bs.carousel",function(){var b=a(i.$indicators.children()[i.getActiveIndex()]);b&&b.addClass("active")})),a.support.transition&&this.$element.hasClass("slide")?(e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid.bs.carousel")},0)}).emulateTransitionEnd(1e3*d.css("transition-duration").slice(0,-1))):(d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid.bs.carousel")),f&&this.cycle(),this)};var c=a.fn.carousel;a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c),g="string"==typeof c?c:f.slide;e||d.data("bs.carousel",e=new b(this,f)),"number"==typeof c?e.to(c):g?e[g]():f.interval&&e.pause().cycle()})},a.fn.carousel.Constructor=b,a.fn.carousel.noConflict=function(){return a.fn.carousel=c,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(b){var c,d=a(this),e=a(d.attr("data-target")||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"")),f=a.extend({},e.data(),d.data()),g=d.attr("data-slide-to");g&&(f.interval=!1),e.carousel(f),(g=d.attr("data-slide-to"))&&e.data("bs.carousel").to(g),b.preventDefault()}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var b=a(this);b.carousel(b.data())})})}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.DEFAULTS={toggle:!0},b.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},b.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b=a.Event("show.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.$parent&&this.$parent.find("> .panel > .in");if(c&&c.length){var d=c.data("bs.collapse");if(d&&d.transitioning)return;c.collapse("hide"),d||c.data("bs.collapse",null)}var e=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[e](0),this.transitioning=1;var f=function(){this.$element.removeClass("collapsing").addClass("collapse in")[e]("auto"),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return f.call(this);var g=a.camelCase(["scroll",e].join("-"));this.$element.one(a.support.transition.end,a.proxy(f,this)).emulateTransitionEnd(350)[e](this.$element[0][g])}}},b.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?void this.$element[c](0).one(a.support.transition.end,a.proxy(d,this)).emulateTransitionEnd(350):d.call(this)}}},b.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var c=a.fn.collapse;a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c);!e&&f.toggle&&"show"==c&&(c=!c),e||d.data("bs.collapse",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.collapse.Constructor=b,a.fn.collapse.noConflict=function(){return a.fn.collapse=c,this},a(document).on("click.bs.collapse.data-api","[data-toggle=collapse]",function(b){var c,d=a(this),e=d.attr("data-target")||b.preventDefault()||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,""),f=a(e),g=f.data("bs.collapse"),h=g?"toggle":d.data(),i=d.attr("data-parent"),j=i&&a(i);g&&g.transitioning||(j&&j.find('[data-toggle=collapse][data-parent="'+i+'"]').not(d).addClass("collapsed"),d[f.hasClass("in")?"addClass":"removeClass"]("collapsed")),f.collapse(h)})}(jQuery),+function(a){"use strict";function b(b){a(d).remove(),a(e).each(function(){var d=c(a(this)),e={relatedTarget:this};d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown",e)),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown",e))})}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}var d=".dropdown-backdrop",e="[data-toggle=dropdown]",f=function(b){a(b).on("click.bs.dropdown",this.toggle)};f.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(''}),b.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),b.prototype.constructor=b,b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content")[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},b.prototype.hasContent=function(){return this.getTitle()||this.getContent()},b.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},b.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")},b.prototype.tip=function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip};var c=a.fn.popover;a.fn.popover=function(c){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof c&&c;(e||"destroy"!=c)&&(e||d.data("bs.popover",e=new b(this,f)),"string"==typeof c&&e[c]())})},a.fn.popover.Constructor=b,a.fn.popover.noConflict=function(){return a.fn.popover=c,this}}(jQuery),+function(a){"use strict";function b(c,d){var e,f=a.proxy(this.process,this);this.$element=a(a(c).is("body")?window:c),this.$body=a("body"),this.$scrollElement=this.$element.on("scroll.bs.scroll-spy.data-api",f),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||(e=a(c).attr("href"))&&e.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.offsets=a([]),this.targets=a([]),this.activeTarget=null,this.refresh(),this.process()}b.DEFAULTS={offset:10},b.prototype.refresh=function(){var b=this.$element[0]==window?"offset":"position";this.offsets=a([]),this.targets=a([]);{var c=this;this.$body.find(this.selector).map(function(){var d=a(this),e=d.data("target")||d.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[b]().top+(!a.isWindow(c.$scrollElement.get(0))&&c.$scrollElement.scrollTop()),e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){c.offsets.push(this[0]),c.targets.push(this[1])})}},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,d=c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(b>=d)return g!=(a=f.last()[0])&&this.activate(a);if(g&&b<=e[0])return g!=(a=f[0])&&this.activate(a);for(a=e.length;a--;)g!=f[a]&&b>=e[a]&&(!e[a+1]||b<=e[a+1])&&this.activate(f[a])},b.prototype.activate=function(b){this.activeTarget=b,a(this.selector).parentsUntil(this.options.target,".active").removeClass("active");var c=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',d=a(c).parents("li").addClass("active");d.parent(".dropdown-menu").length&&(d=d.closest("li.dropdown").addClass("active")),d.trigger("activate.bs.scrollspy")};var c=a.fn.scrollspy;a.fn.scrollspy=function(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=c,this},a(window).on("load",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);b.scrollspy(b.data())})})}(jQuery),+function(a){"use strict";var b=function(b){this.element=a(b)};b.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.data("target");if(d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),!b.parent("li").hasClass("active")){var e=c.find(".active:last a")[0],f=a.Event("show.bs.tab",{relatedTarget:e});if(b.trigger(f),!f.isDefaultPrevented()){var g=a(d);this.activate(b.parent("li"),c),this.activate(g,g.parent(),function(){b.trigger({type:"shown.bs.tab",relatedTarget:e})})}}},b.prototype.activate=function(b,c,d){function e(){f.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),g?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var f=c.find("> .active"),g=d&&a.support.transition&&f.hasClass("fade");g?f.one(a.support.transition.end,e).emulateTransitionEnd(150):e(),f.removeClass("in")};var c=a.fn.tab;a.fn.tab=function(c){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new b(this)),"string"==typeof c&&e[c]()})},a.fn.tab.Constructor=b,a.fn.tab.noConflict=function(){return a.fn.tab=c,this},a(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(b){b.preventDefault(),a(this).tab("show")})}(jQuery),+function(a){"use strict";var b=function(c,d){this.options=a.extend({},b.DEFAULTS,d),this.$window=a(window).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(c),this.affixed=this.unpin=this.pinnedOffset=null,this.checkPosition()};b.RESET="affix affix-top affix-bottom",b.DEFAULTS={offset:0},b.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(b.RESET).addClass("affix");var a=this.$window.scrollTop(),c=this.$element.offset();return this.pinnedOffset=c.top-a},b.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},b.prototype.checkPosition=function(){if(this.$element.is(":visible")){var c=a(document).height(),d=this.$window.scrollTop(),e=this.$element.offset(),f=this.options.offset,g=f.top,h=f.bottom;"top"==this.affixed&&(e.top+=d),"object"!=typeof f&&(h=g=f),"function"==typeof g&&(g=f.top(this.$element)),"function"==typeof h&&(h=f.bottom(this.$element));var i=null!=this.unpin&&d+this.unpin<=e.top?!1:null!=h&&e.top+this.$element.height()>=c-h?"bottom":null!=g&&g>=d?"top":!1;if(this.affixed!==i){this.unpin&&this.$element.css("top","");var j="affix"+(i?"-"+i:""),k=a.Event(j+".bs.affix");this.$element.trigger(k),k.isDefaultPrevented()||(this.affixed=i,this.unpin="bottom"==i?this.getPinnedOffset():null,this.$element.removeClass(b.RESET).addClass(j).trigger(a.Event(j.replace("affix","affixed"))),"bottom"==i&&this.$element.offset({top:c-h-this.$element.height()}))}}};var c=a.fn.affix;a.fn.affix=function(c){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof c&&c;e||d.data("bs.affix",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.affix.Constructor=b,a.fn.affix.noConflict=function(){return a.fn.affix=c,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var b=a(this),c=b.data();c.offset=c.offset||{},c.offsetBottom&&(c.offset.bottom=c.offsetBottom),c.offsetTop&&(c.offset.top=c.offsetTop),b.affix(c)})})}(jQuery); -------------------------------------------------------------------------------- /htdocs/js/jquery.cookie.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Cookie Plugin v1.4.0 3 | * https://github.com/carhartl/jquery-cookie 4 | * 5 | * Copyright 2013 Klaus Hartl 6 | * Released under the MIT license 7 | */ 8 | (function (factory) { 9 | if (typeof define === 'function' && define.amd) { 10 | // AMD. Register as anonymous module. 11 | define(['jquery'], factory); 12 | } else { 13 | // Browser globals. 14 | factory(jQuery); 15 | } 16 | }(function ($) { 17 | 18 | var pluses = /\+/g; 19 | 20 | function encode(s) { 21 | return config.raw ? s : encodeURIComponent(s); 22 | } 23 | 24 | function decode(s) { 25 | return config.raw ? s : decodeURIComponent(s); 26 | } 27 | 28 | function stringifyCookieValue(value) { 29 | return encode(config.json ? JSON.stringify(value) : String(value)); 30 | } 31 | 32 | function parseCookieValue(s) { 33 | if (s.indexOf('"') === 0) { 34 | // This is a quoted cookie as according to RFC2068, unescape... 35 | s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\'); 36 | } 37 | 38 | try { 39 | // Replace server-side written pluses with spaces. 40 | // If we can't decode the cookie, ignore it, it's unusable. 41 | // If we can't parse the cookie, ignore it, it's unusable. 42 | s = decodeURIComponent(s.replace(pluses, ' ')); 43 | return config.json ? JSON.parse(s) : s; 44 | } catch(e) {} 45 | } 46 | 47 | function read(s, converter) { 48 | var value = config.raw ? s : parseCookieValue(s); 49 | return $.isFunction(converter) ? converter(value) : value; 50 | } 51 | 52 | var config = $.cookie = function (key, value, options) { 53 | 54 | // Write 55 | 56 | if (value !== undefined && !$.isFunction(value)) { 57 | options = $.extend({}, config.defaults, options); 58 | 59 | if (typeof options.expires === 'number') { 60 | var days = options.expires, t = options.expires = new Date(); 61 | t.setTime(+t + days * 864e+5); 62 | } 63 | 64 | return (document.cookie = [ 65 | encode(key), '=', stringifyCookieValue(value), 66 | options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE 67 | options.path ? '; path=' + options.path : '', 68 | options.domain ? '; domain=' + options.domain : '', 69 | options.secure ? '; secure' : '' 70 | ].join('')); 71 | } 72 | 73 | // Read 74 | 75 | var result = key ? undefined : {}; 76 | 77 | // To prevent the for loop in the first place assign an empty array 78 | // in case there are no cookies at all. Also prevents odd result when 79 | // calling $.cookie(). 80 | var cookies = document.cookie ? document.cookie.split('; ') : []; 81 | 82 | for (var i = 0, l = cookies.length; i < l; i++) { 83 | var parts = cookies[i].split('='); 84 | var name = decode(parts.shift()); 85 | var cookie = parts.join('='); 86 | 87 | if (key && key === name) { 88 | // If second argument (value) is a function it's a converter... 89 | result = read(cookie, value); 90 | break; 91 | } 92 | 93 | // Prevent storing a cookie that we couldn't decode. 94 | if (!key && (cookie = read(cookie)) !== undefined) { 95 | result[name] = cookie; 96 | } 97 | } 98 | 99 | return result; 100 | }; 101 | 102 | config.defaults = {}; 103 | 104 | $.removeCookie = function (key, options) { 105 | if ($.cookie(key) === undefined) { 106 | return false; 107 | } 108 | 109 | // Must not alter options, thus extending a fresh object... 110 | $.cookie(key, '', $.extend({}, options, { expires: -1 })); 111 | return !$.cookie(key); 112 | }; 113 | 114 | })); 115 | -------------------------------------------------------------------------------- /htdocs/js/modernizr-custom.js: -------------------------------------------------------------------------------- 1 | /* Modernizr 2.8.3 (Custom Build) | MIT & BSD 2 | * Build: http://modernizr.com/download/#-touch-shiv-cssclasses-teststyles-prefixes-load 3 | */ 4 | ;window.Modernizr=function(a,b,c){function w(a){j.cssText=a}function x(a,b){return w(m.join(a+";")+(b||""))}function y(a,b){return typeof a===b}function z(a,b){return!!~(""+a).indexOf(b)}function A(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:y(f,"function")?f.bind(d||b):f}return!1}var d="2.8.3",e={},f=!0,g=b.documentElement,h="modernizr",i=b.createElement(h),j=i.style,k,l={}.toString,m=" -webkit- -moz- -o- -ms- ".split(" "),n={},o={},p={},q=[],r=q.slice,s,t=function(a,c,d,e){var f,i,j,k,l=b.createElement("div"),m=b.body,n=m||b.createElement("body");if(parseInt(d,10))while(d--)j=b.createElement("div"),j.id=e?e[d]:h+(d+1),l.appendChild(j);return f=["­",'"].join(""),l.id=h,(m?l:n).innerHTML+=f,n.appendChild(l),m||(n.style.background="",n.style.overflow="hidden",k=g.style.overflow,g.style.overflow="hidden",g.appendChild(n)),i=c(l,a),m?l.parentNode.removeChild(l):(n.parentNode.removeChild(n),g.style.overflow=k),!!i},u={}.hasOwnProperty,v;!y(u,"undefined")&&!y(u.call,"undefined")?v=function(a,b){return u.call(a,b)}:v=function(a,b){return b in a&&y(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=r.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(r.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(r.call(arguments)))};return e}),n.touch=function(){var c;return"ontouchstart"in a||a.DocumentTouch&&b instanceof DocumentTouch?c=!0:t(["@media (",m.join("touch-enabled),("),h,")","{#modernizr{top:9px;position:absolute}}"].join(""),function(a){c=a.offsetTop===9}),c};for(var B in n)v(n,B)&&(s=B.toLowerCase(),e[s]=n[B](),q.push((e[s]?"":"no-")+s));return e.addTest=function(a,b){if(typeof a=="object")for(var d in a)v(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return e;b=typeof b=="function"?b():b,typeof f!="undefined"&&f&&(g.className+=" "+(b?"":"no-")+a),e[a]=b}return e},w(""),i=k=null,function(a,b){function l(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function m(){var a=s.elements;return typeof a=="string"?a.split(" "):a}function n(a){var b=j[a[h]];return b||(b={},i++,a[h]=i,j[i]=b),b}function o(a,c,d){c||(c=b);if(k)return c.createElement(a);d||(d=n(c));var g;return d.cache[a]?g=d.cache[a].cloneNode():f.test(a)?g=(d.cache[a]=d.createElem(a)).cloneNode():g=d.createElem(a),g.canHaveChildren&&!e.test(a)&&!g.tagUrn?d.frag.appendChild(g):g}function p(a,c){a||(a=b);if(k)return a.createDocumentFragment();c=c||n(a);var d=c.frag.cloneNode(),e=0,f=m(),g=f.length;for(;e",g="hidden"in a,k=a.childNodes.length==1||function(){b.createElement("a");var a=b.createDocumentFragment();return typeof a.cloneNode=="undefined"||typeof a.createDocumentFragment=="undefined"||typeof a.createElement=="undefined"}()}catch(c){g=!0,k=!0}})();var s={elements:d.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output progress section summary template time video",version:c,shivCSS:d.shivCSS!==!1,supportsUnknownElements:k,shivMethods:d.shivMethods!==!1,type:"default",shivDocument:r,createElement:o,createDocumentFragment:p};a.html5=s,r(b)}(this,b),e._version=d,e._prefixes=m,e.testStyles=t,g.className=g.className.replace(/(^|\s)no-js(\s|$)/,"$1$2")+(f?" js "+q.join(" "):""),e}(this,this.document),function(a,b,c){function d(a){return"[object Function]"==o.call(a)}function e(a){return"string"==typeof a}function f(){}function g(a){return!a||"loaded"==a||"complete"==a||"uninitialized"==a}function h(){var a=p.shift();q=1,a?a.t?m(function(){("c"==a.t?B.injectCss:B.injectJs)(a.s,0,a.a,a.x,a.e,1)},0):(a(),h()):q=0}function i(a,c,d,e,f,i,j){function k(b){if(!o&&g(l.readyState)&&(u.r=o=1,!q&&h(),l.onload=l.onreadystatechange=null,b)){"img"!=a&&m(function(){t.removeChild(l)},50);for(var d in y[c])y[c].hasOwnProperty(d)&&y[c][d].onload()}}var j=j||B.errorTimeout,l=b.createElement(a),o=0,r=0,u={t:d,s:c,e:f,a:i,x:j};1===y[c]&&(r=1,y[c]=[]),"object"==a?l.data=c:(l.src=c,l.type=a),l.width=l.height="0",l.onerror=l.onload=l.onreadystatechange=function(){k.call(this,r)},p.splice(e,0,u),"img"!=a&&(r||2===y[c]?(t.insertBefore(l,s?null:n),m(k,j)):y[c].push(l))}function j(a,b,c,d,f){return q=0,b=b||"j",e(a)?i("c"==b?v:u,a,b,this.i++,c,d,f):(p.splice(this.i++,0,a),1==p.length&&h()),this}function k(){var a=B;return a.loader={load:j,i:0},a}var l=b.documentElement,m=a.setTimeout,n=b.getElementsByTagName("script")[0],o={}.toString,p=[],q=0,r="MozAppearance"in l.style,s=r&&!!b.createRange().compareNode,t=s?l:n.parentNode,l=a.opera&&"[object Opera]"==o.call(a.opera),l=!!b.attachEvent&&!l,u=r?"object":l?"script":"img",v=l?"script":u,w=Array.isArray||function(a){return"[object Array]"==o.call(a)},x=[],y={},z={timeout:function(a,b){return b.length&&(a.timeout=b[0]),a}},A,B;B=function(a){function b(a){var a=a.split("!"),b=x.length,c=a.pop(),d=a.length,c={url:c,origUrl:c,prefixes:a},e,f,g;for(f=0;f 3 | This project's homepage is: http://www.ympd.org 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; version 2 of the License. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along 15 | with this program; if not, write to the Free Software Foundation, Inc., 16 | Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | */ 18 | 19 | #ifndef __CONFIG_H__ 20 | #define __CONFIG_H__ 21 | 22 | #define YMPD_VERSION_MAJOR ${CPACK_PACKAGE_VERSION_MAJOR} 23 | #define YMPD_VERSION_MINOR ${CPACK_PACKAGE_VERSION_MINOR} 24 | #define YMPD_VERSION_PATCH ${CPACK_PACKAGE_VERSION_PATCH} 25 | #define SRC_PATH "${ASSETS_PATH}" 26 | #cmakedefine WITH_MPD_HOST_CHANGE 27 | #cmakedefine WITH_DYNAMIC_ASSETS 28 | #endif 29 | 30 | -------------------------------------------------------------------------------- /src/http_server.c: -------------------------------------------------------------------------------- 1 | /* ympd 2 | (c) 2013-2014 Andrew Karpow 3 | This project's homepage is: http://www.ympd.org 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; version 2 of the License. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along 15 | with this program; if not, write to the Free Software Foundation, Inc., 16 | Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | */ 18 | 19 | #include "http_server.h" 20 | 21 | #include 22 | #include 23 | 24 | #include "mpd_client.h" 25 | 26 | int callback_http(struct mg_connection *c) { 27 | const struct embedded_file *req_file; 28 | 29 | if (!strcmp(c->uri, "/")) 30 | req_file = find_embedded_file("/index.html"); 31 | else 32 | req_file = find_embedded_file(c->uri); 33 | 34 | if (req_file) { 35 | mg_send_header(c, "Content-Type", req_file->mimetype); 36 | mg_send_data(c, req_file->data, req_file->size); 37 | 38 | return MG_TRUE; 39 | } 40 | 41 | if (!strcmp(c->uri, "/wss-auth")) { 42 | unsigned char salt[WSS_AUTH_TOKEN_SIZE + 1]; 43 | 44 | RAND_bytes(salt, WSS_AUTH_TOKEN_SIZE); 45 | for (int i = 0; i <= WSS_AUTH_TOKEN_SIZE; i++) salt[i] = salt[i] % 26 + 65; 46 | salt[WSS_AUTH_TOKEN_SIZE] = 0; 47 | if (mpd.wss_auth_token) 48 | free(mpd.wss_auth_token); 49 | mpd.wss_auth_token = strdup((char *)salt); 50 | 51 | mg_send_header(c, "Content-Type", "text/plain"); 52 | mg_send_data(c, salt, WSS_AUTH_TOKEN_SIZE); 53 | 54 | return MG_TRUE; 55 | } 56 | 57 | mg_send_status(c, 404); 58 | mg_printf_data(c, "Not Found"); 59 | return MG_TRUE; 60 | } 61 | -------------------------------------------------------------------------------- /src/http_server.h: -------------------------------------------------------------------------------- 1 | /* ympd 2 | (c) 2013-2014 Andrew Karpow 3 | This project's homepage is: http://www.ympd.org 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; version 2 of the License. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along 15 | with this program; if not, write to the Free Software Foundation, Inc., 16 | Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | */ 18 | 19 | #ifndef __HTTP_SERVER_H__ 20 | #define __HTTP_SERVER_H__ 21 | 22 | #include "mongoose.h" 23 | 24 | struct embedded_file { 25 | const char *name; 26 | const unsigned char *data; 27 | const char *mimetype; 28 | size_t size; 29 | }; 30 | 31 | const struct embedded_file *find_embedded_file(const char *name); 32 | int callback_http(struct mg_connection *c); 33 | 34 | #endif 35 | -------------------------------------------------------------------------------- /src/json_encode.c: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2004-2013 Sergey Lyubka 2 | // Copyright (c) 2013 Cesanta Software Limited 3 | // All rights reserved 4 | // 5 | // This library is dual-licensed: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License version 2 as 7 | // published by the Free Software Foundation. For the terms of this 8 | // license, see . 9 | // 10 | // You are free to use this library under the terms of the GNU General 11 | // Public License, but WITHOUT ANY WARRANTY; without even the implied 12 | // warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | // See the GNU General Public License for more details. 14 | // 15 | // Alternatively, you can license this library under a commercial 16 | // license, as set out in . 17 | 18 | // json encoder 'frozen' from https://github.com/cesanta/frozen 19 | 20 | #include 21 | 22 | #include "json_encode.h" 23 | 24 | int json_emit_int(char *buf, int buf_len, long int value) { 25 | return buf_len <= 0 ? 0 : snprintf(buf, buf_len, "%ld", value); 26 | } 27 | 28 | int json_emit_double(char *buf, int buf_len, double value) { 29 | return buf_len <= 0 ? 0 : snprintf(buf, buf_len, "%g", value); 30 | } 31 | 32 | int json_emit_quoted_str(char *buf, int buf_len, const char *str) { 33 | int i = 0, j = 0, ch; 34 | 35 | #define EMIT(x) do { if (j < buf_len) buf[j++] = x; } while (0) 36 | 37 | EMIT('"'); 38 | while ((ch = str[i++]) != '\0' && j < buf_len) { 39 | switch (ch) { 40 | case '"': EMIT('\\'); EMIT('"'); break; 41 | case '\\': EMIT('\\'); EMIT('\\'); break; 42 | case '\b': EMIT('\\'); EMIT('b'); break; 43 | case '\f': EMIT('\\'); EMIT('f'); break; 44 | case '\n': EMIT('\\'); EMIT('n'); break; 45 | case '\r': EMIT('\\'); EMIT('r'); break; 46 | case '\t': EMIT('\\'); EMIT('t'); break; 47 | default: EMIT(ch); 48 | } 49 | } 50 | EMIT('"'); 51 | EMIT(0); 52 | 53 | return j == 0 ? 0 : j - 1; 54 | } 55 | 56 | int json_emit_raw_str(char *buf, int buf_len, const char *str) { 57 | return buf_len <= 0 ? 0 : snprintf(buf, buf_len, "%s", str); 58 | } -------------------------------------------------------------------------------- /src/json_encode.h: -------------------------------------------------------------------------------- 1 | /* ympd 2 | (c) 2013-2014 Andrew Karpow 3 | This project's homepage is: http://www.ympd.org 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; version 2 of the License. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along 15 | with this program; if not, write to the Free Software Foundation, Inc., 16 | Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | */ 18 | 19 | #ifndef __JSON_ENCODE_H__ 20 | #define __JSON_ENCODE_H__ 21 | 22 | int json_emit_int(char *buf, int buf_len, long int value); 23 | int json_emit_double(char *buf, int buf_len, double value); 24 | int json_emit_quoted_str(char *buf, int buf_len, const char *str); 25 | int json_emit_raw_str(char *buf, int buf_len, const char *str); 26 | 27 | #endif -------------------------------------------------------------------------------- /src/mongoose.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2004-2013 Sergey Lyubka 2 | // Copyright (c) 2013-2014 Cesanta Software Limited 3 | // All rights reserved 4 | // 5 | // This software is dual-licensed: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License version 2 as 7 | // published by the Free Software Foundation. For the terms of this 8 | // license, see . 9 | // 10 | // You are free to use this software under the terms of the GNU General 11 | // Public License, but WITHOUT ANY WARRANTY; without even the implied 12 | // warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | // See the GNU General Public License for more details. 14 | // 15 | // Alternatively, you can license this software under a commercial 16 | // license, as set out in . 17 | 18 | #ifndef MONGOOSE_HEADER_INCLUDED 19 | #define MONGOOSE_HEADER_INCLUDED 20 | 21 | #define MONGOOSE_VERSION "5.6" 22 | 23 | #include // required for FILE 24 | #include // required for size_t 25 | #include // required for time_t 26 | 27 | #ifdef __cplusplus 28 | extern "C" { 29 | #endif // __cplusplus 30 | 31 | // This structure contains information about HTTP request. 32 | struct mg_connection { 33 | const char *request_method; // "GET", "POST", etc 34 | const char *uri; // URL-decoded URI 35 | const char *http_version; // E.g. "1.0", "1.1" 36 | const char *query_string; // URL part after '?', not including '?', or NULL 37 | 38 | char remote_ip[48]; // Max IPv6 string length is 45 characters 39 | char local_ip[48]; // Local IP address 40 | unsigned short remote_port; // Client's port 41 | unsigned short local_port; // Local port number 42 | 43 | int num_headers; // Number of HTTP headers 44 | struct mg_header { 45 | const char *name; // HTTP header name 46 | const char *value; // HTTP header value 47 | } http_headers[30]; 48 | 49 | char *content; // POST (or websocket message) data, or NULL 50 | size_t content_len; // Data length 51 | 52 | int is_websocket; // Connection is a websocket connection 53 | int status_code; // HTTP status code for HTTP error handler 54 | int wsbits; // First byte of the websocket frame 55 | void *server_param; // Parameter passed to mg_create_server() 56 | void *connection_param; // Placeholder for connection-specific data 57 | void *callback_param; 58 | }; 59 | 60 | struct mg_server; // Opaque structure describing server instance 61 | enum mg_result { MG_FALSE, MG_TRUE, MG_MORE }; 62 | enum mg_event { 63 | MG_POLL = 100, // Callback return value is ignored 64 | MG_CONNECT, // If callback returns MG_FALSE, connect fails 65 | MG_AUTH, // If callback returns MG_FALSE, authentication fails 66 | MG_REQUEST, // If callback returns MG_FALSE, Mongoose continues with req 67 | MG_REPLY, // If callback returns MG_FALSE, Mongoose closes connection 68 | MG_RECV, // Mongoose has received POST data chunk. 69 | // Callback should return a number of bytes to discard from 70 | // the receive buffer, or -1 to close the connection. 71 | MG_CLOSE, // Connection is closed, callback return value is ignored 72 | MG_WS_HANDSHAKE, // New websocket connection, handshake request 73 | MG_WS_CONNECT, // New websocket connection established 74 | MG_HTTP_ERROR // If callback returns MG_FALSE, Mongoose continues with err 75 | }; 76 | typedef int (*mg_handler_t)(struct mg_connection *, enum mg_event); 77 | 78 | // Websocket opcodes, from http://tools.ietf.org/html/rfc6455 79 | enum { 80 | WEBSOCKET_OPCODE_CONTINUATION = 0x0, 81 | WEBSOCKET_OPCODE_TEXT = 0x1, 82 | WEBSOCKET_OPCODE_BINARY = 0x2, 83 | WEBSOCKET_OPCODE_CONNECTION_CLOSE = 0x8, 84 | WEBSOCKET_OPCODE_PING = 0x9, 85 | WEBSOCKET_OPCODE_PONG = 0xa 86 | }; 87 | 88 | // Server management functions 89 | struct mg_server *mg_create_server(void *server_param, mg_handler_t handler); 90 | void mg_destroy_server(struct mg_server **); 91 | const char *mg_set_option(struct mg_server *, const char *opt, const char *val); 92 | time_t mg_poll_server(struct mg_server *, int milliseconds); 93 | const char **mg_get_valid_option_names(void); 94 | const char *mg_get_option(const struct mg_server *server, const char *name); 95 | void mg_copy_listeners(struct mg_server *from, struct mg_server *to); 96 | struct mg_connection *mg_next(struct mg_server *, struct mg_connection *); 97 | void mg_wakeup_server(struct mg_server *); 98 | void mg_wakeup_server_ex(struct mg_server *, mg_handler_t, const char *, ...); 99 | struct mg_connection *mg_connect(struct mg_server *, const char *); 100 | 101 | // Connection management functions 102 | void mg_send_status(struct mg_connection *, int status_code); 103 | void mg_send_header(struct mg_connection *, const char *name, const char *val); 104 | size_t mg_send_data(struct mg_connection *, const void *data, int data_len); 105 | size_t mg_printf_data(struct mg_connection *, const char *format, ...); 106 | size_t mg_write(struct mg_connection *, const void *buf, size_t len); 107 | size_t mg_printf(struct mg_connection *conn, const char *fmt, ...); 108 | 109 | size_t mg_websocket_write(struct mg_connection *, int opcode, 110 | const char *data, size_t data_len); 111 | size_t mg_websocket_printf(struct mg_connection* conn, int opcode, 112 | const char *fmt, ...); 113 | 114 | void mg_send_file(struct mg_connection *, const char *path, const char *); 115 | void mg_send_file_data(struct mg_connection *, int fd); 116 | 117 | const char *mg_get_header(const struct mg_connection *, const char *name); 118 | const char *mg_get_mime_type(const char *name, const char *default_mime_type); 119 | int mg_get_var(const struct mg_connection *conn, const char *var_name, 120 | char *buf, size_t buf_len); 121 | int mg_parse_header(const char *hdr, const char *var_name, char *buf, size_t); 122 | int mg_parse_multipart(const char *buf, int buf_len, 123 | char *var_name, int var_name_len, 124 | char *file_name, int file_name_len, 125 | const char **data, int *data_len); 126 | 127 | 128 | // Utility functions 129 | void *mg_start_thread(void *(*func)(void *), void *param); 130 | char *mg_md5(char buf[33], ...); 131 | int mg_authorize_digest(struct mg_connection *c, FILE *fp); 132 | size_t mg_url_encode(const char *src, size_t s_len, char *dst, size_t dst_len); 133 | int mg_url_decode(const char *src, size_t src_len, char *dst, size_t dst_len, int); 134 | int mg_terminate_ssl(struct mg_connection *c, const char *cert); 135 | int mg_forward(struct mg_connection *c, const char *addr); 136 | void *mg_mmap(FILE *fp, size_t size); 137 | void mg_munmap(void *p, size_t size); 138 | 139 | 140 | // Templates support 141 | struct mg_expansion { 142 | const char *keyword; 143 | void (*handler)(struct mg_connection *); 144 | }; 145 | void mg_template(struct mg_connection *, const char *text, 146 | struct mg_expansion *expansions); 147 | 148 | #ifdef __cplusplus 149 | } 150 | #endif // __cplusplus 151 | 152 | #endif // MONGOOSE_HEADER_INCLUDED 153 | -------------------------------------------------------------------------------- /src/mpd_client.c: -------------------------------------------------------------------------------- 1 | /* ympd 2 | (c) 2013-2014 Andrew Karpow 3 | This project's homepage is: http://www.ympd.org 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; version 2 of the License. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along 15 | with this program; if not, write to the Free Software Foundation, Inc., 16 | Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | */ 18 | 19 | #include "mpd_client.h" 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | #include "config.h" 30 | #include "json_encode.h" 31 | 32 | /* forward declaration */ 33 | static int mpd_notify_callback(struct mg_connection *c, enum mg_event ev); 34 | struct t_mpd mpd; 35 | 36 | const char *mpd_cmd_strs[] = {MPD_CMDS(GEN_STR)}; 37 | 38 | char *get_arg1(char *p) { 39 | return strchr(p, ',') + 1; 40 | } 41 | 42 | char *get_arg2(char *p) { 43 | return get_arg1(get_arg1(p)); 44 | } 45 | 46 | static inline enum mpd_cmd_ids get_cmd_id(char *cmd) { 47 | for (int i = 0; i < sizeof(mpd_cmd_strs) / sizeof(mpd_cmd_strs[0]); i++) 48 | if (!strncmp(cmd, mpd_cmd_strs[i], strlen(mpd_cmd_strs[i]))) 49 | return i; 50 | 51 | return -1; 52 | } 53 | 54 | int callback_mpd(struct mg_connection *c) { 55 | enum mpd_cmd_ids cmd_id = get_cmd_id(c->content); 56 | size_t n = 0; 57 | unsigned int uint_buf, uint_buf_2; 58 | int int_buf; 59 | char *p_charbuf = NULL, *token; 60 | 61 | if (!c->connection_param) 62 | c->connection_param = calloc(1, sizeof(struct t_mpd_client_session)); 63 | 64 | struct t_mpd_client_session *s = (struct t_mpd_client_session *)c->connection_param; 65 | 66 | if (!s->authorized && (cmd_id != MPD_API_AUTHORIZE)) { 67 | n = snprintf(mpd.buf, MAX_SIZE, "{\"type\":\"error\",\"data\":\"not authorized\"}"); 68 | mg_websocket_write(c, 1, mpd.buf, n); 69 | 70 | return MG_TRUE; 71 | } 72 | 73 | if (cmd_id == -1) 74 | return MG_TRUE; 75 | 76 | if (mpd.conn_state != MPD_CONNECTED && cmd_id != MPD_API_SET_MPDHOST && 77 | cmd_id != MPD_API_GET_MPDHOST && cmd_id != MPD_API_SET_MPDPASS) 78 | return MG_TRUE; 79 | 80 | switch (cmd_id) { 81 | case MPD_API_AUTHORIZE: 82 | p_charbuf = strdup(c->content); 83 | if (strcmp(strtok(p_charbuf, ","), "MPD_API_AUTHORIZE")) 84 | goto out_authorize; 85 | 86 | if ((token = strtok(NULL, ",")) == NULL) 87 | goto out_authorize; 88 | 89 | free(p_charbuf); 90 | p_charbuf = strdup(c->content); 91 | s->auth_token = strdup(get_arg1(p_charbuf)); 92 | if (!strcmp(mpd.wss_auth_token, s->auth_token)) 93 | s->authorized = 1; 94 | 95 | n = snprintf(mpd.buf, MAX_SIZE, "{\"type\":\"authorized\", \"data\":\"%s\"}", 96 | s->authorized ? "true" : "false"); 97 | out_authorize: 98 | free(p_charbuf); 99 | break; 100 | case MPD_API_UPDATE_DB: 101 | mpd_run_update(mpd.conn, NULL); 102 | break; 103 | case MPD_API_SET_PAUSE: 104 | mpd_run_toggle_pause(mpd.conn); 105 | break; 106 | case MPD_API_SET_PREV: 107 | mpd_run_previous(mpd.conn); 108 | break; 109 | case MPD_API_SET_NEXT: 110 | mpd_run_next(mpd.conn); 111 | break; 112 | case MPD_API_SET_PLAY: 113 | mpd_run_play(mpd.conn); 114 | break; 115 | case MPD_API_SET_STOP: 116 | mpd_run_stop(mpd.conn); 117 | break; 118 | case MPD_API_RM_ALL: 119 | mpd_run_clear(mpd.conn); 120 | break; 121 | case MPD_API_RM_TRACK: 122 | if (sscanf(c->content, "MPD_API_RM_TRACK,%u", &uint_buf)) 123 | mpd_run_delete_id(mpd.conn, uint_buf); 124 | break; 125 | case MPD_API_RM_RANGE: 126 | if (sscanf(c->content, "MPD_API_RM_RANGE,%u,%u", &uint_buf, &uint_buf_2)) 127 | mpd_run_delete_range(mpd.conn, uint_buf, uint_buf_2); 128 | break; 129 | case MPD_API_MOVE_TRACK: 130 | if (sscanf(c->content, "MPD_API_MOVE_TRACK,%u,%u", &uint_buf, &uint_buf_2) == 2) { 131 | uint_buf -= 1; 132 | uint_buf_2 -= 1; 133 | mpd_run_move(mpd.conn, uint_buf, uint_buf_2); 134 | } 135 | break; 136 | case MPD_API_PLAY_TRACK: 137 | if (sscanf(c->content, "MPD_API_PLAY_TRACK,%u", &uint_buf)) 138 | mpd_run_play_id(mpd.conn, uint_buf); 139 | break; 140 | case MPD_API_TOGGLE_RANDOM: 141 | if (sscanf(c->content, "MPD_API_TOGGLE_RANDOM,%u", &uint_buf)) 142 | mpd_run_random(mpd.conn, uint_buf); 143 | break; 144 | case MPD_API_TOGGLE_REPEAT: 145 | if (sscanf(c->content, "MPD_API_TOGGLE_REPEAT,%u", &uint_buf)) 146 | mpd_run_repeat(mpd.conn, uint_buf); 147 | break; 148 | case MPD_API_TOGGLE_CONSUME: 149 | if (sscanf(c->content, "MPD_API_TOGGLE_CONSUME,%u", &uint_buf)) 150 | mpd_run_consume(mpd.conn, uint_buf); 151 | break; 152 | case MPD_API_TOGGLE_SINGLE: 153 | if (sscanf(c->content, "MPD_API_TOGGLE_SINGLE,%u", &uint_buf)) 154 | mpd_run_single(mpd.conn, uint_buf); 155 | break; 156 | case MPD_API_TOGGLE_CROSSFADE: 157 | if (sscanf(c->content, "MPD_API_TOGGLE_CROSSFADE,%u", &uint_buf)) 158 | mpd_run_crossfade(mpd.conn, uint_buf); 159 | break; 160 | case MPD_API_GET_OUTPUTS: 161 | mpd.buf_size = mpd_put_outputs(mpd.buf, 1); 162 | c->callback_param = NULL; 163 | mpd_notify_callback(c, MG_POLL); 164 | break; 165 | case MPD_API_TOGGLE_OUTPUT: 166 | if (sscanf(c->content, "MPD_API_TOGGLE_OUTPUT,%u,%u", &uint_buf, &uint_buf_2)) { 167 | if (uint_buf_2) 168 | mpd_run_enable_output(mpd.conn, uint_buf); 169 | else 170 | mpd_run_disable_output(mpd.conn, uint_buf); 171 | } 172 | break; 173 | case MPD_API_SET_VOLUME: 174 | if (sscanf(c->content, "MPD_API_SET_VOLUME,%ud", &uint_buf) && uint_buf <= 100) 175 | mpd_run_set_volume(mpd.conn, uint_buf); 176 | break; 177 | case MPD_API_SET_SEEK: 178 | if (sscanf(c->content, "MPD_API_SET_SEEK,%u,%u", &uint_buf, &uint_buf_2)) 179 | mpd_run_seek_id(mpd.conn, uint_buf, uint_buf_2); 180 | break; 181 | case MPD_API_GET_QUEUE: 182 | if (sscanf(c->content, "MPD_API_GET_QUEUE,%u", &uint_buf)) 183 | n = mpd_put_queue(mpd.buf, uint_buf); 184 | break; 185 | case MPD_API_GET_BROWSE: 186 | p_charbuf = strdup(c->content); 187 | if (strcmp(strtok(p_charbuf, ","), "MPD_API_GET_BROWSE")) 188 | goto out_browse; 189 | 190 | uint_buf = strtoul(strtok(NULL, ","), NULL, 10); 191 | if ((token = strtok(NULL, ",")) == NULL) 192 | goto out_browse; 193 | 194 | free(p_charbuf); 195 | p_charbuf = strdup(c->content); 196 | n = mpd_put_browse(mpd.buf, get_arg2(p_charbuf), uint_buf); 197 | out_browse: 198 | free(p_charbuf); 199 | break; 200 | case MPD_API_ADD_TRACK: 201 | p_charbuf = strdup(c->content); 202 | if (strcmp(strtok(p_charbuf, ","), "MPD_API_ADD_TRACK")) 203 | goto out_add_track; 204 | 205 | if ((token = strtok(NULL, ",")) == NULL) 206 | goto out_add_track; 207 | 208 | free(p_charbuf); 209 | p_charbuf = strdup(c->content); 210 | mpd_run_add(mpd.conn, get_arg1(p_charbuf)); 211 | out_add_track: 212 | free(p_charbuf); 213 | break; 214 | case MPD_API_ADD_PLAY_TRACK: 215 | p_charbuf = strdup(c->content); 216 | if (strcmp(strtok(p_charbuf, ","), "MPD_API_ADD_PLAY_TRACK")) 217 | goto out_play_track; 218 | 219 | if ((token = strtok(NULL, ",")) == NULL) 220 | goto out_play_track; 221 | 222 | free(p_charbuf); 223 | p_charbuf = strdup(c->content); 224 | int_buf = mpd_run_add_id(mpd.conn, get_arg1(p_charbuf)); 225 | if (int_buf != -1) 226 | mpd_run_play_id(mpd.conn, int_buf); 227 | out_play_track: 228 | free(p_charbuf); 229 | break; 230 | case MPD_API_ADD_PLAYLIST: 231 | p_charbuf = strdup(c->content); 232 | if (strcmp(strtok(p_charbuf, ","), "MPD_API_ADD_PLAYLIST")) 233 | goto out_playlist; 234 | 235 | if ((token = strtok(NULL, ",")) == NULL) 236 | goto out_playlist; 237 | 238 | free(p_charbuf); 239 | p_charbuf = strdup(c->content); 240 | mpd_run_load(mpd.conn, get_arg1(p_charbuf)); 241 | out_playlist: 242 | free(p_charbuf); 243 | break; 244 | case MPD_API_SAVE_QUEUE: 245 | p_charbuf = strdup(c->content); 246 | if (strcmp(strtok(p_charbuf, ","), "MPD_API_SAVE_QUEUE")) 247 | goto out_save_queue; 248 | 249 | if ((token = strtok(NULL, ",")) == NULL) 250 | goto out_save_queue; 251 | 252 | free(p_charbuf); 253 | p_charbuf = strdup(c->content); 254 | mpd_run_save(mpd.conn, get_arg1(p_charbuf)); 255 | out_save_queue: 256 | free(p_charbuf); 257 | break; 258 | case MPD_API_SEARCH: 259 | p_charbuf = strdup(c->content); 260 | if (strcmp(strtok(p_charbuf, ","), "MPD_API_SEARCH")) 261 | goto out_search; 262 | 263 | if ((token = strtok(NULL, ",")) == NULL) 264 | goto out_search; 265 | 266 | free(p_charbuf); 267 | p_charbuf = strdup(c->content); 268 | n = mpd_search(mpd.buf, get_arg1(p_charbuf)); 269 | out_search: 270 | free(p_charbuf); 271 | break; 272 | case MPD_API_SEND_MESSAGE: 273 | p_charbuf = strdup(c->content); 274 | if (strcmp(strtok(p_charbuf, ","), "MPD_API_SEND_MESSAGE")) 275 | goto out_send_message; 276 | 277 | if ((token = strtok(NULL, ",")) == NULL) 278 | goto out_send_message; 279 | 280 | free(p_charbuf); 281 | p_charbuf = strdup(get_arg1(c->content)); 282 | 283 | if (strtok(p_charbuf, ",") == NULL) 284 | goto out_send_message; 285 | 286 | if ((token = strtok(NULL, ",")) == NULL) 287 | goto out_send_message; 288 | 289 | mpd_run_send_message(mpd.conn, p_charbuf, token); 290 | out_send_message: 291 | free(p_charbuf); 292 | break; 293 | case MPD_API_GET_CHANNELS: 294 | mpd.buf_size = mpd_put_channels(mpd.buf); 295 | c->callback_param = NULL; 296 | mpd_notify_callback(c, MG_POLL); 297 | break; 298 | #ifdef WITH_MPD_HOST_CHANGE 299 | /* Commands allowed when disconnected from MPD server */ 300 | case MPD_API_SET_MPDHOST: 301 | int_buf = 0; 302 | p_charbuf = strdup(c->content); 303 | if (strcmp(strtok(p_charbuf, ","), "MPD_API_SET_MPDHOST")) 304 | goto out_host_change; 305 | 306 | if ((int_buf = strtol(strtok(NULL, ","), NULL, 10)) <= 0) 307 | goto out_host_change; 308 | 309 | if ((token = strtok(NULL, ",")) == NULL) 310 | goto out_host_change; 311 | 312 | strncpy(mpd.host, token, sizeof(mpd.host)); 313 | mpd.port = int_buf; 314 | mpd.conn_state = MPD_RECONNECT; 315 | free(p_charbuf); 316 | return MG_TRUE; 317 | out_host_change: 318 | free(p_charbuf); 319 | break; 320 | case MPD_API_GET_MPDHOST: 321 | n = snprintf(mpd.buf, MAX_SIZE, 322 | "{\"type\":\"mpdhost\", \"data\": " 323 | "{\"host\" : \"%s\", \"port\": \"%d\", \"passwort_set\": %s}" 324 | "}", 325 | mpd.host, mpd.port, mpd.password ? "true" : "false"); 326 | break; 327 | case MPD_API_SET_MPDPASS: 328 | p_charbuf = strdup(c->content); 329 | if (strcmp(strtok(p_charbuf, ","), "MPD_API_SET_MPDPASS")) 330 | goto out_set_pass; 331 | 332 | if ((token = strtok(NULL, ",")) == NULL) 333 | goto out_set_pass; 334 | 335 | if (mpd.password) 336 | free(mpd.password); 337 | 338 | mpd.password = strdup(token); 339 | mpd.conn_state = MPD_RECONNECT; 340 | free(p_charbuf); 341 | return MG_TRUE; 342 | out_set_pass: 343 | free(p_charbuf); 344 | break; 345 | #endif 346 | } 347 | 348 | if (mpd.conn_state == MPD_CONNECTED && 349 | mpd_connection_get_error(mpd.conn) != MPD_ERROR_SUCCESS) { 350 | n = snprintf(mpd.buf, MAX_SIZE, "{\"type\":\"error\", \"data\": \"%s\"}", 351 | mpd_connection_get_error_message(mpd.conn)); 352 | 353 | /* Try to recover error */ 354 | if (!mpd_connection_clear_error(mpd.conn)) 355 | mpd.conn_state = MPD_FAILURE; 356 | } 357 | 358 | if (n > 0) 359 | mg_websocket_write(c, 1, mpd.buf, n); 360 | 361 | return MG_TRUE; 362 | } 363 | 364 | int mpd_close_handler(struct mg_connection *c) { 365 | /* Cleanup session data */ 366 | if (c->connection_param) { 367 | struct t_mpd_client_session *s = (struct t_mpd_client_session *)c->connection_param; 368 | if (s->auth_token) 369 | free(s->auth_token); 370 | free(c->connection_param); 371 | } 372 | 373 | return 0; 374 | } 375 | 376 | static int mpd_notify_callback(struct mg_connection *c, enum mg_event ev) { 377 | size_t n; 378 | 379 | if (!c->is_websocket) 380 | return MG_TRUE; 381 | 382 | if (!c->connection_param) 383 | return MG_TRUE; 384 | 385 | struct t_mpd_client_session *s = (struct t_mpd_client_session *)c->connection_param; 386 | 387 | if (!s->authorized) 388 | return MG_TRUE; 389 | 390 | if (c->callback_param) { 391 | /* error message? */ 392 | n = snprintf(mpd.buf, MAX_SIZE, "{\"type\":\"error\",\"data\":\"%s\"}", 393 | (const char *)c->callback_param); 394 | 395 | mg_websocket_write(c, 1, mpd.buf, n); 396 | return MG_TRUE; 397 | } 398 | 399 | if (mpd.conn_state != MPD_CONNECTED) { 400 | n = snprintf(mpd.buf, MAX_SIZE, "{\"type\":\"disconnected\"}"); 401 | mg_websocket_write(c, 1, mpd.buf, n); 402 | } else { 403 | mg_websocket_write(c, 1, mpd.buf, mpd.buf_size); 404 | 405 | if (s->song_id != mpd.song_id) { 406 | n = mpd_put_current_song(mpd.buf); 407 | mg_websocket_write(c, 1, mpd.buf, n); 408 | s->song_id = mpd.song_id; 409 | } 410 | 411 | if (s->queue_version != mpd.queue_version) { 412 | n = snprintf(mpd.buf, MAX_SIZE, "{\"type\":\"update_queue\"}"); 413 | mg_websocket_write(c, 1, mpd.buf, n); 414 | s->queue_version = mpd.queue_version; 415 | } 416 | } 417 | 418 | return MG_TRUE; 419 | } 420 | 421 | void mpd_poll(struct mg_server *s) { 422 | switch (mpd.conn_state) { 423 | case MPD_DISCONNECTED: 424 | /* Try to connect */ 425 | fprintf(stdout, "MPD Connecting to %s:%d\n", mpd.host, mpd.port); 426 | mpd.conn = mpd_connection_new(mpd.host, mpd.port, 3000); 427 | if (mpd.conn == NULL) { 428 | fprintf(stderr, "Out of memory."); 429 | mpd.conn_state = MPD_FAILURE; 430 | return; 431 | } 432 | 433 | if (mpd_connection_get_error(mpd.conn) != MPD_ERROR_SUCCESS) { 434 | fprintf(stderr, "MPD connection: %s\n", mpd_connection_get_error_message(mpd.conn)); 435 | for (struct mg_connection *c = mg_next(s, NULL); c != NULL; c = mg_next(s, c)) { 436 | c->callback_param = (void *)mpd_connection_get_error_message(mpd.conn); 437 | mpd_notify_callback(c, MG_POLL); 438 | } 439 | mpd.conn_state = MPD_FAILURE; 440 | return; 441 | } 442 | 443 | if (mpd.password && !mpd_run_password(mpd.conn, mpd.password)) { 444 | fprintf(stderr, "MPD connection: %s\n", mpd_connection_get_error_message(mpd.conn)); 445 | for (struct mg_connection *c = mg_next(s, NULL); c != NULL; c = mg_next(s, c)) { 446 | c->callback_param = (void *)mpd_connection_get_error_message(mpd.conn); 447 | mpd_notify_callback(c, MG_POLL); 448 | } 449 | mpd.conn_state = MPD_FAILURE; 450 | return; 451 | } 452 | 453 | fprintf(stderr, "MPD connected.\n"); 454 | mpd_connection_set_timeout(mpd.conn, 10000); 455 | mpd.conn_state = MPD_CONNECTED; 456 | /* write outputs */ 457 | mpd.buf_size = mpd_put_outputs(mpd.buf, 1); 458 | for (struct mg_connection *c = mg_next(s, NULL); c != NULL; c = mg_next(s, c)) { 459 | c->callback_param = NULL; 460 | mpd_notify_callback(c, MG_POLL); 461 | } 462 | break; 463 | 464 | case MPD_FAILURE: 465 | fprintf(stderr, "MPD connection failed.\n"); 466 | 467 | case MPD_DISCONNECT: 468 | case MPD_RECONNECT: 469 | if (mpd.conn != NULL) 470 | mpd_connection_free(mpd.conn); 471 | mpd.conn = NULL; 472 | mpd.conn_state = MPD_DISCONNECTED; 473 | break; 474 | 475 | case MPD_CONNECTED: 476 | mpd.buf_size = mpd_put_state(mpd.buf, &mpd.song_id, &mpd.queue_version); 477 | for (struct mg_connection *c = mg_next(s, NULL); c != NULL; c = mg_next(s, c)) { 478 | c->callback_param = NULL; 479 | mpd_notify_callback(c, MG_POLL); 480 | } 481 | mpd.buf_size = mpd_put_outputs(mpd.buf, 0); 482 | for (struct mg_connection *c = mg_next(s, NULL); c != NULL; c = mg_next(s, c)) { 483 | c->callback_param = NULL; 484 | mpd_notify_callback(c, MG_POLL); 485 | } 486 | mpd.buf_size = mpd_put_channels(mpd.buf); 487 | for (struct mg_connection *c = mg_next(s, NULL); c != NULL; c = mg_next(s, c)) { 488 | c->callback_param = NULL; 489 | mpd_notify_callback(c, MG_POLL); 490 | } 491 | break; 492 | } 493 | } 494 | 495 | char *mpd_get_title(struct mpd_song const *song) { 496 | char *str; 497 | 498 | str = (char *)mpd_song_get_tag(song, MPD_TAG_TITLE, 0); 499 | if (str == NULL) { 500 | str = basename((char *)mpd_song_get_uri(song)); 501 | } 502 | 503 | return str; 504 | } 505 | 506 | char *mpd_get_album(struct mpd_song const *song) { 507 | char *str; 508 | 509 | str = (char *)mpd_song_get_tag(song, MPD_TAG_ALBUM, 0); 510 | if (str == NULL) { 511 | str = "-"; 512 | } 513 | 514 | return str; 515 | } 516 | 517 | char *mpd_get_artist(struct mpd_song const *song) { 518 | char *str; 519 | 520 | str = (char *)mpd_song_get_tag(song, MPD_TAG_ARTIST, 0); 521 | if (str == NULL) { 522 | str = "-"; 523 | } 524 | 525 | return str; 526 | } 527 | 528 | char *mpd_get_year(struct mpd_song const *song) { 529 | char *str; 530 | 531 | str = (char *)mpd_song_get_tag(song, MPD_TAG_DATE, 0); 532 | if (str == NULL) { 533 | str = "-"; 534 | } 535 | 536 | return str; 537 | } 538 | 539 | int mpd_put_state(char *buffer, int *current_song_id, unsigned *queue_version) { 540 | struct mpd_status *status; 541 | int len; 542 | 543 | status = mpd_run_status(mpd.conn); 544 | if (!status) { 545 | fprintf(stderr, "MPD mpd_run_status: %s\n", mpd_connection_get_error_message(mpd.conn)); 546 | mpd.conn_state = MPD_FAILURE; 547 | return 0; 548 | } 549 | 550 | len = snprintf(buffer, MAX_SIZE, 551 | "{\"type\":\"state\", \"data\":{" 552 | " \"state\":%d, \"volume\":%d, \"repeat\":%d," 553 | " \"single\":%d, \"crossfade\":%d, \"consume\":%d, \"random\":%d, " 554 | " \"songpos\": %d, \"elapsedTime\": %d, \"totalTime\":%d, " 555 | " \"currentsongid\": %d" 556 | "}}", 557 | mpd_status_get_state(status), mpd_status_get_volume(status), 558 | mpd_status_get_repeat(status), mpd_status_get_single(status), 559 | mpd_status_get_crossfade(status), mpd_status_get_consume(status), 560 | mpd_status_get_random(status), mpd_status_get_song_pos(status), 561 | mpd_status_get_elapsed_time(status), mpd_status_get_total_time(status), 562 | mpd_status_get_song_id(status)); 563 | 564 | *current_song_id = mpd_status_get_song_id(status); 565 | *queue_version = mpd_status_get_queue_version(status); 566 | mpd_status_free(status); 567 | return len; 568 | } 569 | 570 | int mpd_put_outputs(char *buffer, int names) { 571 | struct mpd_output *out; 572 | int nout; 573 | char *str, *strend; 574 | 575 | str = buffer; 576 | strend = buffer + MAX_SIZE; 577 | str += snprintf(str, strend - str, "{\"type\":\"%s\", \"data\":{", 578 | names ? "outputnames" : "outputs"); 579 | 580 | mpd_send_outputs(mpd.conn); 581 | nout = 0; 582 | while ((out = mpd_recv_output(mpd.conn)) != NULL) { 583 | if (nout++) 584 | *str++ = ','; 585 | if (names) 586 | str += snprintf(str, strend - str, " \"%d\":\"%s\"", mpd_output_get_id(out), 587 | mpd_output_get_name(out)); 588 | else 589 | str += snprintf(str, strend - str, " \"%d\":%d", mpd_output_get_id(out), 590 | mpd_output_get_enabled(out)); 591 | mpd_output_free(out); 592 | } 593 | if (!mpd_response_finish(mpd.conn)) { 594 | fprintf(stderr, "MPD outputs: %s\n", mpd_connection_get_error_message(mpd.conn)); 595 | mpd_connection_clear_error(mpd.conn); 596 | return 0; 597 | } 598 | str += snprintf(str, strend - str, " }}"); 599 | return str - buffer; 600 | } 601 | 602 | int mpd_put_channels(char *buffer) { 603 | struct mpd_pair *channel; 604 | int nchan; 605 | char *str, *strend; 606 | 607 | str = buffer; 608 | strend = buffer + MAX_SIZE; 609 | str += snprintf(str, strend - str, "{\"type\":\"%s\", \"data\":{", "channels"); 610 | 611 | mpd_send_channels(mpd.conn); 612 | nchan = 0; 613 | while ((channel = mpd_recv_channel_pair(mpd.conn)) != NULL) { 614 | if (nchan++) 615 | *str++ = ','; 616 | str += snprintf(str, strend - str, " \"%d\":\"%s\"", nchan, channel->value); 617 | mpd_return_pair(mpd.conn, channel); 618 | } 619 | if (!mpd_response_finish(mpd.conn)) { 620 | fprintf(stderr, "MPD outputs: %s\n", mpd_connection_get_error_message(mpd.conn)); 621 | mpd_connection_clear_error(mpd.conn); 622 | return 0; 623 | } 624 | str += snprintf(str, strend - str, " }}"); 625 | return str - buffer; 626 | } 627 | 628 | int mpd_put_current_song(char *buffer) { 629 | char *cur = buffer; 630 | const char *end = buffer + MAX_SIZE; 631 | struct mpd_song *song; 632 | 633 | song = mpd_run_current_song(mpd.conn); 634 | if (song == NULL) 635 | return 0; 636 | 637 | cur += json_emit_raw_str(cur, end - cur, "{\"type\": \"song_change\", \"data\":{\"pos\":"); 638 | cur += json_emit_int(cur, end - cur, mpd_song_get_pos(song)); 639 | cur += json_emit_raw_str(cur, end - cur, ",\"title\":"); 640 | cur += json_emit_quoted_str(cur, end - cur, mpd_get_title(song)); 641 | cur += json_emit_raw_str(cur, end - cur, ",\"artist\":"); 642 | cur += json_emit_quoted_str(cur, end - cur, mpd_get_artist(song)); 643 | cur += json_emit_raw_str(cur, end - cur, ",\"album\":"); 644 | cur += json_emit_quoted_str(cur, end - cur, mpd_get_album(song)); 645 | 646 | cur += json_emit_raw_str(cur, end - cur, "}}"); 647 | mpd_song_free(song); 648 | mpd_response_finish(mpd.conn); 649 | 650 | return cur - buffer; 651 | } 652 | 653 | int mpd_put_queue(char *buffer, unsigned int offset) { 654 | char *cur = buffer; 655 | const char *end = buffer + MAX_SIZE; 656 | struct mpd_entity *entity; 657 | unsigned long totalTime = 0; 658 | 659 | if (!mpd_send_list_queue_range_meta(mpd.conn, offset, offset + MAX_ELEMENTS_PER_PAGE)) 660 | RETURN_ERROR_AND_RECOVER("mpd_send_list_queue_meta"); 661 | 662 | cur += json_emit_raw_str(cur, end - cur, "{\"type\":\"queue\",\"data\":[ "); 663 | 664 | while ((entity = mpd_recv_entity(mpd.conn)) != NULL) { 665 | const struct mpd_song *song; 666 | unsigned int drtn; 667 | 668 | if (mpd_entity_get_type(entity) == MPD_ENTITY_TYPE_SONG) { 669 | song = mpd_entity_get_song(entity); 670 | drtn = mpd_song_get_duration(song); 671 | 672 | cur += json_emit_raw_str(cur, end - cur, "{\"id\":"); 673 | cur += json_emit_int(cur, end - cur, mpd_song_get_id(song)); 674 | cur += json_emit_raw_str(cur, end - cur, ",\"pos\":"); 675 | cur += json_emit_int(cur, end - cur, mpd_song_get_pos(song)); 676 | cur += json_emit_raw_str(cur, end - cur, ",\"duration\":"); 677 | cur += json_emit_int(cur, end - cur, drtn); 678 | cur += json_emit_raw_str(cur, end - cur, ",\"artist\":"); 679 | cur += json_emit_quoted_str(cur, end - cur, mpd_get_artist(song)); 680 | cur += json_emit_raw_str(cur, end - cur, ",\"album\":"); 681 | cur += json_emit_quoted_str(cur, end - cur, mpd_get_album(song)); 682 | cur += json_emit_raw_str(cur, end - cur, ",\"title\":"); 683 | cur += json_emit_quoted_str(cur, end - cur, mpd_get_title(song)); 684 | cur += json_emit_raw_str(cur, end - cur, ",\"artist\":"); 685 | cur += json_emit_quoted_str(cur, end - cur, mpd_get_artist(song)); 686 | cur += json_emit_raw_str(cur, end - cur, ",\"album\":"); 687 | cur += json_emit_quoted_str(cur, end - cur, mpd_get_album(song)); 688 | cur += json_emit_raw_str(cur, end - cur, "},"); 689 | 690 | totalTime += drtn; 691 | } 692 | mpd_entity_free(entity); 693 | } 694 | 695 | /* remove last ',' */ 696 | cur--; 697 | 698 | cur += json_emit_raw_str(cur, end - cur, "],\"totalTime\":"); 699 | cur += json_emit_int(cur, end - cur, totalTime); 700 | cur += json_emit_raw_str(cur, end - cur, "}"); 701 | return cur - buffer; 702 | } 703 | 704 | int mpd_put_browse(char *buffer, char *path, unsigned int offset) { 705 | char *cur = buffer; 706 | const char *end = buffer + MAX_SIZE; 707 | struct mpd_entity *entity; 708 | unsigned int entity_count = 0; 709 | 710 | if (!mpd_send_list_meta(mpd.conn, path)) 711 | RETURN_ERROR_AND_RECOVER("mpd_send_list_meta"); 712 | 713 | cur += json_emit_raw_str(cur, end - cur, "{\"type\":\"browse\",\"data\":[ "); 714 | 715 | while ((entity = mpd_recv_entity(mpd.conn)) != NULL) { 716 | const struct mpd_song *song; 717 | const struct mpd_directory *dir; 718 | const struct mpd_playlist *pl; 719 | 720 | if (offset > entity_count) { 721 | mpd_entity_free(entity); 722 | entity_count++; 723 | continue; 724 | } else if (offset + MAX_ELEMENTS_PER_PAGE - 1 < entity_count) { 725 | mpd_entity_free(entity); 726 | cur += json_emit_raw_str(cur, end - cur, "{\"type\":\"wrap\",\"count\":"); 727 | cur += json_emit_int(cur, end - cur, entity_count); 728 | cur += json_emit_raw_str(cur, end - cur, "} "); 729 | break; 730 | } 731 | 732 | switch (mpd_entity_get_type(entity)) { 733 | case MPD_ENTITY_TYPE_UNKNOWN: 734 | break; 735 | 736 | case MPD_ENTITY_TYPE_SONG: 737 | song = mpd_entity_get_song(entity); 738 | cur += json_emit_raw_str(cur, end - cur, "{\"type\":\"song\",\"uri\":"); 739 | cur += json_emit_quoted_str(cur, end - cur, mpd_song_get_uri(song)); 740 | cur += json_emit_raw_str(cur, end - cur, ",\"album\":"); 741 | cur += json_emit_quoted_str(cur, end - cur, mpd_get_album(song)); 742 | cur += json_emit_raw_str(cur, end - cur, ",\"artist\":"); 743 | cur += json_emit_quoted_str(cur, end - cur, mpd_get_artist(song)); 744 | cur += json_emit_raw_str(cur, end - cur, ",\"duration\":"); 745 | cur += json_emit_int(cur, end - cur, mpd_song_get_duration(song)); 746 | cur += json_emit_raw_str(cur, end - cur, ",\"title\":"); 747 | cur += json_emit_quoted_str(cur, end - cur, mpd_get_title(song)); 748 | cur += json_emit_raw_str(cur, end - cur, "},"); 749 | break; 750 | 751 | case MPD_ENTITY_TYPE_DIRECTORY: 752 | dir = mpd_entity_get_directory(entity); 753 | 754 | cur += json_emit_raw_str(cur, end - cur, "{\"type\":\"directory\",\"dir\":"); 755 | cur += json_emit_quoted_str(cur, end - cur, mpd_directory_get_path(dir)); 756 | cur += json_emit_raw_str(cur, end - cur, "},"); 757 | break; 758 | 759 | case MPD_ENTITY_TYPE_PLAYLIST: 760 | pl = mpd_entity_get_playlist(entity); 761 | cur += json_emit_raw_str(cur, end - cur, "{\"type\":\"playlist\",\"plist\":"); 762 | cur += json_emit_quoted_str(cur, end - cur, mpd_playlist_get_path(pl)); 763 | cur += json_emit_raw_str(cur, end - cur, "},"); 764 | break; 765 | } 766 | mpd_entity_free(entity); 767 | entity_count++; 768 | } 769 | 770 | if (mpd_connection_get_error(mpd.conn) != MPD_ERROR_SUCCESS || !mpd_response_finish(mpd.conn)) { 771 | fprintf(stderr, "MPD mpd_send_list_meta: %s\n", mpd_connection_get_error_message(mpd.conn)); 772 | mpd.conn_state = MPD_FAILURE; 773 | return 0; 774 | } 775 | 776 | /* remove last ',' */ 777 | cur--; 778 | 779 | cur += json_emit_raw_str(cur, end - cur, "]}"); 780 | return cur - buffer; 781 | } 782 | 783 | int mpd_search(char *buffer, char *searchstr) { 784 | int i = 0; 785 | char *cur = buffer; 786 | const char *end = buffer + MAX_SIZE; 787 | struct mpd_song *song; 788 | 789 | if (mpd_search_db_songs(mpd.conn, false) == false) 790 | RETURN_ERROR_AND_RECOVER("mpd_search_db_songs"); 791 | else if (mpd_search_add_any_tag_constraint(mpd.conn, MPD_OPERATOR_DEFAULT, searchstr) == false) 792 | RETURN_ERROR_AND_RECOVER("mpd_search_add_any_tag_constraint"); 793 | else if (mpd_search_commit(mpd.conn) == false) 794 | RETURN_ERROR_AND_RECOVER("mpd_search_commit"); 795 | else { 796 | cur += json_emit_raw_str(cur, end - cur, "{\"type\":\"search\",\"data\":[ "); 797 | 798 | while ((song = mpd_recv_song(mpd.conn)) != NULL) { 799 | cur += json_emit_raw_str(cur, end - cur, "{\"type\":\"song\",\"uri\":"); 800 | cur += json_emit_quoted_str(cur, end - cur, mpd_song_get_uri(song)); 801 | cur += json_emit_raw_str(cur, end - cur, ",\"album\":"); 802 | cur += json_emit_quoted_str(cur, end - cur, mpd_get_album(song)); 803 | cur += json_emit_raw_str(cur, end - cur, ",\"artist\":"); 804 | cur += json_emit_quoted_str(cur, end - cur, mpd_get_artist(song)); 805 | cur += json_emit_raw_str(cur, end - cur, ",\"duration\":"); 806 | cur += json_emit_int(cur, end - cur, mpd_song_get_duration(song)); 807 | cur += json_emit_raw_str(cur, end - cur, ",\"title\":"); 808 | cur += json_emit_quoted_str(cur, end - cur, mpd_get_title(song)); 809 | cur += json_emit_raw_str(cur, end - cur, ",\"artist\":"); 810 | cur += json_emit_quoted_str(cur, end - cur, mpd_get_artist(song)); 811 | cur += json_emit_raw_str(cur, end - cur, ",\"album\":"); 812 | cur += json_emit_quoted_str(cur, end - cur, mpd_get_album(song)); 813 | cur += json_emit_raw_str(cur, end - cur, "},"); 814 | mpd_song_free(song); 815 | 816 | /* Maximum results */ 817 | if (i++ >= 300) { 818 | cur += json_emit_raw_str(cur, end - cur, "{\"type\":\"wrap\"},"); 819 | break; 820 | } 821 | } 822 | 823 | /* remove last ',' */ 824 | cur--; 825 | 826 | cur += json_emit_raw_str(cur, end - cur, "]}"); 827 | } 828 | return cur - buffer; 829 | } 830 | 831 | void mpd_disconnect() { 832 | mpd.conn_state = MPD_DISCONNECT; 833 | mpd_poll(NULL); 834 | } 835 | -------------------------------------------------------------------------------- /src/mpd_client.h: -------------------------------------------------------------------------------- 1 | /* ympd 2 | (c) 2013-2014 Andrew Karpow 3 | This project's homepage is: http://www.ympd.org 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; version 2 of the License. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along 15 | with this program; if not, write to the Free Software Foundation, Inc., 16 | Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | */ 18 | 19 | #ifndef __MPD_CLIENT_H__ 20 | #define __MPD_CLIENT_H__ 21 | 22 | #include "mongoose.h" 23 | 24 | #define RETURN_ERROR_AND_RECOVER(X) \ 25 | do { \ 26 | fprintf(stderr, "MPD X: %s\n", mpd_connection_get_error_message(mpd.conn)); \ 27 | cur += snprintf(cur, end - cur, "{\"type\":\"error\",\"data\":\"%s\"}", \ 28 | mpd_connection_get_error_message(mpd.conn)); \ 29 | if (!mpd_connection_clear_error(mpd.conn)) \ 30 | mpd.conn_state = MPD_FAILURE; \ 31 | return cur - buffer; \ 32 | } while (0) 33 | 34 | #define MAX_SIZE 1024 * 100 35 | #define MAX_ELEMENTS_PER_PAGE 512 36 | 37 | #define WSS_AUTH_TOKEN_SIZE 50 38 | 39 | #define GEN_ENUM(X) X, 40 | #define GEN_STR(X) #X, 41 | #define MPD_CMDS(X) \ 42 | X(MPD_API_GET_QUEUE) \ 43 | X(MPD_API_GET_BROWSE) \ 44 | X(MPD_API_GET_MPDHOST) \ 45 | X(MPD_API_ADD_TRACK) \ 46 | X(MPD_API_ADD_PLAY_TRACK) \ 47 | X(MPD_API_ADD_PLAYLIST) \ 48 | X(MPD_API_PLAY_TRACK) \ 49 | X(MPD_API_SAVE_QUEUE) \ 50 | X(MPD_API_RM_TRACK) \ 51 | X(MPD_API_RM_RANGE) \ 52 | X(MPD_API_RM_ALL) \ 53 | X(MPD_API_MOVE_TRACK) \ 54 | X(MPD_API_SEARCH) \ 55 | X(MPD_API_GET_CHANNELS) \ 56 | X(MPD_API_SEND_MESSAGE) \ 57 | X(MPD_API_SET_VOLUME) \ 58 | X(MPD_API_SET_PAUSE) \ 59 | X(MPD_API_SET_PLAY) \ 60 | X(MPD_API_SET_STOP) \ 61 | X(MPD_API_SET_SEEK) \ 62 | X(MPD_API_SET_NEXT) \ 63 | X(MPD_API_SET_PREV) \ 64 | X(MPD_API_SET_MPDHOST) \ 65 | X(MPD_API_SET_MPDPASS) \ 66 | X(MPD_API_UPDATE_DB) \ 67 | X(MPD_API_GET_OUTPUTS) \ 68 | X(MPD_API_TOGGLE_OUTPUT) \ 69 | X(MPD_API_TOGGLE_RANDOM) \ 70 | X(MPD_API_TOGGLE_CONSUME) \ 71 | X(MPD_API_TOGGLE_SINGLE) \ 72 | X(MPD_API_TOGGLE_CROSSFADE) \ 73 | X(MPD_API_TOGGLE_REPEAT) \ 74 | X(MPD_API_AUTHORIZE) 75 | 76 | enum mpd_cmd_ids { MPD_CMDS(GEN_ENUM) }; 77 | 78 | enum mpd_conn_states { 79 | MPD_DISCONNECTED, 80 | MPD_FAILURE, 81 | MPD_CONNECTED, 82 | MPD_RECONNECT, 83 | MPD_DISCONNECT 84 | }; 85 | 86 | struct t_mpd { 87 | int port; 88 | int local_port; 89 | char host[128]; 90 | char *password; 91 | char *gpass; 92 | char *wss_auth_token; 93 | 94 | struct mpd_connection *conn; 95 | enum mpd_conn_states conn_state; 96 | 97 | /* Reponse Buffer */ 98 | char buf[MAX_SIZE]; 99 | size_t buf_size; 100 | 101 | int song_id; 102 | unsigned queue_version; 103 | }; 104 | 105 | extern struct t_mpd mpd; 106 | 107 | struct t_mpd_client_session { 108 | int song_id; 109 | unsigned queue_version; 110 | int authorized; 111 | char *auth_token; 112 | }; 113 | 114 | void mpd_poll(struct mg_server *s); 115 | int callback_mpd(struct mg_connection *c); 116 | int mpd_close_handler(struct mg_connection *c); 117 | int mpd_put_state(char *buffer, int *current_song_id, unsigned *queue_version); 118 | int mpd_put_outputs(char *buffer, int putnames); 119 | int mpd_put_channels(char *buffer); 120 | int mpd_put_current_song(char *buffer); 121 | int mpd_put_queue(char *buffer, unsigned int offset); 122 | int mpd_put_browse(char *buffer, char *path, unsigned int offset); 123 | int mpd_search(char *buffer, char *searchstr); 124 | void mpd_disconnect(); 125 | #endif 126 | -------------------------------------------------------------------------------- /src/ympd.c: -------------------------------------------------------------------------------- 1 | /* ympd 2 | (c) 2013-2014 Andrew Karpow 3 | This project's homepage is: http://www.ympd.org 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; version 2 of the License. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License along 15 | with this program; if not, write to the Free Software Foundation, Inc., 16 | Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17 | */ 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | #include "config.h" 28 | #include "http_server.h" 29 | #include "mongoose.h" 30 | #include "mpd_client.h" 31 | 32 | extern char *optarg; 33 | 34 | int force_exit = 0; 35 | 36 | void bye() { 37 | force_exit = 1; 38 | } 39 | 40 | static int server_callback(struct mg_connection *c, enum mg_event ev) { 41 | int result = MG_FALSE; 42 | FILE *fp = NULL; 43 | 44 | switch (ev) { 45 | case MG_CLOSE: 46 | mpd_close_handler(c); 47 | return MG_TRUE; 48 | case MG_REQUEST: 49 | if (c->is_websocket) { 50 | c->content[c->content_len] = '\0'; 51 | if (c->content_len) 52 | return callback_mpd(c); 53 | else 54 | return MG_TRUE; 55 | } else 56 | #ifdef WITH_DYNAMIC_ASSETS 57 | return MG_FALSE; 58 | #else 59 | return callback_http(c); 60 | #endif 61 | case MG_AUTH: 62 | // no auth for websockets since mobile safari does not support it 63 | if ((mpd.gpass == NULL) || (c->is_websocket) || 64 | ((mpd.local_port > 0) && (c->local_port == mpd.local_port))) 65 | return MG_TRUE; 66 | else { 67 | if ((fp = fopen(mpd.gpass, "r")) != NULL) { 68 | result = mg_authorize_digest(c, fp); 69 | fclose(fp); 70 | } 71 | } 72 | return result; 73 | default: 74 | return MG_FALSE; 75 | } 76 | } 77 | 78 | int main(int argc, char **argv) { 79 | int n, option_index = 0; 80 | struct mg_server *server = mg_create_server(NULL, server_callback); 81 | unsigned int current_timer = 0, last_timer = 0; 82 | char *run_as_user = NULL; 83 | char const *error_msg = NULL; 84 | char *webport = "8080"; 85 | 86 | atexit(bye); 87 | #ifdef WITH_DYNAMIC_ASSETS 88 | mg_set_option(server, "document_root", SRC_PATH); 89 | #endif 90 | 91 | mg_set_option(server, "auth_domain", "ympd"); 92 | mpd.port = 6600; 93 | mpd.local_port = 0; 94 | mpd.gpass = NULL; 95 | strcpy(mpd.host, "127.0.0.1"); 96 | 97 | /* clang-format off */ 98 | static struct option long_options[] = { 99 | {"digest", required_argument, 0, 'D'}, 100 | {"host", required_argument, 0, 'h'}, 101 | {"port", required_argument, 0, 'p'}, 102 | {"localport", required_argument, 0, 'l'}, 103 | {"webport", required_argument, 0, 'w'}, 104 | {"user", required_argument, 0, 'u'}, 105 | {"version", no_argument, 0, 'v'}, 106 | {"help", no_argument, 0, 0 }, 107 | {"mpdpass", required_argument, 0, 'm'}, 108 | {0, 0, 0, 0 } 109 | }; 110 | /* clang-format on */ 111 | 112 | while ((n = getopt_long(argc, argv, "D:h:p:l:w:u:d:v:m", long_options, &option_index)) != -1) { 113 | switch (n) { 114 | case 'D': 115 | mpd.gpass = strdup(optarg); 116 | break; 117 | case 'h': 118 | strncpy(mpd.host, optarg, sizeof(mpd.host)); 119 | break; 120 | case 'p': 121 | mpd.port = atoi(optarg); 122 | break; 123 | case 'l': 124 | mpd.local_port = atoi(optarg); 125 | break; 126 | case 'w': 127 | webport = strdup(optarg); 128 | break; 129 | case 'u': 130 | run_as_user = strdup(optarg); 131 | break; 132 | case 'm': 133 | if (strlen(optarg) > 0) 134 | mpd.password = strdup(optarg); 135 | break; 136 | case 'v': 137 | fprintf(stdout, 138 | "ympd %d.%d.%d\n" 139 | "Copyright (C) 2014 Andrew Karpow \n" 140 | "built " __DATE__ 141 | " "__TIME__ 142 | " ("__VERSION__ 143 | ")\n", 144 | YMPD_VERSION_MAJOR, YMPD_VERSION_MINOR, YMPD_VERSION_PATCH); 145 | return EXIT_SUCCESS; 146 | break; 147 | default: 148 | fprintf(stderr, 149 | "Usage: %s [OPTION]...\n\n" 150 | " -D, --digest \tpath to htdigest file for authorization\n" 151 | " \t(realm ympd) [no authorization]\n" 152 | " -h, --host \t\tconnect to mpd at host [localhost]\n" 153 | " -p, --port \t\tconnect to mpd at port [6600]\n" 154 | " -l, --localport \t\tskip authorization for local port\n" 155 | " -w, --webport [ip:]\tlisten interface/port for webserver [8080]\n" 156 | " -u, --user \t\tdrop priviliges to user after socket bind\n" 157 | " -v, --version\t\t\tget version\n" 158 | " -m, --mpdpass \tspecifies the password to use when connecting " 159 | "to mpd\n" 160 | " --help\t\t\t\tthis help\n", 161 | argv[0]); 162 | return EXIT_FAILURE; 163 | } 164 | 165 | if (error_msg) { 166 | fprintf(stderr, "Mongoose error: %s\n", error_msg); 167 | return EXIT_FAILURE; 168 | } 169 | } 170 | 171 | error_msg = mg_set_option(server, "listening_port", webport); 172 | if (error_msg) { 173 | fprintf(stderr, "Mongoose error: %s\n", error_msg); 174 | return EXIT_FAILURE; 175 | } 176 | 177 | /* drop privilges at last to ensure proper port binding */ 178 | if (run_as_user != NULL) { 179 | error_msg = mg_set_option(server, "run_as_user", run_as_user); 180 | free(run_as_user); 181 | if (error_msg) { 182 | fprintf(stderr, "Mongoose error: %s\n", error_msg); 183 | return EXIT_FAILURE; 184 | } 185 | } 186 | 187 | while (!force_exit) { 188 | mg_poll_server(server, 200); 189 | current_timer = time(NULL); 190 | if (current_timer - last_timer) { 191 | last_timer = current_timer; 192 | mpd_poll(server); 193 | } 194 | } 195 | 196 | mpd_disconnect(); 197 | mg_destroy_server(&server); 198 | 199 | return EXIT_SUCCESS; 200 | } 201 | -------------------------------------------------------------------------------- /tools/lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | repo="$(git rev-parse --show-toplevel)" 4 | 5 | pushd "$repo" 6 | prettier --write . 7 | for i in http_server.c http_server.h json_encode.h mpd_client.c mpd_client.h ympd.c; do 8 | clang-format -i -style=file "src/$i" 9 | done 10 | popd 11 | -------------------------------------------------------------------------------- /tools/mkdata.c: -------------------------------------------------------------------------------- 1 | /* This program is used to embed arbitrary data into a C binary. It takes 2 | * a list of files as an input, and produces a .c data file that contains 3 | * contents of all these files as collection of char arrays. 4 | * 5 | * Usage: ./mkdata [file2, ...] > embedded_data.c 6 | */ 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | const char* header = 15 | "#include \n" 16 | "#include \n" 17 | "#include \n" 18 | "#include \"src/http_server.h\"\n" 19 | "\n" 20 | "static const struct embedded_file embedded_files[] = {\n"; 21 | 22 | const char* footer = 23 | " {NULL, NULL, NULL, 0}\n" 24 | "};\n" 25 | "\n" 26 | "const struct embedded_file *find_embedded_file(const char *name) {\n" 27 | " const struct embedded_file *p;\n" 28 | " for (p = embedded_files; p->name != NULL; p++)\n" 29 | " if (!strcmp(p->name, name))\n" 30 | " return p;\n" 31 | " return NULL;\n" 32 | "}\n"; 33 | 34 | static const char* get_mime(char* filename) 35 | { 36 | const char *extension = strrchr(filename, '.'); 37 | if(!strcmp(extension, ".js")) 38 | return "application/javascript"; 39 | if(!strcmp(extension, ".css")) 40 | return "text/css"; 41 | if(!strcmp(extension, ".ico")) 42 | return "image/vnd.microsoft.icon"; 43 | if(!strcmp(extension, ".woff")) 44 | return "application/font-woff"; 45 | if(!strcmp(extension, ".ttf")) 46 | return "application/x-font-ttf"; 47 | if(!strcmp(extension, ".eot")) 48 | return "application/octet-stream"; 49 | if(!strcmp(extension, ".svg")) 50 | return "image/svg+xml"; 51 | if(!strcmp(extension, ".html")) 52 | return "text/html"; 53 | return "text/plain"; 54 | } 55 | 56 | int main(int argc, char *argv[]) 57 | { 58 | int i, j, buf; 59 | FILE *fd; 60 | 61 | if(argc <= 1) 62 | err(EXIT_FAILURE, "Usage: ./%s [file2, ...] > embedded_data.c", argv[0]); 63 | 64 | for(i = 1; i < argc; i++) 65 | { 66 | fd = fopen(argv[i], "r"); 67 | if(!fd) 68 | err(EXIT_FAILURE, "%s", argv[i]); 69 | 70 | printf("static const unsigned char v%d[] = {", i); 71 | 72 | j = 0; 73 | while((buf = fgetc(fd)) != EOF) 74 | { 75 | if(!(j % 12)) 76 | putchar('\n'); 77 | 78 | printf(" %#04x, ", buf); 79 | j++; 80 | } 81 | printf(" 0x00\n};\n\n"); 82 | fclose(fd); 83 | } 84 | fputs(header, stdout); 85 | 86 | for(i = 1; i < argc; i++) 87 | { 88 | printf(" {\"%s\", v%d, \"%s\", sizeof(v%d) - 1}, \n", 89 | argv[i]+6, i, get_mime(argv[i]), i); 90 | } 91 | 92 | fputs(footer, stdout); 93 | return EXIT_SUCCESS; 94 | } 95 | -------------------------------------------------------------------------------- /tools/mkdata.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | # This program is used to embed arbitrary data into a C binary. It takes 3 | # a list of files as an input, and produces a .c data file that contains 4 | # contents of all these files as collection of char arrays. 5 | # 6 | # Usage: perl [file2, ...] > embedded_data.c 7 | 8 | use File::Basename; 9 | 10 | %mimetypes = ( 11 | js => 'application/javascript', 12 | css => 'text/css', 13 | ico => 'image/vnd.microsoft.icon', 14 | woff => 'application/font-woff', 15 | ttf => 'application/x-font-ttf', 16 | eot => 'application/octet-stream', 17 | svg => 'image/svg+xml', 18 | html => 'text/html' 19 | ); 20 | 21 | foreach my $i (0 .. $#ARGV) { 22 | open FD, '<:raw', $ARGV[$i] or die "Cannot open $ARGV[$i]: $!\n"; 23 | printf("static const unsigned char v%d[] = {", $i); 24 | my $byte; 25 | my $j = 0; 26 | while (read(FD, $byte, 1)) { 27 | if (($j % 12) == 0) { 28 | print "\n"; 29 | } 30 | printf ' %#04x,', ord($byte); 31 | $j++; 32 | } 33 | print " 0x00\n};\n"; 34 | close FD; 35 | } 36 | 37 | print < 39 | #include 40 | #include 41 | #include "src/http_server.h" 42 | 43 | static const struct embedded_file embedded_files[] = { 44 | EOS 45 | 46 | foreach my $i (0 .. $#ARGV) { 47 | my ($ext) = $ARGV[$i] =~ /([^.]+)$/; 48 | my $mime = $mimetypes{$ext}; 49 | $ARGV[$i] =~ s/htdocs//; 50 | print " {\"$ARGV[$i]\", v$i, \"$mime\", sizeof(v$i) - 1},\n"; 51 | } 52 | 53 | print <name != NULL; p++) 60 | if (!strcmp(p->name, name)) 61 | return p; 62 | return NULL; 63 | } 64 | 65 | EOS 66 | -------------------------------------------------------------------------------- /ympd.1: -------------------------------------------------------------------------------- 1 | .\" Manpage for ympd. 2 | .\" Contact andy@ympd.org to correct errors or typos. 3 | .TH man 1 "19 Oct 2014" "1.2.3" "ympd man page" 4 | .SH NAME 5 | ympd \- Standalone MPD Web GUI written in C, utilizing Websockets and Bootstrap/JS 6 | .SH SYNOPSIS 7 | ympd [OPTION]... 8 | .SH DESCRIPTION 9 | ympd is a lightweight MPD (Music Player Daemon) web client that runs without a dedicated webserver or interpreters like PHP, NodeJS or Ruby. It's tuned for minimal resource usage and requires only very litte dependencies. 10 | 11 | ympd is based in part on the work of the mongoose http server (https://github.com/cesanta/mongoose) 12 | .SH OPTIONS 13 | .TP 14 | \fB\-h\fR, \fB\-\-host HOST\fR 15 | connect to mpd at host, defaults to localhost 16 | .TP 17 | \fB\-p\fR, \fB\-\-port PORT\fR 18 | connect to mpd at port, defaults to 6600 19 | .TP 20 | \fB\-w\fR, \fB\-\-webport PORT\fR 21 | specifies the port for the webserver to listen to, defaults to 8080 22 | .TP 23 | \fB\-u\fR, \fB\-\-user username\fR 24 | drop privileges to the provided username after socket binding 25 | .TP 26 | \fB\-m\fR, \fB\-\-mpdpass password\fR 27 | specifies the password to use when connecting to mpd 28 | .TP 29 | \fB\-V\fR, \fB\-\-version\fR 30 | print version and exit 31 | .TP 32 | \fB\-\-help\fR 33 | print all valid options and exits 34 | .SH BUGS 35 | No known bugs. 36 | .SH AUTHOR 37 | Andrew Karpow (andy@ndyk.de) 38 | 39 | http://www.ympd.org 40 | --------------------------------------------------------------------------------