├── .editorconfig ├── .gitattributes ├── I2S_modules.md ├── README.md ├── install.sh └── srv └── http ├── assets ├── css │ ├── addons.css │ ├── camilla.css │ ├── colors.css │ ├── common.css │ ├── equalizer.css │ ├── hovercursor.css │ ├── main.css │ ├── plugin │ │ ├── roundslider-1.6.1.min.css │ │ ├── select2-4.0.13.min.css │ │ └── simplekeyboard-3.6.2.min.css │ ├── select2.css │ ├── settings.css │ └── simplekeyboard.css ├── data │ ├── regdomcodes.json │ ├── system-i2s.json │ └── titles_with_paren ├── fonts │ ├── Inconsolata.woff2 │ ├── Lato-Light.woff2 │ ├── Lato-Regular.woff2 │ └── rern.woff2 ├── img │ ├── addons │ │ ├── thumbdab.jpg │ │ ├── thumbfran.gif │ │ ├── thumbgpio.gif │ │ ├── thumbplst.jpg │ │ ├── thumbrank.png │ │ └── thumbwebr.png │ ├── coverart.svg │ ├── gpio.svg │ ├── i2cbackpack.jpg │ ├── icon.png │ ├── icon.svg │ ├── lcd.jpg │ ├── lcdchar.jpg │ ├── mpdoled.jpg │ ├── powerbutton.jpg │ ├── relays.jpg │ ├── rotaryencoder.jpg │ ├── splash.png │ ├── stopwatch.svg │ ├── vu.svg │ └── vuled.jpg └── js │ ├── addons.js │ ├── camilla.js │ ├── common.js │ ├── context.js │ ├── features.js │ ├── function.js │ ├── guide.js │ ├── main.js │ ├── networks.js │ ├── passive.js │ ├── player.js │ ├── plugin │ ├── camilladsp_plot-3.0.2.min.js │ ├── complex-2.4.2.min.js │ ├── jquery-3.7.1.min.js │ ├── lazysizes-5.3.2.min.js │ ├── pica-9.0.1.min.js │ ├── plotly-basic-3.0.1.min.js │ ├── qrcode-20200314.min.js │ ├── select2-4.0.13.min.js │ └── simplekeyboard-3.8.1.min.js │ ├── settings.js │ ├── shortcut.js │ ├── simplekeyboard.js │ └── system.js ├── bash ├── albumthumbnail.sh ├── audiocd.sh ├── bashrc ├── bluealsa-dbus.py ├── bluetoothbutton.sh ├── cmd-coverart.sh ├── cmd-list.sh ├── cmd.sh ├── common.sh ├── dab-scan.sh ├── dab-start.sh ├── lcdchar.py ├── motd.sh ├── mpdidle.sh ├── power.sh ├── powerbutton.sh ├── relays-timer.sh ├── relays.sh ├── rotaryencoder.sh ├── scrobble.sh ├── settings │ ├── addons-data.sh │ ├── addons.sh │ ├── camilla-bluetooth.sh │ ├── camilla-data.sh │ ├── camilla-devices.sh │ ├── camilla.sh │ ├── camilla_audiofileread.py │ ├── camilla_cooley_tukey.py │ ├── data-config.sh │ ├── data-service.sh │ ├── data-status.sh │ ├── features-data.sh │ ├── features.sh │ ├── networks-bluetooth.sh │ ├── networks-data.sh │ ├── networks-scan.sh │ ├── networks.sh │ ├── player-asound.sh │ ├── player-conf.sh │ ├── player-data.sh │ ├── player-devices.sh │ ├── player-wm5102.sh │ ├── player.sh │ ├── system-data.sh │ ├── system-databackup.sh │ ├── system-datadefault.sh │ ├── system-datareset.sh │ ├── system-datarestore.sh │ ├── system-mount.sh │ ├── system-storage.sh │ └── system.sh ├── shairport.sh ├── smbdfree.sh ├── snapclient.sh ├── spotifyd.sh ├── startup.sh ├── startx.sh ├── status-bluetooth.sh ├── status-coverart.sh ├── status-coverartonline.sh ├── status-coverartupnp.py ├── status-dab.sh ├── status-push.sh ├── status-radio.sh ├── status.sh ├── stoptimer.sh ├── tageditor.sh ├── vu.sh └── websocket.py ├── cmd.php ├── common.php ├── function.php ├── index.php ├── library.php ├── login.php ├── playlist.php ├── settings.php └── settings ├── addonsprogress.php ├── camilla.php ├── features.php ├── function.php ├── guide.php ├── networks.php ├── player.php └── system.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = tab 6 | indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | 10 | [*.{css,js,jsx,html,md,php,sass}] # Matches multiple types with brace expansion notation 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Automatically normalize line endings for all text-based files 2 | # http://git-scm.com/docs/gitattributes#_end_of_line_conversion 3 | * text=auto 4 | 5 | # For the following file types, normalize line endings to LF on 6 | # checkin and prevent conversion to CRLF when they are checked out 7 | # (this is required in order to prevent newline related issues like, 8 | # for example, after the build script is run) 9 | .* text eol=lf 10 | *.css text eol=lf 11 | *.html text eol=lf 12 | *.js text eol=lf 13 | *.json text eol=lf 14 | *.md text eol=lf 15 | *.php text eol=lf 16 | *.py text eol=lf 17 | *.sh text eol=lf 18 | *.svg text eol=lf 19 | *.txt text eol=lf 20 | *.xml text eol=lf 21 | 22 | # Ensure those won't be messed up with 23 | *.jpg binary 24 | *.png binary 25 | *.gif binary 26 | *.data binary 27 | *.woff2 binary 28 | -------------------------------------------------------------------------------- /I2S_modules.md: -------------------------------------------------------------------------------- 1 | ### Supported I2S Modules 2 | ```sh 3 | sed -e '/[{}]/ d' -e 's/\s*"\(.*\)":.*/- \1/' /srv/http/assets/data/system-i2s.json 4 | ``` 5 | 6 | - Adafruit MAX98357 7 | - Adafruit UDA1334A 8 | - Allo Boss DAC 9 | - Allo Boss2 DAC 10 | - Allo DigiOne 11 | - Allo DigiOne Signature 12 | - Allo Katana DAC 13 | - Allo Piano 2.0 DAC 14 | - Allo Piano 2.1 DAC 15 | - Allo Piano 2.1 DAC (with Kali Reclocker) 16 | - Allo Piano DAC 17 | - Aoide DAC II 18 | - Apple Pi DAC 19 | - Audioinjector Addons 20 | - Audioinjector Octo 21 | - Audioinjector Stereo 22 | - Audioinjector Ultra 23 | - Audioinjector WM8731 24 | - Audioinjector Zero 25 | - Audiophonics Evo-Sabre DAC 2xES9038Q2M 26 | - Audiophonics I-Sabre DAC ES9023 27 | - Audiophonics I-Sabre AMP DAC ES9023 28 | - Audiophonics I-Sabre DAC ES9028Q2M 29 | - Audiophonics I-Sabre DAC ES9038Q2M 30 | - AudioSense-Pi 31 | - BerryNOS 32 | - ChipDip DAC 33 | - DACBerry AMP2 34 | - DACBerry One+ - RCA 35 | - DACBerry One+ - Digital 36 | - DACBerry Pro 37 | - DACBerry Pro+ 38 | - DACBerry RDY+ - RCA 39 | - DACBerry RDY+ - Digital 40 | - Digital Dreamtime Akkordion 41 | - Dion Audio Loco DAC-AMP 42 | - Dion Audio Loco V2 DAC-AMP 43 | - Fe-Pi Audio 44 | - Generic AKM AK4xxx (i2s-dac/rpi-dac) 45 | - Generic AKM AK4xxx (hifiberry-dac) 46 | - Generic Burr-Brown PCM1794 47 | - Generic Burr-Brown PCM510x 48 | - Generic Burr-Brown PCM512x 49 | - Generic Cirrus Logic WM5102 50 | - Generic ESS ES90xx 51 | - Generic RPi-DAC compatible 52 | - HiFiBerry Amp 53 | - HiFiBerry Amp+ 54 | - HiFiBerry Amp100 55 | - HiFiBerry Amp2 56 | - HiFiBerry Amp3 57 | - HiFiBerry DAC 58 | - HiFiBerry DAC+ 59 | - HiFiBerry DAC+ HD 60 | - HiFiBerry DAC+ Lite 61 | - HiFiBerry DAC+ Pro 62 | - HiFiBerry DAC+ Pro XLR 63 | - HiFiBerry DAC+ RTC 64 | - HiFiBerry DAC+ Zero 65 | - HiFiBerry DAC+ADC 66 | - HiFiBerry DAC+ADC Pro 67 | - HiFiBerry DAC+DSP 68 | - HiFiBerry DAC2 Pro 69 | - HiFiBerry Digi 70 | - HiFiBerry Digi Pro 71 | - HiFiBerry Digi+ 72 | - HiFiBerry Digi+ Pro 73 | - HiFiBerry Digi2 Pro 74 | - HiFiBox DAC 75 | - InnoMaker HiFi DAC 76 | - IQaudIO Amp 77 | - IQaudIO Amp (with auto mute) 78 | - IQaudIO Amp (with unmute) 79 | - IQaudIO Codec 80 | - IQaudIO DAC 81 | - IQaudIO DAC Pro 82 | - IQaudIO DAC+ 83 | - IQaudIO Digi WM8804 84 | - IQaudIO Pi-DAC PRO 85 | - IQaudIO Pi-DAC Zero 86 | - IQaudIO Pi-DAC+ 87 | - IQaudIO Pi-Digi+ 88 | - IQaudIO Pi-DigiAMP+ 89 | - IQaudIO Pi-DigiAMP+ (with auto mute) 90 | - IQaudIO Pi-DigiAMP+ (with unmute) 91 | - JustBoom Amp HAT 92 | - JustBoom Amp Zero 93 | - JustBoom DAC 94 | - JustBoom DAC HAT 95 | - JustBoom DAC Zero 96 | - JustBoom Digi 97 | - Mamboberry HD DAC+ 98 | - Mamboberry Precision DAC+ 99 | - Maxim MAX98357A 100 | - MERUS Audio Amp 101 | - NanoSound HiFi DAC Pro 102 | - PecanPi DAC 103 | - PI 2 Design 502DAC 104 | - PI 2 Design 502DAC Pro 105 | - PI 2 Design 503HTA Hybrid Tube Amp 106 | - Picade HAT 107 | - PiFi DAC+ 108 | - PiFi DAC HD 109 | - PiFi DAC Zero 110 | - PiFi Digi+ 111 | - Pimoroni pHAT DAC 112 | - Pimoroni Audio DAC SHIM 113 | - Blokas Labs Pisound 114 | - Raspberry Pi Codec Zero 115 | - Raspberry Pi DAC+ 116 | - Raspberry Pi DAC Pro 117 | - Raspberry Pi DigiAMP+ 118 | - RaspiDAC 3 119 | - RaspyPlay 4 120 | - Red Rocks Audio DigiDAC 1 121 | - RPi-DAC 122 | - Soekris dam 1021 123 | - ST400 Dac - Amp 124 | - SuperAudioBoard 125 | - SupTronics X400 126 | - SupTronics X4000K 127 | - Terra-Berry DAC 2 128 | - Waveshare WM8960 129 | - Wolfson Audio 130 | - X10 DAC 131 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | alias=r1 4 | 5 | . /srv/http/bash/settings/addons.sh 6 | 7 | # 20250530 8 | file=/etc/pacman.conf 9 | grep -q mpd $file && sed -i 's/ mpd//' $file 10 | 11 | # 20250502 12 | if [[ -e $dirmpd/album && $( uniq -d $dirmpd/album ) ]]; then 13 | for t in album latest; do 14 | sort -o $dirmpd/$t{,} 15 | sort -o $dirmpd/$t'byartist'{,} 16 | done 17 | fi 18 | 19 | if ! locale | grep -qi ^LANG=.*utf-*8; then 20 | ! locale -a | grep -q ^C.utf8 && locale-gen C.utf8 21 | localectl set-locale LANG=C.utf8 22 | fi 23 | 24 | file=/etc/systemd/system/cava.service 25 | if ! grep -q ^User $file; then 26 | sed -i -e '/^ExecStart/ i\User=root' -e 's/cava/vu/' $file 27 | ln -s /etc/cava.conf /root/.config/cava 28 | systemctl daemon-reload 29 | file=$dirsystem/vuled.conf 30 | if [[ -e $file ]] && grep -q = $file; then 31 | conf=$( sed 's/.*=//' $file ) 32 | echo $conf > $file 33 | fi 34 | [[ -e $dirsystem/vuled ]] && systemctl start cava 35 | fi 36 | 37 | # 20250404 38 | file=/etc/systemd/system/localbrowser.service 39 | if grep -q startx$ $file; then 40 | sed -i -E 's|^(ExecStart=).*|\1/usr/bin/startx /srv/http/bash/startx.sh|' $file 41 | systemctl daemon-reload 42 | rm /etc/X11/xinit/xinitrc 43 | fi 44 | 45 | if [[ $( pacman -Q snapcast ) != 'snapcast 0.31.0-3' ]]; then 46 | pacman -Sy --noconfirm snapcast 47 | if [[ $? == 0 ]]; then 48 | sed -i -e '/^bind_to_address/ d' -e '/^#bind_to_address/ a\bind_to_address = 0.0.0.0' /etc/snapserver.conf 49 | else 50 | echo $warn upgrade snapcast failed. 51 | fi 52 | fi 53 | 54 | #------------------------------------------------------------------------------- 55 | installstart "$1" 56 | 57 | rm -rf /srv/http/assets/{css,js} /srv/http/{bash,settings} 58 | 59 | getinstallzip 60 | 61 | . $dirbash/common.sh 62 | dirPermissions 63 | $dirbash/cmd.sh cachebust 64 | [[ -e $dirsystem/color ]] && $dirbash/cmd.sh color 65 | 66 | installfinish 67 | -------------------------------------------------------------------------------- /srv/http/assets/css/addons.css: -------------------------------------------------------------------------------- 1 | img { 2 | border: 0 3 | } 4 | hr { 5 | margin-top: 20px; 6 | margin-bottom: 20px; 7 | border: 0; 8 | border-top: 1px solid #eee 9 | } 10 | @media (max-width: 540px) { 11 | .divaddon { 12 | padding: 10px !important; 13 | } 14 | } 15 | #list { 16 | padding: 30px 0 0 25px; 17 | list-style: none; 18 | } 19 | #list li:before { 20 | margin-right: 5px; 21 | content: '\2022'; 22 | color: var( --cg60 ); 23 | } 24 | #list li.installed:before { 25 | color: #0f0; 26 | } 27 | #list li.update:before, 28 | legend .title.installed.update::before { 29 | animation: blinkopaque 1.5s linear infinite; 30 | } 31 | .source { 32 | display: block; 33 | text-align: right; 34 | color: var( --cg60 ); 35 | } 36 | .source i { 37 | font-size: 18px; 38 | } 39 | .revisiontext { 40 | padding-left: 20px; 41 | line-height: 22px; 42 | } 43 | .detailtext i { 44 | color: var( --cg60 ); 45 | } 46 | .detailtext a { 47 | color: var( --cml ); 48 | } 49 | .divaddon { 50 | margin: 10px 0; 51 | padding: 10px 20px; 52 | border-radius: 6px; 53 | background: var( --cga ); 54 | } 55 | .divaddon img { 56 | max-width: 100px; 57 | } 58 | .content { 59 | float: left; 60 | width: calc( 100% - 110px); 61 | } 62 | legend { 63 | width: 100%; 64 | margin-bottom: 5px; 65 | font-size: 14px; 66 | line-height: 30px; 67 | color: var( --cg60 ); 68 | border-bottom: 1px solid var( --cg ); 69 | } 70 | legend .title { 71 | font-size: 21px; 72 | } 73 | legend .title.installed::before { 74 | content: '• '; 75 | color: #0f0; 76 | } 77 | legend p, 78 | legend a { 79 | display: inline-block; 80 | } 81 | legend > i:last-child { 82 | float: right; 83 | } 84 | legend .status { 85 | font-size: 24px; 86 | } 87 | #hidescrollv { 88 | max-height: calc( 100vh - 110px ); 89 | margin-top: 10px; 90 | overflow: hidden; 91 | } 92 | .detailtext { 93 | line-height: 18px; 94 | } 95 | .detailtext code { 96 | padding: 3px; 97 | } 98 | .info { 99 | display: inline-block; 100 | } 101 | i.info { 102 | width: 30px; 103 | } 104 | .thumbnail { 105 | float: right; 106 | width: 100px; 107 | cursor: pointer; 108 | } 109 | a, 110 | a:hover { 111 | text-decoration: none; 112 | } 113 | a:active, 114 | a:hover { 115 | outline: 0 116 | } 117 | .container .infobtn { 118 | min-width: 90px; 119 | } 120 | .infobtn.disabled { 121 | pointer-events: auto; 122 | } 123 | #list, 124 | #list li { 125 | width: fit-content; 126 | width: -moz-fit-content; 127 | line-height: 22px; 128 | color: var( --cw ); 129 | } 130 | #list a { 131 | color: #ff8888; 132 | text-decoration: none; 133 | } 134 | #list li, 135 | .divaddon legend, 136 | a { 137 | cursor: pointer; 138 | } 139 | strike { 140 | text-decoration: line-through; 141 | } 142 | li i, 143 | .revision, 144 | .divaddon legend i { 145 | color: var( --cg60 ); 146 | } 147 | span, 148 | code, 149 | code i { 150 | color: var( --cw ); 151 | } 152 | p, 153 | ul, 154 | .disabled { 155 | color: var( --cw ); 156 | } 157 | .install { 158 | background-color: var( --cm ); 159 | } 160 | .uninstall { 161 | background-color: #bb2828; 162 | } 163 | -------------------------------------------------------------------------------- /srv/http/assets/css/colors.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --cd : hsl( 200, 100%, 35% ); /* default main*/ 3 | --cw : hsl( 0, 0%, 90% ); 4 | 5 | --h : 200; 6 | --s : 100%; 7 | --ml60 : 60%; 8 | --ml40 : 40%; 9 | --ml35 : 35%; 10 | --ml30 : 30%; 11 | --ml20 : 20%; 12 | 13 | --cg75 : hsl( var( --h ), 3%, 75% ); 14 | --cg70 : hsl( var( --h ), 3%, 70% ); 15 | --cg60 : hsl( var( --h ), 3%, 60% ); 16 | --cg50 : hsl( var( --h ), 3%, 50% ); 17 | --cgl : hsl( var( --h ), 3%, 40% ); 18 | --cg : hsl( var( --h ), 3%, 30% ); 19 | --cga : hsl( var( --h ), 3%, 20% ); 20 | --cgd : hsl( var( --h ), 3%, 10% ); 21 | --cm60 : hsl( var( --h ), var( --s ), var( --ml60 ) ); 22 | --cml : hsl( var( --h ), var( --s ), var( --ml40 ) ); 23 | --cm : hsl( var( --h ), var( --s ), var( --ml35 ) ); 24 | --cma : hsl( var( --h ), var( --s ), var( --ml30 ) ); 25 | --cmd : hsl( var( --h ), var( --s ), var( --ml20 ) ); 26 | 27 | --shadow-btn : -2px 2px 4px #000; 28 | --shadow-btn-u : -2px -2px 4px #000; 29 | --shadow-bar : 0 0 10px 4px #000; 30 | --shadow-check : -1px 1px 2px 1px #000; 31 | --shadow-menu : 4px 4px 10px #000; 32 | --shadow-menu-l : -4px 4px 10px #000; 33 | --shadow-menu-u : -4px -4px 10px #000; 34 | --shadow-search : -2px 2px 4px #000; 35 | --shadow-select : -1px 1px 1px #000; 36 | --shadow-text : -1px 0 2px #000; 37 | /* w h x y inner(color) w outer(color) w */ 38 | --glossy-btn : radial-gradient( 225% 90% at 100% 0%, hsla(0,0%,100%,.3) 50%, hsla(0,0%,100%, 0) 55% ); 39 | --glossy-btn-l : radial-gradient( 225% 90% at 100% 0%, hsla(0,0%,100%,.3) 50%, hsla(0,0%,100%, 0) 55% ); 40 | --glossy-btn-r : radial-gradient( 300% 90% at 0% 0%, hsla(0,0%,100%,.3) 50%, hsla(0,0%,100%, 0) 55% ); 41 | --glossy-btn-m : linear-gradient( to bottom, hsla(0,0%,100%,.3) 0%, hsla(0,0%,100%,.3) 45%, hsla(0,0%,0%,0) 50%, hsla(0,0%,0%,0) 100% ); 42 | 43 | --wheel : conic-gradient( in hsl longer hue, red 0 100% ); 44 | } 45 | 46 | bl, .bl { color : var( --cm ) !important } 47 | bll, .bll { color : var( --cm60 ) !important } 48 | g { color : var( --cgl ) } 49 | gr, .gr { color : var( --cg60 ) !important } 50 | .grd { color : var( --cg ) !important } 51 | grn, .grn { color : #0f0 !important } 52 | ora, .ora { color : #f80 !important } 53 | pur, .pur { color : #f0f !important } 54 | red, .red { color : #f00 !important } 55 | yl, .yl { color : #ff0 !important } 56 | wh, .wh { color : var( --cw ) !important } 57 | 58 | .bgr { background : var( --cgd ) !important } 59 | .bgr60 { background-color : var( --cg60 ) !important } 60 | 61 | i.bl { color : var( --cml ) !important } 62 | input { 63 | color-scheme: dark; 64 | } 65 | -------------------------------------------------------------------------------- /srv/http/assets/css/equalizer.css: -------------------------------------------------------------------------------- 1 | #eq .bottom { z-index: 1 } 2 | #eq .vertical { z-index: 0 } 3 | 4 | /* calc var */ 5 | :root { 6 | --eqSliderH : 300px; 7 | --eqBandW : 50px; 8 | --eqLabel : calc( var( --eqBandW ) - 10px ); 9 | --eqLabelM : 0 5px; 10 | } 11 | @media ( max-height: 450px ) and ( orientation: landscape ) { 12 | #eq { --eqSliderH: calc( 100vh - 150px ) } 13 | } 14 | @media ( max-width: 530px ) { 15 | #eq { 16 | --eqBandW : calc( 100vw / 10 - 4px ); 17 | --eqThumbW : calc( var( --eqBandW ) - 8px ); 18 | } 19 | } 20 | @media ( max-width: 450px ) { 21 | #eq { 22 | --eqLabel : calc( var( --eqBandW ) - 4px ); 23 | --eqLabelM : 0 2px; 24 | } 25 | } 26 | @media ( max-width: 330px ) { /* scaled 0.9 */ 27 | #eq { --eqBandW: calc( 100vw / 10 ) } 28 | } 29 | @media ( max-width: 320px ) and ( max-height: 480px ) { 30 | #eq { --eqSliderH: calc( 100vh - 180px ) } 31 | } 32 | 33 | /* size - position */ 34 | #eq i { width : var( --eqBandW ) } 35 | #eq .vertical { width : var( --eqSliderH ) } 36 | #eq .label a { width : var( --eqLabel ) } 37 | #eq input { height : var( --eqBandW ) } 38 | 39 | @media ( max-width: 530px ) { 40 | #eq input::-webkit-slider-thumb { width : var( --eqThumbW ) } 41 | #eq input::-moz-range-thumb { width : var( --eqThumbW ) } 42 | } /* <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< */ 43 | #eq .label { 44 | margin-bottom: 15px; 45 | white-space: nowrap; 46 | } 47 | #eq .label a { 48 | display: inline-block; 49 | margin: var( --eqLabelM ); 50 | height: 17px; 51 | line-height: 17px; 52 | font-size: 15px; 53 | color: var( --cw ); 54 | text-align: center; 55 | text-shadow: none; 56 | font-family: Inconsolata; 57 | background: var( --cg ); 58 | border-radius: 3px; 59 | cursor: pointer; 60 | } 61 | #eq .label.dn, 62 | #eq .label.up { 63 | max-width: unset; 64 | user-select: none; 65 | -webkit-touch-callout: none; 66 | -webkit-user-select: none; 67 | } 68 | #eq .label.dn a { 69 | background: var( --cgd ); 70 | } 71 | #eq .bottom { 72 | position: absolute; 73 | top: 405px; 74 | left: 50%; 75 | transform: translateX( -50% ); 76 | } 77 | #eq i { 78 | position: relative; 79 | width: 40px !important; 80 | height: 40px !important; 81 | line-height: 40px !important; 82 | vertical-align: middle; 83 | } 84 | #eq .vertical { 85 | position: relative; 86 | max-width: 300px; 87 | margin: 0; 88 | transform : rotate( -90deg ); 89 | transform-origin: top left; 90 | translate: 10px 300px; 91 | } 92 | #eq .vertical::before { /* flat line */ 93 | position: absolute; 94 | content: ''; 95 | width: 1px; 96 | height: 100%; 97 | right: 150px; 98 | background: var( --cgl ); 99 | } 100 | #eq input { 101 | display: block; 102 | position: relative; 103 | max-width: 100%; 104 | touch-action: none; /* fix: android chrome cannot drag */ 105 | } 106 | #eq input::-webkit-slider-thumb { 107 | margin-top: -19px; 108 | transform : rotate( 90deg ); 109 | } 110 | #eq input::-moz-range-thumb { 111 | transform : rotate( 90deg ); 112 | } 113 | #divpreset { 114 | display: inline-block; 115 | width: 230px; 116 | height: 42px; 117 | } -------------------------------------------------------------------------------- /srv/http/assets/css/hovercursor.css: -------------------------------------------------------------------------------- 1 | /* :hover *********************************************************/ 2 | #lib-title a:hover { 3 | color: var( --cm ); 4 | } 5 | #lib-list li:hover, 6 | #pl-savedlist li:hover, 7 | #pl-list li:hover, 8 | #lib-list li.updn:hover, 9 | #pl-savedlist li.updn:hover { 10 | background-color: var( --cga ); 11 | } 12 | #pl-list li.updn:hover { 13 | background-color: var( --cmd ); 14 | } 15 | #bar-top i.active:hover, 16 | #bar-bottom i.active:hover, 17 | #button-library:hover, 18 | #button-playlist:hover, 19 | #playback-controls button.active:hover, 20 | #playback-row .btn.active:hover { 21 | background-color: var( --cml ) !important; 22 | } 23 | #lib-list li:not( .licover ).active:hover, 24 | #pl-savedlist li.active:hover, 25 | #pl-list li.active:hover { 26 | background-color: var( --cm ); 27 | } 28 | .menu a:hover, 29 | .submenu:not( .disabled ):hover, 30 | #bar-bottom i:hover, 31 | #button-settings:hover, 32 | #divlyricsartist i:hover, 33 | #playback-controls i:hover, 34 | #playback-row .btn:hover { 35 | background-color: var( --cgl ) !important; 36 | } 37 | .index a:hover, 38 | #button-lib-search:hover, 39 | #pl-manage i:hover, 40 | #button-lib-back:hover, 41 | #button-pl-back:hover, 42 | #button-pl-search:hover { 43 | background-color: var( --cg ) !important; 44 | } 45 | /* :active:hover *********************************************************/ 46 | #colorok:active:hover, 47 | #lib-list li:not( .licover ).active:active:hover, 48 | #pl-savedlist li.active:active:hover, 49 | #pl-list li.active:active:hover, 50 | #playback-controls button.active:active:hover, 51 | #playback-row .btn.active:active:hover { 52 | background-color: var( --cma ) !important; 53 | } 54 | .menu a:active:hover, 55 | .submenu:not( .disabled ):active:hover, 56 | #button-lib-search:active:hover, 57 | #button-settings:active:hover, 58 | #lib-list li:active:hover, 59 | #pl-manage i:active:hover, 60 | #button-pl-search:active:hover, 61 | #playback-controls button:active:hover, 62 | #playback-row .btn:active:hover { 63 | background-color: var( --cga ) !important; 64 | } 65 | #lib-list li:active:hover, 66 | #pl-savedlist li:active:hover, 67 | #pl-list li:active:hover, 68 | #power.active, 69 | #screenoff.active, 70 | #power:hover, 71 | #screenoff:hover { 72 | background-color: var( --cgd ) !important; 73 | } 74 | .sub.nohover:hover { 75 | background-color: var( --cg ) !important; 76 | } 77 | 78 | /* cursor *********************************************************/ 79 | button, 80 | li:not( .licover ), 81 | .band, 82 | .btn, 83 | .bioback, 84 | .biosimilar, 85 | .button-dab-refresh, 86 | .button-latest-clear, 87 | .button-webradio-new, 88 | .close-root, 89 | .coverart, 90 | .coveredit, 91 | .bkedit, 92 | .emptyadd, 93 | .i-search, 94 | .index a.indexed, 95 | .index i, 96 | .licoverimg, 97 | .liinfo, 98 | .map, 99 | .menu a, 100 | .mode:not( .nodata ), 101 | .point, 102 | .savedlist, 103 | .submenu:not( .disabled ), 104 | #album:not( .albumgray ), 105 | #artist, 106 | #bar-bottom, 107 | #bar-top i, 108 | #button-library, 109 | #button-lib-back, 110 | #button-lib-update, 111 | #button-pl-back, 112 | #button-pl-playlists, 113 | #button-playlist, 114 | #colorcancel, 115 | #colorok, 116 | #coloroption, 117 | #elapsed, 118 | #eq i, 119 | #eq .label a, 120 | #liboptions, 121 | #lib-title a, 122 | #lib-search-close, 123 | #lyrics i, 124 | #pl-manage i, 125 | #pl-search-close, 126 | #playericon.i-audiocd, 127 | #playlist-empty, 128 | #refresh, 129 | #sampling i, 130 | #status, 131 | #thumbupdate, 132 | #time-band, 133 | #time svg, 134 | #title, 135 | #volume-band, 136 | #volume-bar-dn, 137 | #volume-bar-up, 138 | #volume-band-level { 139 | cursor: pointer; 140 | } 141 | .disabled, 142 | #lib-title a:last-of-type:hover { 143 | cursor: default; 144 | } 145 | #lib-title a:last-of-type:hover { 146 | color: var( --cw ); 147 | } 148 | #bio .container:focus-visible, 149 | #lyricstext { 150 | outline: none; 151 | } 152 | .content-top > i:focus-visible, 153 | #pl-manage i:focus-visible { 154 | border-radius: 50%; 155 | outline: 2px solid var(--cgl ); 156 | outline-offset: -5px; 157 | } 158 | -------------------------------------------------------------------------------- /srv/http/assets/css/plugin/roundslider-1.6.1.min.css: -------------------------------------------------------------------------------- 1 | /*! roundSlider v1.6.1 | (c) 2015-2020, Soundar | MIT license | http://roundsliderui.com/licence.html */ 2 | .rs-ie,.rs-edge,.rs-handle{-ms-touch-action:none;touch-action:none}.rs-control{position:relative;outline:0 none}.rs-container{position:relative}.rs-control *,.rs-control *:before,.rs-control *:after{-webkit-box-sizing:border-box;box-sizing:border-box}.rs-animation .rs-transition{transition:all 0.5s linear 0s}.rs-bar{-webkit-transform-origin:100% 50%;-ms-transform-origin:100% 50%;transform-origin:100% 50%}.rs-control .rs-split .rs-path,.rs-control .rs-overlay1,.rs-control .rs-overlay2{-webkit-transform-origin:50% 100%;-ms-transform-origin:50% 100%;transform-origin:50% 100%}.rs-control .rs-overlay{-webkit-transform-origin:100% 100%;-ms-transform-origin:100% 100%;transform-origin:100% 100%}.rs-rounded .rs-seperator,.rs-split .rs-path{-webkit-background-clip:padding-box;background-clip:padding-box}.rs-disabled{opacity:.35}.rs-inner-container{height:100%;width:100%;position:absolute;top:0;overflow:hidden}.rs-control .rs-quarter div.rs-block{height:200%;width:200%}.rs-control .rs-half.rs-top div.rs-block,.rs-control .rs-half.rs-bottom div.rs-block{height:200%;width:100%}.rs-control .rs-half.rs-left div.rs-block,.rs-control .rs-half.rs-right div.rs-block{height:100%;width:200%}.rs-control .rs-bottom .rs-block{top:auto;bottom:0}.rs-control .rs-right .rs-block{right:0}.rs-block.rs-outer{border-radius:1000px}.rs-block{height:100%;width:100%;display:block;position:absolute;top:0;overflow:hidden;z-index:3}.rs-block .rs-inner{border-radius:1000px;display:block;height:100%;width:100%;position:relative}.rs-overlay{width:50%}.rs-overlay1,.rs-overlay2{width:100%}.rs-overlay,.rs-overlay1,.rs-overlay2{position:absolute;background-color:#fff;z-index:3;top:0;height:50%}.rs-bar{display:block;position:absolute;bottom:0;height:0;z-index:10}.rs-bar.rs-rounded{z-index:5}.rs-bar .rs-seperator{height:0;display:block;float:left}.rs-bar:not(.rs-rounded) .rs-seperator{border-left:none;border-right:none}.rs-bar.rs-start .rs-seperator{border-top:none}.rs-bar.rs-end .rs-seperator{border-bottom:none}.rs-bar.rs-start.rs-rounded .rs-seperator{border-radius:0 0 1000px 1000px}.rs-bar.rs-end.rs-rounded .rs-seperator{border-radius:1000px 1000px 0 0}.rs-full .rs-bar,.rs-half .rs-bar{width:50%}.rs-half.rs-left .rs-bar,.rs-half.rs-right .rs-bar,.rs-quarter .rs-bar{width:100%}.rs-full .rs-bar,.rs-half.rs-left .rs-bar,.rs-half.rs-right .rs-bar{top:50%}.rs-bottom .rs-bar{top:0}.rs-half.rs-right .rs-bar,.rs-quarter.rs-right .rs-bar{right:100%}.rs-handle.rs-move{cursor:move}.rs-readonly .rs-handle.rs-move{cursor:default}.rs-classic-mode .rs-path{display:block;height:100%;width:100%}.rs-split .rs-path{border-radius:1000px 1000px 0 0;overflow:hidden;height:50%;position:absolute;top:0;z-index:2}.rs-control .rs-svg-container{display:block;position:absolute;top:0}.rs-control .rs-bottom .rs-svg-container{top:auto;bottom:0}.rs-control .rs-right .rs-svg-container{right:0}.rs-tooltip{position:absolute;cursor:default;border:1px solid transparent;z-index:10}.rs-full .rs-tooltip{top:50%;left:50%}.rs-bottom .rs-tooltip{top:0}.rs-top .rs-tooltip{bottom:0}.rs-right .rs-tooltip{left:0}.rs-left .rs-tooltip{right:0}.rs-half.rs-top .rs-tooltip,.rs-half.rs-bottom .rs-tooltip{left:50%}.rs-half.rs-left .rs-tooltip,.rs-half.rs-right .rs-tooltip{top:50%}.rs-tooltip .rs-input{outline:0 none;border:none;background:transparent}.rs-tooltip-text{font-family:verdana;font-size:13px;border-radius:7px;text-align:center;color:inherit}.rs-tooltip.rs-edit{padding:5px 8px}.rs-tooltip.rs-hover,.rs-tooltip.rs-edit:hover{border:1px solid #AAA;cursor:pointer}.rs-readonly .rs-tooltip.rs-edit:hover{border-color:transparent;cursor:default}.rs-tooltip.rs-center{margin:0px!important}.rs-half.rs-top .rs-tooltip.rs-center,.rs-half.rs-bottom .rs-tooltip.rs-center{transform:translate(-50%,0)}.rs-half.rs-left .rs-tooltip.rs-center,.rs-half.rs-right .rs-tooltip.rs-center{transform:translate(0,-50%)}.rs-full .rs-tooltip.rs-center{transform:translate(-50%,-50%)}.rs-tooltip.rs-reset{margin:0px!important;top:0px!important;left:0px!important}.rs-handle{border-radius:1000px;outline:0 none;float:left}.rs-handle.rs-handle-square{border-radius:0}.rs-handle-dot{border:1px solid #AAA;padding:6px}.rs-handle-dot:after{display:block;content:"";border:1px solid #AAA;height:100%;width:100%;border-radius:1000px}.rs-seperator{border:1px solid #AAA}.rs-border{border:1px solid #AAA}.rs-path-color{background-color:#FFF}.rs-range-color{background-color:#54BBE0}.rs-bg-color{background-color:#FFF}.rs-handle{background-color:#838383}.rs-handle-dot{background-color:#FFF}.rs-handle-dot:after{background-color:#838383}.rs-path-inherited .rs-path{opacity:.2}.rs-svg-mode .rs-path{stroke:#FFF}.rs-svg-mode .rs-range{stroke:#54BBE0}.rs-svg-mode .rs-border{stroke:#AAA} -------------------------------------------------------------------------------- /srv/http/assets/css/plugin/simplekeyboard-3.6.2.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * simple-keyboard v3.6.2 4 | * https://github.com/hodgef/simple-keyboard 5 | * 6 | * Copyright (c) Francisco Hodge (https://github.com/hodgef) and project contributors. 7 | * 8 | * This source code is licensed under the MIT license found in the 9 | * LICENSE file in the root directory of this source tree. 10 | * 11 | */.hg-theme-default{background-color:#ececec;border-radius:5px;box-sizing:border-box;font-family:HelveticaNeue-Light,Helvetica Neue Light,Helvetica Neue,Helvetica,Arial,Lucida Grande,sans-serif;overflow:hidden;padding:5px;touch-action:manipulation;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:100%}.hg-theme-default .hg-button span{pointer-events:none}.hg-theme-default button.hg-button{border-width:0;font-size:inherit;outline:0}.hg-theme-default .hg-button{display:inline-block;flex-grow:1}.hg-theme-default .hg-row{display:flex}.hg-theme-default .hg-row:not(:last-child){margin-bottom:5px}.hg-theme-default .hg-row .hg-button-container,.hg-theme-default .hg-row .hg-button:not(:last-child){margin-right:5px}.hg-theme-default .hg-row>div:last-child{margin-right:0}.hg-theme-default .hg-row .hg-button-container{display:flex}.hg-theme-default .hg-button{-webkit-tap-highlight-color:rgba(0,0,0,0);align-items:center;background:#fff;border-bottom:1px solid #b5b5b5;border-radius:5px;box-shadow:0 0 3px -1px rgba(0,0,0,.3);box-sizing:border-box;cursor:pointer;display:flex;height:40px;justify-content:center;padding:5px}.hg-theme-default .hg-button.hg-standardBtn{width:20px}.hg-theme-default .hg-button.hg-activeButton{background:#efefef}.hg-theme-default.hg-layout-numeric .hg-button{align-items:center;display:flex;height:60px;justify-content:center;width:33.3%}.hg-theme-default .hg-button.hg-button-numpadadd,.hg-theme-default .hg-button.hg-button-numpadenter{height:85px}.hg-theme-default .hg-button.hg-button-numpad0{width:105px}.hg-theme-default .hg-button.hg-button-com{max-width:85px}.hg-theme-default .hg-button.hg-standardBtn.hg-button-at{max-width:45px}.hg-theme-default .hg-button.hg-selectedButton{background:rgba(5,25,70,.53);color:#fff}.hg-theme-default .hg-button.hg-standardBtn[data-skbtn=".com"]{max-width:82px}.hg-theme-default .hg-button.hg-standardBtn[data-skbtn="@"]{max-width:60px}.hg-candidate-box{background:#ececec;border-bottom:2px solid #b5b5b5;border-radius:5px;display:inline-flex;margin-top:-10px;max-width:272px;position:absolute;transform:translateY(-100%);-webkit-user-select:none;-moz-user-select:none;user-select:none}ul.hg-candidate-box-list{display:flex;flex:1;list-style:none;margin:0;padding:0}li.hg-candidate-box-list-item{align-items:center;display:flex;height:40px;justify-content:center;width:40px}li.hg-candidate-box-list-item:hover{background:rgba(0,0,0,.03);cursor:pointer}li.hg-candidate-box-list-item:active{background:rgba(0,0,0,.1)}.hg-candidate-box-prev:before{content:"◄"}.hg-candidate-box-next:before{content:"►"}.hg-candidate-box-next,.hg-candidate-box-prev{align-items:center;color:#969696;cursor:pointer;display:flex;padding:0 10px}.hg-candidate-box-next{border-bottom-right-radius:5px;border-top-right-radius:5px}.hg-candidate-box-prev{border-bottom-left-radius:5px;border-top-left-radius:5px}.hg-candidate-box-btn-active{color:#444} -------------------------------------------------------------------------------- /srv/http/assets/css/select2.css: -------------------------------------------------------------------------------- 1 | .select2-container { 2 | max-width : 230px; 3 | height : 40px; 4 | margin-bottom : 10px; 5 | font-size : 16px; 6 | line-height : 24px; 7 | border-radius : 4px; 8 | box-shadow : var( --shadow-select ); 9 | text-align : left; 10 | } 11 | .container .select2-container { 12 | width : 100% !important; 13 | } 14 | .select2-selection { 15 | height: 40px !important; 16 | } 17 | .select2-selection--single { 18 | height : 40px; 19 | background : none; 20 | background : none !important; 21 | border : none !important; 22 | border-radius : 4px !important; 23 | } 24 | .select2-selection__rendered { 25 | line-height : 40px !important; 26 | border-radius : 4px; 27 | color : var( --cw ) !important; 28 | background : var( --cg ); 29 | } 30 | .select2-results__option { 31 | padding-left: 8px; 32 | } 33 | .select2-selection__arrow { 34 | height : 40px !important; 35 | width : 30px !important; 36 | } 37 | .select2-search--dropdown { 38 | box-shadow: var( --shadow-search ); 39 | } 40 | .select2-dropdown { 41 | background : var( --cga ); 42 | border : none; 43 | border-radius : 0; 44 | } 45 | .select2-results__options { 46 | max-height: 360px !important; 47 | } 48 | @media ( max-height: 510px ) { 49 | .select2-results__options { 50 | max-height: 70vh !important; 51 | } 52 | } 53 | .select2-dropdown--above { 54 | box-shadow: var( --shadow-menu-u ); 55 | } 56 | .select2-dropdown--below { 57 | box-shadow: var( --shadow-menu-l ); 58 | } 59 | .select2-search__field { 60 | font-family : lato; 61 | font-size : 16px; 62 | line-height : 24px; 63 | border : none !important; 64 | border-radius : 4px; 65 | background : #000; 66 | color : var( --cw ); 67 | } 68 | .select2-results__option--selected { 69 | color : var( --cw ) !important; 70 | background : var( --cg ) !important; 71 | } 72 | .select2-results__option--highlighted.select2-results__option--selectable { 73 | background: var( --cgl ) !important; 74 | } 75 | .select2-selection__arrow b { 76 | margin-top : -4px !important; 77 | border-color : var( --cw ) transparent transparent transparent !important; 78 | border-width : 5px 5px 0 5px !important; 79 | } 80 | .select2-container--open .select2-selection__arrow b { 81 | margin-top : -5px !important; 82 | border-color : transparent transparent var( --cw ) transparent !important; 83 | border-width : 0 5px 5px 5px !important; 84 | } 85 | .select2-container.disabled .select2-selection__arrow, 86 | .select2-container--disabled .select2-selection__arrow { 87 | display: none; 88 | } 89 | .select2-container.disabled, 90 | .select2-container--disabled { 91 | pointer-events: none; 92 | } 93 | .select2 i { 94 | vertical-align: 0 !important; 95 | } 96 | @media ( max-height: 768px ) { 97 | .select2-results__options { 98 | max-height: calc( ( 100vh - 100px ) / 2 ) !important; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /srv/http/assets/css/simplekeyboard.css: -------------------------------------------------------------------------------- 1 | #infoBox { 2 | margin-bottom: 200px; 3 | } 4 | /* Simple-keyboard */ 5 | #keyboard { 6 | position: fixed; 7 | width: 100%; 8 | bottom: 0; 9 | background: var( --cga ) !important; 10 | box-shadow: var( --shadow-bar ); 11 | -webkit-touch-callout: none; /* iOS Safari */ 12 | } 13 | .simple-keyboard { 14 | /* max-width: 560px;*/ 15 | max-width: 485px; 16 | margin: auto; 17 | padding-right: 3px !important; 18 | background: none !important; 19 | } 20 | .hg-button { 21 | min-width: 25px !important; 22 | padding: 0 !important; 23 | font-family: Lato; 24 | font-size: 22px; 25 | text-shadow: var( --shadow-text ); 26 | background: var( --cg ) !important; 27 | box-shadow: var( --shadow-btn ) !important; 28 | border-radius: 4px !important; 29 | border-bottom: none !important; 30 | border-top: 1px solid var( --cg60 ); 31 | } 32 | .hg-button:active { 33 | opacity: 0.5; 34 | } 35 | .hg-functionBtn { 36 | width: 17%; 37 | max-width: 75px; 38 | } 39 | .hg-button-space { 40 | width: 50%; 41 | max-width: 205px; 42 | /* max-width: 290px;*/ 43 | } 44 | .hg-button-enter { 45 | font-size: 14px; 46 | background: var( --glossy-btn ) !important; 47 | background-color: var( --cm ) !important; 48 | border: none; 49 | } 50 | .hg-button-enter.disabled { 51 | color: var( --cg60 ) !important; 52 | background-color: var( --cg ) !important; 53 | pointer-events: none; 54 | } 55 | .hg-standardBtn, 56 | .hg-button-lock, 57 | .hg-button-numlock, 58 | .hg-button-num, 59 | .hg-button-alpha { 60 | max-width: 40px !important; 61 | } 62 | .hgrow2 { margin-left: 10px } 63 | .hgrow3 { margin-left: 20px } 64 | .hgrow4 { margin-left: 30px } 65 | .bgbl { background: var( --cm ) !important; } 66 | 67 | @media (max-width: 400px) { 68 | .hg-button { 69 | margin-right: 3px !important; 70 | } 71 | .hg-button-space { 72 | max-width: 190px; 73 | } 74 | .hg-button-enter { 75 | font-size: 14px !important; 76 | } 77 | } 78 | @media (max-width: 600px) { 79 | .simple-keyboard { max-width: 545px } 80 | .hgspace { max-width: 265px } 81 | .hgrow2, 82 | .hgrow3, 83 | .hgrow4 { 84 | margin-left: 0; 85 | } 86 | } 87 | @media (max-height: 320px) { 88 | .hg-button { 89 | height: 30px !important; 90 | font-size: 18px !important; 91 | } 92 | } -------------------------------------------------------------------------------- /srv/http/assets/data/regdomcodes.json: -------------------------------------------------------------------------------- 1 | { 2 | "00": "00", 3 | "Afghanistan": "AF", 4 | "Albania": "AL", 5 | "Algeria": "DZ", 6 | "American Samoa": "AS", 7 | "Andorra": "AD", 8 | "Anguilla": "AI", 9 | "Argentina": "AR", 10 | "Armenia": "AM", 11 | "Aruba": "AW", 12 | "Australia": "AU", 13 | "Austria": "AT", 14 | "Azerbaijan": "AZ", 15 | "Bahamas": "BS", 16 | "Bahrain": "BH", 17 | "Bangladesh": "BD", 18 | "Barbados": "BB", 19 | "Belarus": "BY", 20 | "Belgium": "BE", 21 | "Belize": "BZ", 22 | "Bermuda": "BM", 23 | "Bhutan": "BT", 24 | "Bolivia, Plurinational State of": "BO", 25 | "Bosnia and Herzegovina": "BA", 26 | "Brazil": "BR", 27 | "Brunei Darussalam": "BN", 28 | "Bulgaria": "BG", 29 | "Burkina Faso": "BF", 30 | "Cambodia": "KH", 31 | "Canada": "CA", 32 | "Cayman Islands": "KY", 33 | "Central African Republic": "CF", 34 | "Chad": "TD", 35 | "Chile": "CL", 36 | "China": "CN", 37 | "Christmas Island": "CX", 38 | "Colombia": "CO", 39 | "Costa Rica": "CR", 40 | "Croatia": "HR", 41 | "Cuba": "CU", 42 | "Cyprus": "CY", 43 | "Czechia": "CZ", 44 | "Côte d'Ivoire": "CI", 45 | "Denmark": "DK", 46 | "Dominica": "DM", 47 | "Dominican Republic": "DO", 48 | "Ecuador": "EC", 49 | "Egypt": "EG", 50 | "El Salvador": "SV", 51 | "Estonia": "EE", 52 | "Ethiopia": "ET", 53 | "Finland": "FI", 54 | "France": "FR", 55 | "French Guiana": "GF", 56 | "French Polynesia": "PF", 57 | "Georgia": "GE", 58 | "Germany": "DE", 59 | "Ghana": "GH", 60 | "Greece": "GR", 61 | "Greenland": "GL", 62 | "Grenada": "GD", 63 | "Guadeloupe": "GP", 64 | "Guam": "GU", 65 | "Guatemala": "GT", 66 | "Guyana": "GY", 67 | "Haiti": "HT", 68 | "Honduras": "HN", 69 | "Hong Kong": "HK", 70 | "Hungary": "HU", 71 | "Iceland": "IS", 72 | "India": "IN", 73 | "Indonesia": "ID", 74 | "Iran, Islamic Republic of": "IR", 75 | "Ireland": "IE", 76 | "Israel": "IL", 77 | "Italy": "IT", 78 | "Jamaica": "JM", 79 | "Japan": "JP", 80 | "Jordan": "JO", 81 | "Kazakhstan": "KZ", 82 | "Kenya": "KE", 83 | "Korea, Democratic People's Republic of": "KP", 84 | "Korea, Republic of": "KR", 85 | "Kuwait": "KW", 86 | "Latvia": "LV", 87 | "Lebanon": "LB", 88 | "Lesotho": "LS", 89 | "Liechtenstein": "LI", 90 | "Lithuania": "LT", 91 | "Luxembourg": "LU", 92 | "Macao": "MO", 93 | "Malawi": "MW", 94 | "Malaysia": "MY", 95 | "Maldives": "MV", 96 | "Malta": "MT", 97 | "Marshall Islands": "MH", 98 | "Martinique": "MQ", 99 | "Mauritania": "MR", 100 | "Mauritius": "MU", 101 | "Mayotte": "YT", 102 | "Mexico": "MX", 103 | "Micronesia, Federated States of": "FM", 104 | "Moldova, Republic of": "MD", 105 | "Monaco": "MC", 106 | "Mongolia": "MN", 107 | "Montenegro": "ME", 108 | "Morocco": "MA", 109 | "Nepal": "NP", 110 | "Netherlands": "NL", 111 | "New Zealand": "NZ", 112 | "Nicaragua": "NI", 113 | "Nigeria": "NG", 114 | "North Macedonia": "MK", 115 | "Northern Mariana Islands": "MP", 116 | "Norway": "NO", 117 | "Oman": "OM", 118 | "Pakistan": "PK", 119 | "Palau": "PW", 120 | "Panama": "PA", 121 | "Papua New Guinea": "PG", 122 | "Paraguay": "PY", 123 | "Peru": "PE", 124 | "Philippines": "PH", 125 | "Poland": "PL", 126 | "Portugal": "PT", 127 | "Puerto Rico": "PR", 128 | "Qatar": "QA", 129 | "Romania": "RO", 130 | "Russian Federation": "RU", 131 | "Rwanda": "RW", 132 | "Réunion": "RE", 133 | "Saint Barthélemy": "BL", 134 | "Saint Kitts and Nevis": "KN", 135 | "Saint Lucia": "LC", 136 | "Saint Martin (French part)": "MF", 137 | "Saint Pierre and Miquelon": "PM", 138 | "Saint Vincent and the Grenadines": "VC", 139 | "Samoa": "WS", 140 | "Saudi Arabia": "SA", 141 | "Senegal": "SN", 142 | "Serbia": "RS", 143 | "Singapore": "SG", 144 | "Slovakia": "SK", 145 | "Slovenia": "SI", 146 | "South Africa": "ZA", 147 | "Spain": "ES", 148 | "Sri Lanka": "LK", 149 | "Suriname": "SR", 150 | "Sweden": "SE", 151 | "Switzerland": "CH", 152 | "Syrian Arab Republic": "SY", 153 | "Taiwan, Province of China": "TW", 154 | "Tanzania, United Republic of": "TZ", 155 | "Thailand": "TH", 156 | "Togo": "TG", 157 | "Trinidad and Tobago": "TT", 158 | "Tunisia": "TN", 159 | "Turks and Caicos Islands": "TC", 160 | "Türkiye": "TR", 161 | "Uganda": "UG", 162 | "Ukraine": "UA", 163 | "United Arab Emirates": "AE", 164 | "United Kingdom": "GB", 165 | "United States": "US", 166 | "Uruguay": "UY", 167 | "Uzbekistan": "UZ", 168 | "Vanuatu": "VU", 169 | "Venezuela, Bolivarian Republic of": "VE", 170 | "Viet Nam": "VN", 171 | "Virgin Islands, U.S.": "VI", 172 | "Wallis and Futuna": "WF", 173 | "Yemen": "YE", 174 | "Zimbabwe": "ZW" 175 | } 176 | -------------------------------------------------------------------------------- /srv/http/assets/data/system-i2s.json: -------------------------------------------------------------------------------- 1 | { 2 | "Adafruit MAX98357": "hifiberry-dac", 3 | "Adafruit UDA1334A": "hifiberry-dac,i2s-mmap", 4 | "Allo Boss DAC": "allo-boss-dac-pcm512x-audio", 5 | "Allo Boss2 DAC": "allo-boss2-dac-audio", 6 | "Allo DigiOne": "allo-digione", 7 | "Allo DigiOne Signature": "allo-digione", 8 | "Allo Katana DAC": "allo-katana-dac-audio", 9 | "Allo Piano 2.0 DAC": "allo-boss-dac-pcm512x-audio", 10 | "Allo Piano 2.1 DAC": "allo-piano-dac-plus-pcm512x-audio", 11 | "Allo Piano 2.1 DAC (with Kali Reclocker)": "allo-piano-dac-plus-pcm512x-audio,glb_mclk", 12 | "Allo Piano DAC": "allo-piano-dac-pcm512x-audio", 13 | "Aoide DAC II": "i2s-dac", 14 | "Apple Pi DAC": "applepi-dac", 15 | "Audioinjector Addons": "audioinjector-addons", 16 | "Audioinjector Octo": "audioinjector-addons", 17 | "Audioinjector Stereo": "audioinjector-wm8731-audio", 18 | "Audioinjector Ultra": "audioinjector-ultra", 19 | "Audioinjector WM8731": "audioinjector-wm8731-audio", 20 | "Audioinjector Zero": "audioinjector-wm8731-audio", 21 | "Audiophonics Evo-Sabre DAC 2xES9038Q2M": "i-sabre-q2m", 22 | "Audiophonics I-Sabre DAC ES9023": "i2s-dac", 23 | "Audiophonics I-Sabre AMP DAC ES9023": "i2s-dac", 24 | "Audiophonics I-Sabre DAC ES9028Q2M": "i2s-dac", 25 | "Audiophonics I-Sabre DAC ES9038Q2M": "i-sabre-q2m", 26 | "AudioSense-Pi": "audiosense-pi", 27 | "BerryNOS": "hifiberry_dac", 28 | "ChipDip DAC": "chipdip-dac", 29 | "DACBerry AMP2": "allo-piano-dac-plus-pcm512x-audio", 30 | "DACBerry One+ - RCA": "iqaudio-dacplus", 31 | "DACBerry One+ - Digital": "justboom-digi", 32 | "DACBerry Pro": "iqaudio-dacplus", 33 | "DACBerry Pro+": "iqaudio-dacplus", 34 | "DACBerry RDY+ - RCA": "iqaudio-dacplus", 35 | "DACBerry RDY+ - Digital": "justboom-digi", 36 | "Digital Dreamtime Akkordion": "akkordion-iqdacplus", 37 | "Dion Audio Loco DAC-AMP": "dionaudio-loco", 38 | "Dion Audio Loco V2 DAC-AMP": "dionaudio-loco-v2", 39 | "Fe-Pi Audio": "fe-pi-audio", 40 | "Generic AKM AK4xxx (i2s-dac/rpi-dac)": "i2s-dac", 41 | "Generic AKM AK4xxx (hifiberry-dac)": "hifiberry-dac", 42 | "Generic Burr-Brown PCM1794": "i2s-dac", 43 | "Generic Burr-Brown PCM510x": "hifiberry-dac", 44 | "Generic Burr-Brown PCM512x": "hifiberry-dacplus", 45 | "Generic Cirrus Logic WM5102": "cirrus-wm5102", 46 | "Generic ESS ES90xx": "i2s-dac", 47 | "Generic RPi-DAC compatible": "i2s-dac", 48 | "HiFiBerry Amp": "hifiberry-amp", 49 | "HiFiBerry Amp+": "hifiberry-amp", 50 | "HiFiBerry Amp100": "hifiberry-amp100", 51 | "HiFiBerry Amp2": "hifiberry-dacplus", 52 | "HiFiBerry Amp3": "hifiberry-amp3", 53 | "HiFiBerry DAC": "hifiberry-dac", 54 | "HiFiBerry DAC+": "hifiberry-dacplus", 55 | "HiFiBerry DAC+ HD": "hifiberry-dacplushd", 56 | "HiFiBerry DAC+ Lite": "i2s-dac", 57 | "HiFiBerry DAC+ Pro": "hifiberry-dacplus", 58 | "HiFiBerry DAC+ Pro XLR": "hifiberry-dacplus", 59 | "HiFiBerry DAC+ RTC": "hifiberry-dac", 60 | "HiFiBerry DAC+ Zero": "hifiberry-dac", 61 | "HiFiBerry DAC+ADC": "hifiberry-dacplusadc", 62 | "HiFiBerry DAC+ADC Pro": "hifiberry-dacplusadcpro", 63 | "HiFiBerry DAC+DSP": "hifiberry-dacplusdsp", 64 | "HiFiBerry DAC2 Pro": "hifiberry-dacplus", 65 | "HiFiBerry Digi": "hifiberry-digi", 66 | "HiFiBerry Digi Pro": "hifiberry-digi-pro", 67 | "HiFiBerry Digi+": "hifiberry-digi", 68 | "HiFiBerry Digi+ Pro": "hifiberry-digi-pro", 69 | "HiFiBerry Digi2 Pro": "hifiberry-digi-pro", 70 | "HiFiBox DAC": "hifiberry-dacplus", 71 | "InnoMaker HiFi DAC": "allo-boss-dac-pcm512x-audio", 72 | "IQaudIO Amp": "iqaudio-dacplus", 73 | "IQaudIO Amp (with auto mute)": "iqaudio-dacplus,auto_mute_amp", 74 | "IQaudIO Amp (with unmute)": "iqaudio-dacplus,unmute_amp", 75 | "IQaudIO Codec": "iqaudio-codec", 76 | "IQaudIO DAC": "iqaudio-dac", 77 | "IQaudIO DAC Pro": "iqaudio-dacplus", 78 | "IQaudIO DAC+": "iqaudio-dacplus", 79 | "IQaudIO Digi WM8804": "iqaudio-digi-wm8804-audio", 80 | "IQaudIO Pi-DAC PRO": "iqaudio-dacplus", 81 | "IQaudIO Pi-DAC Zero": "iqaudio-dacplus", 82 | "IQaudIO Pi-DAC+": "iqaudio-dacplus", 83 | "IQaudIO Pi-Digi+": "iqaudio-digi-wm8804-audio", 84 | "IQaudIO Pi-DigiAMP+": "iqaudio-dacplus", 85 | "IQaudIO Pi-DigiAMP+ (with auto mute)": "iqaudio-dacplus,auto_mute_amp", 86 | "IQaudIO Pi-DigiAMP+ (with unmute)": "iqaudio-dacplus,unmute_amp", 87 | "JustBoom Amp HAT": "justboom-dac", 88 | "JustBoom Amp Zero": "justboom-dac", 89 | "JustBoom DAC": "justboom-dac", 90 | "JustBoom DAC HAT": "justboom-dac", 91 | "JustBoom DAC Zero": "justboom-dac", 92 | "JustBoom Digi": "justboom-digi", 93 | "Mamboberry HD DAC+": "i2s-dac", 94 | "Mamboberry Precision DAC+": "i2s-dac", 95 | "Maxim MAX98357A": "max98357a", 96 | "MERUS Audio Amp": "merus-amp", 97 | "NanoSound HiFi DAC Pro": "hifiberry-dacplus", 98 | "PecanPi DAC": "i2s-dac", 99 | "PI 2 Design 502DAC": "hifiberry-dacplus", 100 | "PI 2 Design 502DAC Pro": "hifiberry-dacplus", 101 | "PI 2 Design 503HTA Hybrid Tube Amp": "hifiberry-dac", 102 | "Picade HAT": "hifiberry-dac", 103 | "PiFi DAC+": "hifiberry-dacplus", 104 | "PiFi DAC HD": "pifi-dac-hd", 105 | "PiFi DAC Zero": "pifi-dac-zero", 106 | "PiFi Digi+": "hifiberry-digi", 107 | "Pimoroni pHAT DAC": "hifiberry-dac", 108 | "Pimoroni Audio DAC SHIM": "hifiberry-dac", 109 | "Blokas Labs Pisound": "pisound", 110 | "Raspberry Pi Codec Zero": "rpi-codeczero", 111 | "Raspberry Pi DAC+": "rpi-dacplus", 112 | "Raspberry Pi DAC Pro": "rpi-dacpro", 113 | "Raspberry Pi DigiAMP+": "rpi-digiampplus", 114 | "RaspiDAC 3": "raspidac3", 115 | "RaspyPlay 4": "hifiberry-dacplus", 116 | "Red Rocks Audio DigiDAC 1": "rra-digidac1-wm8741-audio", 117 | "RPi-DAC": "i2s-dac", 118 | "Soekris dam 1021": "hifiberry-dac", 119 | "ST400 Dac - Amp": "iqaudio-dacplus", 120 | "SuperAudioBoard": "superaudioboard", 121 | "SupTronics X400": "hifiberry-dacplus", 122 | "SupTronics X4000K": "i2s-dac", 123 | "Terra-Berry DAC 2": "hifiberry-dac", 124 | "Waveshare WM8960": "wm8960-soundcard", 125 | "Wolfson Audio": "cirrus-wm5102", 126 | "X10 DAC": "i2s-dac" 127 | } 128 | -------------------------------------------------------------------------------- /srv/http/assets/data/titles_with_paren: -------------------------------------------------------------------------------- 1 | 4th of July, Asbury Park (Sandy) 2 | 10,000 Years (Peace Is Now) 3 | The 59th Street Bridge Song (Feelin’ Groovy) 4 | Against All Odds (Take a Look at Me Now) 5 | All Night Long (All Night) 6 | Auctioneer (Another Engine) 7 | Bang A Gong (Get It On) 8 | Bloodletting (The Vampire Song) 9 | Brandy (You’re a Fine Girl) 10 | Brass in Pocket (I’m Special) 11 | Break on Through (To the Other Side) 12 | Caffeine, Nicotine, Benzedrine (And Wish Me Luck) 13 | The Chipmunk Song (Christmas Don’t Be Late) 14 | Christmas (Baby, Please Come Home) 15 | Come On Over (All I Want Is You) 16 | Da Doo Ron Ron (When He Walked Me Home) 17 | Damn Sam (I Love a Woman That Rains) 18 | December 1963 (Oh, What A Night) 19 | Don’t Be Stupid (You Know I Love You) 20 | Don’t Forget Me (When I’m Gone) 21 | Don’t Go Away Mad (Just Go Away) 22 | Don’t Know What You Got (Till It’s Gone) 23 | Don’t You (Forget About Me) 24 | Doo Wop (That Thing) 25 | Dreamboat Annie (Fantasy Child) 26 | Dude (Looks Like a Lady) 27 | Earth Angel (Will You Be Mine) 28 | Escape (The Piña Colada Song) 29 | Everybody (Backstreet’s Back) 30 | Everybody’s Free (To Feel Good) 31 | Everybody’s Free (To Wear Sunscreen) 32 | Everything (Between Us) 33 | Exhale (Shoop Shoop) 34 | Exit Music (For a Film) 35 | Falling in Love (Is Hard on the Knees) 36 | Fooled Again (I Don’t Like It) 37 | Forever Afternoon (Tuesday?) 38 | Fuck It (I Don’t Want You Back) 39 | Fuckin’ With My Head (Mountain Dew Rock) 40 | Get Naked (I Got a Plan) 41 | Gimme! Gimme! Gimme! (A Man After Midnight) 42 | Good Riddance (Time of Your Life) 43 | Happy Xmas (War Is Over) 44 | Hard Knock Life (Ghetto Anthem) 45 | He Hit Me (It Felt Like A Kiss) 46 | Hemorrhage (In My Hands) 47 | High 5 (Rock the Catskills) 48 | Home Ain’t Where His Heart Is (Anymore) 49 | How Sweet It Is (To Be Loved By You) 50 | I Believe (When I Fall in Love it Will Be Forever) 51 | I Can’t Go for That (No Can Do) 52 | I Don’t Care (So There) 53 | I Got You (I Feel Good) 54 | I Knew You Were Waiting (For Me) 55 | I Never Loved a Man (The Way I Love You) 56 | I Ran (So Far Away) 57 | I Wanna Dance with Somebody (Who Loves Me) 58 | I Want Your (Hands on Me) 59 | Instant Karma (We All Shine On) 60 | Istanbul (Not Constantinople) 61 | It’s Alright, Ma (I’m Only Bleeding) 62 | It’s Only Rock ’n’ Roll (But I Like It) 63 | It’s The End of the World As We Know It (And I Feel Fine) 64 | I’d Do Anything for Love (But I Won’t Do That) 65 | I’d Lie for You (And That’s the Truth) 66 | I’ll Be Loving You (Forever) 67 | I’m Gonna Be (500 Miles) 68 | I’m Holdin’ On to Love (To Save My Life) 69 | Jesus (Don’t Touch My Baby) 70 | Jump (For My Love) 71 | Lady (You Bring Me Up) 72 | Lily (My One and Only) 73 | Major Tom (Coming Home) 74 | Mama Told Me (Not to Come) 75 | Many Men (Wish Death) 76 | Molly (16 Candles Down the Drain) 77 | Money (That’s What I Want) 78 | Morning Train (Nine to Five) 79 | Movin’ Out (Anthony’s Song) 80 | Na Na Na (Na Na Na Na Na Na Na Na Na) 81 | No-One but You (Only the Good Die Young) 82 | Norwegian Wood (This Bird Has Flown) 83 | Only Girl (In the World) 84 | Operation Spirit (The Tyranny of Tradition) 85 | P.Y.T (Pretty Young Thing) 86 | Pay No Mind (Snoozer) 87 | Platypus (I Hate You) 88 | Pretty Fly (For a White Guy) 89 | Pride (In The Name Of Love) 90 | Puddin N’ Tain (Ask Me Again, I’ll Tell You the Same) 91 | Pulling Mussels (From The Shell) 92 | Rebirth of Slick (Cool Like Dat) 93 | Remember (Walking in the Sand) 94 | Rock! Rock! (Till You Drop) 95 | Rock Me Again and Again and Again and Again and Again and Again (Six Times) 96 | Rocket Man (I Think It’s Going To Be A Long Long Time) 97 | Rockin’ Around (With You) 98 | Rollout (My Business) 99 | Rosalita (Come Out Tonight) 100 | Sad Songs (Say So Much) 101 | Same Ol’ Situation (S.O.S.) 102 | Save a Horse (Ride a Cowboy) 103 | Scary Monsters (and Super Creeps) 104 | Second-Hand Satin Lady (And a Bargain Basement Boy) 105 | Separate Ways (Worlds Apart) 106 | Separate Ways (Worlds Apart) 107 | Sex (I’m A…) 108 | She Got the Goldmine (I Got the Shaft) 109 | The Shoop Shoop Song (It’s in His Kiss) 110 | Single Ladies (Put a Ring On It) 111 | So. Central Rain (I’m Sorry) 112 | Sometimes (Lester Piggot) 113 | Space (I Believe In) 114 | St. Elmo’s Fire (Man in Motion) 115 | Sweet Dreams (Are Made of This) 116 | T.nT. (Terror ‘n Tinseltown) 117 | That’s the Way (I Like It) 118 | That’s Too Bad (Byron Jam) 119 | Theme from The Dukes of Hazzard (Good Ol’ Boys) 120 | There’s A Moon in the Sky (Called the Moon) 121 | This Must Be the Place (Naive Melody) 122 | To Be Young (Is to Be Sad, Is to Be High) 123 | Tonight’s the Night (Gonna Be Alright) 124 | Truckdrivin’ Neighbors Downstairs (Yellow Sweat) 125 | Try (Just a Little Bit Harder) 126 | Turn! Turn! Turn! (to Everything There Is a Season) 127 | Ultra Anxiety (Teenage Style) 128 | Until You Come Back to Me (That’s What I’m Gonna Do) 129 | Wham Rap! (Enjoy What You Do) 130 | The Woman in Me (Needs the Man in You) 131 | You Got It (The Right Stuff) 132 | You’re in My Heart (The Final Acclaim) 133 | You’re Pretty Good Looking (For a Girl) 134 | -------------------------------------------------------------------------------- /srv/http/assets/fonts/Inconsolata.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rern/rAudio/f5b314fcd3162126f1d4e7bf43578e7357331090/srv/http/assets/fonts/Inconsolata.woff2 -------------------------------------------------------------------------------- /srv/http/assets/fonts/Lato-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rern/rAudio/f5b314fcd3162126f1d4e7bf43578e7357331090/srv/http/assets/fonts/Lato-Light.woff2 -------------------------------------------------------------------------------- /srv/http/assets/fonts/Lato-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rern/rAudio/f5b314fcd3162126f1d4e7bf43578e7357331090/srv/http/assets/fonts/Lato-Regular.woff2 -------------------------------------------------------------------------------- /srv/http/assets/fonts/rern.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rern/rAudio/f5b314fcd3162126f1d4e7bf43578e7357331090/srv/http/assets/fonts/rern.woff2 -------------------------------------------------------------------------------- /srv/http/assets/img/addons/thumbdab.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rern/rAudio/f5b314fcd3162126f1d4e7bf43578e7357331090/srv/http/assets/img/addons/thumbdab.jpg -------------------------------------------------------------------------------- /srv/http/assets/img/addons/thumbfran.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rern/rAudio/f5b314fcd3162126f1d4e7bf43578e7357331090/srv/http/assets/img/addons/thumbfran.gif -------------------------------------------------------------------------------- /srv/http/assets/img/addons/thumbgpio.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rern/rAudio/f5b314fcd3162126f1d4e7bf43578e7357331090/srv/http/assets/img/addons/thumbgpio.gif -------------------------------------------------------------------------------- /srv/http/assets/img/addons/thumbplst.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rern/rAudio/f5b314fcd3162126f1d4e7bf43578e7357331090/srv/http/assets/img/addons/thumbplst.jpg -------------------------------------------------------------------------------- /srv/http/assets/img/addons/thumbrank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rern/rAudio/f5b314fcd3162126f1d4e7bf43578e7357331090/srv/http/assets/img/addons/thumbrank.png -------------------------------------------------------------------------------- /srv/http/assets/img/addons/thumbwebr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rern/rAudio/f5b314fcd3162126f1d4e7bf43578e7357331090/srv/http/assets/img/addons/thumbwebr.png -------------------------------------------------------------------------------- /srv/http/assets/img/coverart.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 23 | 26 | 31 | 35 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /srv/http/assets/img/i2cbackpack.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rern/rAudio/f5b314fcd3162126f1d4e7bf43578e7357331090/srv/http/assets/img/i2cbackpack.jpg -------------------------------------------------------------------------------- /srv/http/assets/img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rern/rAudio/f5b314fcd3162126f1d4e7bf43578e7357331090/srv/http/assets/img/icon.png -------------------------------------------------------------------------------- /srv/http/assets/img/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /srv/http/assets/img/lcd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rern/rAudio/f5b314fcd3162126f1d4e7bf43578e7357331090/srv/http/assets/img/lcd.jpg -------------------------------------------------------------------------------- /srv/http/assets/img/lcdchar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rern/rAudio/f5b314fcd3162126f1d4e7bf43578e7357331090/srv/http/assets/img/lcdchar.jpg -------------------------------------------------------------------------------- /srv/http/assets/img/mpdoled.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rern/rAudio/f5b314fcd3162126f1d4e7bf43578e7357331090/srv/http/assets/img/mpdoled.jpg -------------------------------------------------------------------------------- /srv/http/assets/img/powerbutton.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rern/rAudio/f5b314fcd3162126f1d4e7bf43578e7357331090/srv/http/assets/img/powerbutton.jpg -------------------------------------------------------------------------------- /srv/http/assets/img/relays.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rern/rAudio/f5b314fcd3162126f1d4e7bf43578e7357331090/srv/http/assets/img/relays.jpg -------------------------------------------------------------------------------- /srv/http/assets/img/rotaryencoder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rern/rAudio/f5b314fcd3162126f1d4e7bf43578e7357331090/srv/http/assets/img/rotaryencoder.jpg -------------------------------------------------------------------------------- /srv/http/assets/img/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rern/rAudio/f5b314fcd3162126f1d4e7bf43578e7357331090/srv/http/assets/img/splash.png -------------------------------------------------------------------------------- /srv/http/assets/img/vu.svg: -------------------------------------------------------------------------------- 1 | 2 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 20 60 | 10 61 | 7 62 | 5 63 | 3 64 | 2 65 | 1 66 | 0 67 | 1 68 | 2 69 | 3 70 | 71 | 0 72 | 20 73 | 40 74 | 60 75 | 80 76 | 100 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /srv/http/assets/img/vuled.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rern/rAudio/f5b314fcd3162126f1d4e7bf43578e7357331090/srv/http/assets/img/vuled.jpg -------------------------------------------------------------------------------- /srv/http/assets/js/addons.js: -------------------------------------------------------------------------------- 1 | var KEYS = [ 'installurl', 'postmessage', 'title', 'uninstall', 'version' ]; 2 | 3 | $( '.helphead' ).remove(); 4 | if ( V.localhost ) $( 'a' ).removeAttr( 'href' ); 5 | $( '.container' ).on( 'click', '.revision', function() { 6 | $this = $( this ); 7 | $revisiontext = $this.parent().next(); 8 | var hidden = $revisiontext.hasClass( 'hide' ); 9 | $revisiontext.toggleClass( 'hide', ! hidden ); 10 | $this.toggleClass( 'active' ); 11 | } ).on( 'click', '#list li', function() { 12 | alias = $( this ).data( 'alias' ); 13 | $( 'html, body' ).scrollTop( $( '#'+ alias ).offset().top - 50 ); 14 | } ).on( 'click', '.infobtn', function() { 15 | $this = $( this ); 16 | if ( $this.hasClass( 'disabled' ) && ! $this.hasClass( 'uninstall' ) ) { 17 | if ( ! S.status.online ) { 18 | INFO( { 19 | icon : 'addons' 20 | , title : 'Addons' 21 | , message : 'Internet connection is offline.' 22 | } ); 23 | } 24 | return 25 | } 26 | 27 | addonData( $this ); 28 | if ( 'option' in V.addon ) { 29 | optionGet(); 30 | } else { 31 | INFO( { 32 | icon : 'addons' 33 | , title : V.addon.title 34 | , message : V.addon.version ? V.label +' to '+ V.addon.version +' ?' : V.label +'?' 35 | , ok : postData 36 | } ); 37 | } 38 | } ).on( 'click', '.thumbnail', function() { 39 | if ( S.status.online ) $( this ).prev().find( '.source' )[ 0 ].trigger( 'click' ); 40 | } ).press( { 41 | delegate : '.install' 42 | , action : e => { 43 | if ( ! S.status.online ) return 44 | 45 | addonData( $( e.currentTarget ) ); 46 | INFO( { 47 | icon : 'addons' 48 | , title : V.addon.title 49 | , list : [ 'Branch / Release', 'text' ] 50 | , boxwidth : 200 51 | , values : 'UPDATE' 52 | , ok : () => { 53 | V.branch = _INFO.val(); 54 | if ( ! V.branch ) return 55 | 56 | V.installurl = V.addon.installurl.replace( 'raw/main', 'raw/'+ V.branch ); 57 | 'option' in V.addon ? optionGet() : postData(); 58 | } 59 | } ); 60 | } 61 | } ); 62 | 63 | function addonData( $this ) { 64 | V.alias = $this.parents( 'form' ).data( 'alias' ); 65 | V.addon = S[ V.alias ]; 66 | V.branch = 'main'; 67 | V.label = $this.find( '.label' ).text(); 68 | KEYS.forEach( k => V[ k ] = V.addon[ k ] || -1 ); 69 | } 70 | function buttonLabel( icon, label ) { 71 | return ICON( icon ) +' '+ label +''; 72 | } 73 | function optionGet() { 74 | INFO( $.extend( { 75 | icon : 'addons' 76 | , title : V.addon.title 77 | , ok : () => postData( _INFO.val() ) 78 | }, V.addon.option ) ); 79 | } 80 | function postData( opt ) { 81 | var input = {} 82 | KEYS = [ 'alias', 'branch', 'label' ].concat( KEYS ); 83 | KEYS.forEach( k => { 84 | if ( V[ k ] !== -1 ) input[ k ] = V[ k ]; 85 | } ); 86 | if ( opt ) { 87 | if ( typeof opt !== 'object' ) opt = [ opt ]; 88 | opt.forEach( v => input[ 'opt[]' ] = v ); 89 | } 90 | if ( opt ) opt.forEach( v => input[ 'opt[]' ] = v ); 91 | COMMON.formSubmit( input ); 92 | } 93 | function renderPage() { 94 | var list = ''; 95 | var addons = ''; 96 | delete S.push; 97 | $.each( S, ( alias, addon ) => { 98 | if ( alias === 'status' || ( S.status.hidden.includes( alias ) ) ) return 99 | var notverified = S.status.notverified.includes( alias ); 100 | var version = 'version' in addon ? ' '+ addon.version +'' : ''; 101 | if ( 'revision' in addon ) { 102 | var revision = '

'; 103 | addon.revision.forEach( el => revision += ' '+ el +'
' ); 104 | revision += '

'; 105 | } else { 106 | var revision = ''; 107 | } 108 | if ( notverified ) { 109 | var button = V.i_warning + addon.notverified; 110 | } else { 111 | var installed = S.status.installed.includes( alias ) ? ' installed' : ''; 112 | var update = S.status.update.includes( alias ) ? ' update' : ''; 113 | var disabled = '' 114 | if ( installed ) { 115 | disabled = update ? '' : ' disabled'; 116 | var buttonlabel = buttonLabel( 'update', 'Update' ); 117 | } else if ( addon.buttonlabel ) { 118 | var buttonlabel = buttonLabel( addon.buttonlabel[ 0 ], addon.buttonlabel[ 1 ] ); 119 | } else { 120 | var buttonlabel = buttonLabel( 'plus-circle', 'Install' ); 121 | } 122 | var button = ''+ buttonlabel +''; 123 | if ( installed && 'uninstall' in addon ) button += ' Uninstall'; 124 | } 125 | addons += `\ 126 |
127 |
128 | ${ addon.title }${ version } 129 | ${ revision } 130 |
131 |

${ addon.description }
source

132 | ${ button } 133 |
134 |
135 | 136 |
137 |
138 | `; 139 | list += '
  • '+ addon.title +'
  • '; 140 | } ); 141 | html = ''+ addons; 142 | $( '.container' ).html( html ).promise().done( function() { 143 | if ( ! S.status.online ) $( '.infobtn' ).addClass( 'disabled' ); 144 | $( '.head, .container, #bar-bottom' ).removeClass( 'hide' ); 145 | COMMON.loaderHide(); 146 | $( 'a[ href ]' ).prop( 'tabindex', -1 ); 147 | $( '.infobtn:not(.disabled)' ).prop( 'tabindex', 0 ); 148 | } ); 149 | } 150 | -------------------------------------------------------------------------------- /srv/http/assets/js/guide.js: -------------------------------------------------------------------------------- 1 | document.title = 'Guide'; 2 | [ '.helphead', '#debug' ].forEach( cl => document.querySelector( cl ).remove() ); 3 | [ '.head', '#bar-bottom' ].forEach( k => document.querySelector( k ).classList.remove( 'hide' ) ); 4 | var P = { 5 | playback : 1 6 | , library : 22 7 | , playlist : 39 8 | , settings : 47 9 | , total : 57 10 | } 11 | var E = { 12 | bar : document.getElementById( 'bar-bottom' ) 13 | , img : document.querySelector( 'img' ) 14 | }; 15 | var HASH = '?v='+ Math.round( Date.now() / 1000 ); 16 | var n = 1; 17 | [ 'close', 'library', 'playback', 'playlist', 'settings', 'prev', 'next' ].forEach( id => { 18 | E[ id ] = document.getElementById( id ) 19 | E[ id ].addEventListener( 'click', function() { 20 | var tabactive = document.querySelector( 'div.active' ); 21 | if ( this === tabactive ) return 22 | 23 | var active; 24 | if ( id === 'close' ) { 25 | location.href = '/'; 26 | } else if ( id === 'next' ) { 27 | n++; 28 | if ( n > P.total ) n = 1; 29 | } else if ( id === 'prev' ) { 30 | n--; 31 | if ( n < 1 ) n = P.total; 32 | } else { 33 | n = P[ id ]; 34 | } 35 | if ( n >= P.settings ) { 36 | active = 'settings'; 37 | } else if ( n >= P.playlist ) { 38 | active = 'playlist'; 39 | } else if ( n >= P.library ) { 40 | active = 'library'; 41 | } else { 42 | active = 'playback'; 43 | } 44 | tabactive.className = ''; 45 | document.getElementById( active ).className = 'active' 46 | E.img.src = '/assets/img/guide/'+ n +'.jpg'+ HASH; 47 | } ); 48 | } ); 49 | E.playback.classList.add( 'active' ); 50 | //--------------------------------------------------------------------------------------- 51 | document.querySelector( '.i-gear' ).addEventListener( 'click', () => { 52 | var hide = window.getComputedStyle( E.bar ).getPropertyValue( 'display' ) === 'none' ; 53 | E.bar.style.display = hide ? 'block' : 'none'; 54 | } ); 55 | document.body.addEventListener( 'keydown', e => { 56 | switch ( e.key ) { 57 | case 'ArrowLeft': 58 | case 'ArrowUp': 59 | E.prev.click(); 60 | break 61 | case 'ArrowRight': 62 | case 'ArrowDown': 63 | case ' ': 64 | e.preventDefault(); 65 | E.next.click(); 66 | break 67 | case 'x': 68 | if ( e.ctrlKey ) E.close.click(); 69 | break 70 | } 71 | } ); 72 | if ( navigator.maxTouchPoints ) { // swipe 73 | var xstart; 74 | window.addEventListener( 'touchstart', e => { 75 | xstart = e.changedTouches[ 0 ].pageX; 76 | } ); 77 | window.addEventListener( 'touchend', e => { 78 | var xdiff = xstart - e.changedTouches[ 0 ].pageX; 79 | if ( Math.abs( xdiff ) > 100 ) { 80 | xdiff > 0 ? E.next.click() : E.prev.click(); 81 | } 82 | } ); 83 | } 84 | -------------------------------------------------------------------------------- /srv/http/assets/js/simplekeyboard.js: -------------------------------------------------------------------------------- 1 | $( function() { // document ready start >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 2 | 3 | var current = 'alpha'; 4 | var capslock = false; 5 | var numslock = false; 6 | /*var normal = [ 7 | '1 2 3 4 5 6 7 8 9 0 - =' 8 | , 'q w e r t y u i o p [ ]' 9 | , "a s d f g h j k l ; ' \\" 10 | , 'z x c v b n m , . / {enter}' 11 | , '{shift} {lock} {space} {bksp}' 12 | ]; 13 | var shift = [ 14 | '! @ # $ % ^ & * ( ) _ +' 15 | , 'Q W E R T Y U I O P { }' 16 | , 'A S D F G H J K L : " |' 17 | , 'Z X C V B N M < > ? {enter}' 18 | , '{shift} {lock} {space} {bksp}' 19 | ];*/ 20 | var buttontheme = [ 21 | { class: 'hgrow1', buttons: '1 !' } 22 | , { class: 'hgrow2', buttons: 'q Q' } 23 | , { class: 'hgrow3', buttons: 'a A' } 24 | , { class: 'hgrow4', buttons: 'z Z' } 25 | ] 26 | var narrow = [ 27 | 'q w e r t y u i o p' 28 | , "a s d f g h j k l '" 29 | , 'z x c v b n m . {enter}' 30 | , '{shift} {lock} {num} {space} {bksp}' 31 | ]; 32 | var narrowshift = [ 33 | 'Q W E R T Y U I O P' 34 | , 'A S D F G H J K L "' 35 | , 'Z X C V B N M , {enter}' 36 | , '{shift} {lock} {num} {space} {bksp}' 37 | ]; 38 | var narrownum = [ 39 | '1 2 3 4 5 6 7 8 9 0' 40 | , '! @ # $ % ? & * ( )' 41 | , '+ - = / ; : . , {enter}' 42 | , '{numshift} {numlock} {alpha} {space} {bksp}' 43 | ]; 44 | var narrownumshift = [ 45 | '1 2 3 4 5 6 7 8 9 0' 46 | , '` ~ _ ^ < > [ ] { }' 47 | , '+ - = / × ÷ | \\ {enter}' 48 | , '{numshift} {numlock} {alpha} {space} {bksp}' 49 | ]; 50 | var narrowbuttontheme = [ 51 | { class: 'hgrow2', buttons: 'q Q 1 {shift} {numshift}' } 52 | , { class: 'hgrow3', buttons: 'a A ! `' } 53 | , { class: 'hgrow4', buttons: 'z Z +' } 54 | ]; 55 | 56 | var Keyboard = window.SimpleKeyboard.default; 57 | var keyboard = new Keyboard( { 58 | onChange : value => onChange( value ) 59 | , onKeyPress : key => onKeyPress( key ) 60 | , layout: { 61 | alpha : narrow 62 | , shift : narrowshift 63 | , num : narrownum 64 | , numshift : narrownumshift 65 | } 66 | , layoutName : "alpha" 67 | , display : { 68 | '{alpha}' : 'Aa' 69 | , '{bksp}' : ICON( 'backspace' ) 70 | , '{enter}' : 'OK' 71 | , '{lock}' : ICON( 'capslock' ) 72 | , '{numlock}' : ICON( 'capslock' ) 73 | , '{num}' : '1?' 74 | , '{numshift}' : ICON( 'shift' ) 75 | , '{shift}' : ICON( 'shift' ) 76 | , '{space}' : ' ' 77 | } 78 | , buttonTheme: narrowbuttontheme 79 | } ); 80 | var $kb = $( '#keyboard' ); 81 | var inputs = 'input[type=text], input[type=textarea], input[type=passowrd]'; 82 | 83 | $( 'body' ).on( 'click', inputs, function() { 84 | $kb.removeClass( 'hide' ); 85 | $( '#infoList input' ).removeClass( 'active' ); 86 | $( this ).addClass( 'active' ); 87 | keyboard.setInput( $( this ).val() ); 88 | } ).on( 'click touchstart', function( e ) { 89 | if ( ! $kb.hasClass( 'hide' ) 90 | && ! $( e.target ).is( inputs ) 91 | && ! $( e.target ).parents( '#keyboard' ).length 92 | && e.target.id !== 'keyboard' 93 | ) hideKeyboard(); 94 | } ); 95 | 96 | function hideKeyboard() { 97 | keyboard.clearInput(); 98 | $kb.addClass( 'hide' ); 99 | } 100 | function onChange( value ) { 101 | $( 'input.active' ).val( value ); 102 | if ( $( 'input.active' ).prop( 'id' ) === 'pl-search-input' ) { 103 | $( '#pl-search-input' ).trigger( 'input' ); 104 | } else { 105 | $( '#infoList input' ).trigger( 'keyup' ); 106 | } 107 | } 108 | function onKeyPress( key ) { // input value not yet changed until onChange 109 | var id = $( 'input.active' ).prop( 'id' ); 110 | var layout = keyboard.options.layoutName; 111 | switch ( key ) { 112 | case '{shift}': 113 | current = layout !== 'shift' ? 'shift' : 'alpha'; 114 | capslock = false; 115 | break; 116 | case '{numshift}': 117 | current = layout !== 'numshift' ? 'numshift' : 'num'; 118 | numslock = false; 119 | break; 120 | case '{num}': 121 | current = 'num'; 122 | capslock = false; 123 | break; 124 | case '{alpha}': 125 | current = 'alpha'; 126 | break; 127 | case '{lock}': 128 | if ( layout !== 'shift' ) { 129 | current = 'shift'; 130 | capslock = true; 131 | } else { 132 | current = capslock ? 'alpha' : 'shift'; 133 | capslock = current === 'shift' ? true : false; 134 | } 135 | break; 136 | case '{numlock}': 137 | if ( layout !== 'numshift' ) { 138 | current = 'numshift'; 139 | numslock = true; 140 | } else { 141 | current = numslock ? 'num' : 'numshift'; 142 | numslock = current === 'numshift' ? true : false; 143 | } 144 | break; 145 | case '{enter}': 146 | hideKeyboard(); 147 | break; 148 | default: 149 | if ( ( layout === 'shift' && ! capslock ) ) { 150 | current = 'alpha'; 151 | } else if ( layout === 'numshift' && ! numslock ) { 152 | current = 'num'; 153 | } 154 | } 155 | keyboard.setOptions( { layoutName: current } ); 156 | $( '.hg-button-lock' ).toggleClass( 'bgbl', capslock ); 157 | $( '.hg-button-numlock' ).toggleClass( 'bgbl', numslock ); 158 | } 159 | 160 | } ); // document ready end <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 161 | -------------------------------------------------------------------------------- /srv/http/bash/albumthumbnail.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | path=$1 4 | [[ $2 == true ]] && overwrite=1 5 | bar=' ' 6 | padw=' ' 7 | padg=' ' 8 | padgr=' ' 9 | warn=' ! ' 10 | 11 | . /srv/http/bash/common.sh 12 | 13 | basename $0 .sh > $dirshm/script 14 | 15 | hhmmss() { 16 | local fmt 17 | (( $total < 3600 )) && fmt='+%M:%S' || fmt='+%H:%M:%S' 18 | date -d@$1 -u $fmt 19 | } 20 | tagColor() { 21 | echo ''$@'' 22 | } 23 | warningWrite() { 24 | echo " $warn" No write permission: $( tagColor $dir ) $( stat -c '%A (%a)' "$dir" ) 25 | } 26 | 27 | dir="/mnt/MPD/$path" 28 | [[ ! -w "$dir" ]] && warningWrite && exit 29 | # -------------------------------------------------------------------- 30 | 31 | echo -e "\nDirectory: $( tagColor $dir )\n" 32 | 33 | SECONDS=0 34 | 35 | albumfile=/srv/http/data/mpd/album 36 | 37 | if [[ ! $path ]]; then 38 | mpdpathlist=$( cut -d^ -f7 $albumfile ) 39 | else 40 | mpdpathlist=$( find "$dir" -type d | cut -c10- ) 41 | fi 42 | unsharp=0x.5 43 | 44 | [[ ! $mpdpathlist ]] && echo "$padw No albums found in database." && exit 45 | # -------------------------------------------------------------------- 46 | count=$( wc -l <<< $mpdpathlist ) 47 | while read mpdpath; do 48 | (( i++ )) 49 | percent=$(( $i * 100 / $count )) 50 | if (( $percent > 0 )); then 51 | sec=$SECONDS 52 | total=$(( $sec * 100 / $percent )) 53 | else 54 | sec=0 55 | total=0 56 | fi 57 | echo $percent'% '$( hhmmss $sec )/$( hhmmss $total )'' 58 | echo $i/$count $( tagColor $mpdpath ) 59 | 60 | dir="/mnt/MPD/$mpdpath" 61 | if [[ ! $overwrite ]] && ls "$dir/coverart".* &> /dev/null; then 62 | echo " $padw Thumbnail already exists." 63 | continue 64 | fi 65 | 66 | for name in cover folder front album; do # file 67 | for ext in jpg png gif; do 68 | coverfile="$dir/$name.$ext" 69 | [[ -e $coverfile ]] && break 2 70 | coverfile="$dir/${name^}.$ext" # capitalize 71 | [[ -e $coverfile ]] && break 2 72 | done 73 | coverfile= 74 | done 75 | if [[ ! $coverfile ]]; then # embedded 76 | files=$( mpc ls "$mpdpath" 2> /dev/null ) 77 | while read file; do 78 | file="/mnt/MPD/$file" 79 | if [[ -f "$file" ]]; then 80 | coverfile="$dir/cover.jpg" 81 | kid3-cli -c "select \"$file\"" -c "get picture:\"$coverfile\"" &> /dev/null 82 | [[ ! -e $coverfile ]] && coverfile= 83 | break 84 | fi 85 | done <<< $files 86 | fi 87 | if [[ $coverfile ]]; then 88 | error= 89 | ext=${coverfile: -3} 90 | if [[ $ext == gif ]]; then 91 | [[ $( gifsicle -I "$coverfile" | awk 'NR==1 {print $NF}' ) == images ]] && echo " Resize aninated GIF ..." 92 | gifsicle -O3 --resize-fit 200x200 "$coverfile" > "$dir/coverart.gif" 93 | [[ $? == 0 ]] && gifsicle -O3 --resize-fit 80x80 "$coverfile" > "$dir/thumb.gif" || error=1 94 | else 95 | magick "$coverfile" -thumbnail 200x200\> -unsharp $unsharp "$dir/coverart.jpg" 96 | [[ $? == 0 ]] && magick "$coverfile" -thumbnail 80x80\> -unsharp $unsharp "$dir/thumb.jpg" || error=1 97 | fi 98 | if [[ $error ]]; then 99 | if [[ ! -w "$dir" ]]; then 100 | warningWrite 101 | errorwrite+=" 102 | $dir" 103 | else 104 | echo " $warn Coversion failed: $( tagColor $coverfile )" 105 | errorconvert+=" 106 | $coverfile" 107 | fi 108 | else 109 | (( thumb++ )) 110 | echo " $padg #$thumb - Thumbnail created." 111 | fi 112 | else 113 | echo " $padgr No coverart found." 114 | fi 115 | done <<< $mpdpathlist 116 | 117 | [[ $errorwrite ]] && echo " 118 | $warn No write permission: 119 | $errorwrite" 120 | [[ $errorconvert ]] && echo " 121 | $warn Coversion failed: 122 | $errorconvert" 123 | 124 | echo " 125 | Duration: $( hhmmss $SECONDS ) 126 | 127 | $bar Done. 128 |
    129 | " 130 | -------------------------------------------------------------------------------- /srv/http/bash/bashrc: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PS1='\[\e[38;5;242m\]\h\[\e[0m\]\ 4 | :\ 5 | \[\e[36m\]\w\[\e[0m\] \ 6 | \[\e[30m\e[46m\] \$ \[\e[0m\] ' 7 | -------------------------------------------------------------------------------- /srv/http/bash/bluealsa-dbus.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # RPi as renderer - bluealsa-dbus.service > this: 4 | # - start: set Player dest file 5 | # - connect/disconnect: networks-bluetooth.sh 6 | # - status: dbus emits events and data 7 | # start play - cmd.sh playerstart 8 | # changed - status-push.sh 9 | 10 | import dbus 11 | import dbus.service 12 | import dbus.mainloop.glib 13 | from gi.repository import GLib 14 | from subprocess import Popen 15 | 16 | AGENT_INTERFACE = 'org.bluez.Agent1' 17 | path = '/test/autoagent' 18 | filesink = '/srv/http/data/shm/bluetoothsink' 19 | 20 | def statusPush(): 21 | Popen( [ '/srv/http/bash/status-push.sh' ] ) 22 | 23 | def property_changed( interface, changed, invalidated, path ): 24 | for name, value in changed.items(): 25 | # Player : /org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/playerX (sink not emit this data) 26 | # Connected : 1 | 0 (use udev rules instead) 27 | # Position : elapsed 28 | # State : active | idle | pending 29 | # Status : paused | playing | stopped 30 | # Track : metadata 31 | # Type : dest playerX 32 | if name == 'Player': 33 | with open( '/srv/http/data/shm/bluetoothdest', 'w' ) as f: f.write( value ) 34 | elif name == 'Position' or name == 'Track': 35 | statusPush() 36 | elif name == 'Status': 37 | if value == 'playing' and not os.path.isfile( filesink ): 38 | open( filesink, 'a' ) 39 | Popen( [ '/srv/http/bash/cmd.sh', 'playerstart' ] ) 40 | statusPush() 41 | 42 | if __name__ == '__main__': 43 | dbus.mainloop.glib.DBusGMainLoop( set_as_default=True ) 44 | bus = dbus.SystemBus() 45 | bus.add_signal_receiver( property_changed, bus_name='org.bluez', 46 | dbus_interface='org.freedesktop.DBus.Properties', 47 | signal_name='PropertiesChanged', 48 | path_keyword='path' ) 49 | mainloop = GLib.MainLoop() 50 | obj = bus.get_object( 'org.bluez', '/org/bluez' ) 51 | mainloop.run() 52 | -------------------------------------------------------------------------------- /srv/http/bash/bluetoothbutton.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /srv/http/bash/common.sh 4 | 5 | control=$( < $dirshm/btmixer ) 6 | mac=$( bluetoothctl show | sed -E -n '1 {s/.* (.*) .*/\L\1/; p}' ) 7 | for i in {0..5}; do 8 | grep -q $mac /proc/bus/input/devices && break || sleep 1 9 | done 10 | event=$( sed -E -n "/$mac/,/^H:/ {/^H:/ {s/.* (.*) /\1/; p}}" /proc/bus/input/devices ) # event - with trailing space 11 | 12 | # line='Event: time nnnnnnnnnn.nnnnnn, type 1 (EV_KEY), code NNN (KEY_XXXXXX), value N' 13 | evtest /dev/input/$event | while read line; do 14 | ! grep -q 1$ <<< $line && continue # 1st of multiple 'value N' 15 | 16 | ! grep -Eq 'KEY_.*CD|KEY_.*SONG' <<< $line && continue # PLAYCD PAUSECD STOPCD NEXTSONG PREVIOUSSONG 17 | 18 | key=$( sed -E 's/.*KEY_|\).*//g; s/CD|SONG//; s/.*/\L&/' <<< $line ) 19 | case $key in 20 | play | pause ) 21 | mpcPlayback 22 | ;; 23 | stop ) 24 | mpcPlayback stop 25 | ;; 26 | next | previous ) 27 | . <( mpc status 'current=%songpos%; length=%length% random=%random%' ) 28 | if [[ $key == next ]]; then 29 | (( $current == $length )) && pos=1 || pos=$(( current + 1 )) 30 | else 31 | (( $current == 1 )) && pos=$length || pos=$(( current - 1 )) 32 | fi 33 | $dirbash/cmd.sh mpcskip$'\n'$pos$'\nCMD POS' 34 | ;; 35 | esac 36 | done 37 | -------------------------------------------------------------------------------- /srv/http/bash/cmd-coverart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # album coverart : cover.* - max 2400 x 2400 ( generic 600 x 4 ) 4 | # album thumbnail : coverart.* - 200 x 200 ( imgW 100 x 2 ) 5 | # folder thumbnail : thumb.jpg - 80 x 80 ( imgW 80 x 2 ) 6 | 7 | . /srv/http/bash/common.sh 8 | 9 | args2var "$1" 10 | 11 | if [[ $CMD == reset ]]; then 12 | case $TYPE in 13 | coverart ) 14 | dir=$( dirname "$FILE" ) 15 | rm -f "$dir/cover".* "$dir/coverart".* "$dir/thumb".* $dirshm/{embedded,local,online}/* 16 | pushData coverart '{ "coverart": "'$FILE'", "current": '$CURRENT' }' 17 | ;; 18 | folderthumb ) 19 | rm -f "$DIR/coverart".* "$DIR/thumb".* 20 | pushData coverart '{ "coverart": "'$DIR'/coverart.jpg" }' 21 | ;; 22 | stationart ) 23 | rm "$FILENOEXT".* "$FILENOEXT-thumb".* 24 | pushData coverart '{ "coverart": "'$FILENOEXT'.jpg", "current": '$CURRENT' }' 25 | ;; 26 | esac 27 | exit 28 | # -------------------------------------------------------------------- 29 | fi 30 | 31 | imageSave() { 32 | source=$1 33 | target=$2 34 | size=$3 35 | if [[ ${target: -3} == gif ]]; then 36 | gifsicle -O3 --resize-fit $sizex$size "$source" > "$target" 37 | else 38 | [[ ${source: -3} == gif ]] && source+='[0]' 39 | magick "$source" -thumbnail $sizex$size\> -unsharp 0x.5 "$target" 40 | fi 41 | } 42 | 43 | targetnoext=${TARGET:0:-4} 44 | 45 | [[ ${TARGET:9:13} == '/data/audiocd' ]] && TYPE=audiocd 46 | case $CMD in 47 | bookmark | folder ) 48 | imageSave "$TARGET" "$( dirname "$TARGET" )"/thumb.jpg 80 49 | ;; 50 | coverart ) 51 | dir=$( dirname "$TARGET" ) 52 | imageSave "$TARGET" "$dir"/coverart.${TARGET: -3} 200 53 | imageSave "$TARGET" "$dir"/thumb.jpg 80 54 | ;; 55 | dabradio | webradio ) 56 | imageSave "$TARGET" "$targetnoext"-thumb.jpg 80 57 | ;; 58 | esac 59 | if [[ $CMD == bookmark ]]; then 60 | pushBookmark 61 | else 62 | pushData coverart '{ 63 | "coverart" : "'$( php -r "echo rawurlencode( '${TARGET//\'/\\\'}' );" )'" 64 | , "current" : '$( [[ $CURRENT ]] && echo true || echo false )' 65 | }' 66 | fi 67 | rm -f $dirshm/{embedded,local,online}/* 68 | -------------------------------------------------------------------------------- /srv/http/bash/dab-scan.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /srv/http/bash/common.sh 4 | 5 | basename $0 .sh > $dirshm/script 6 | 7 | echo 8 | dabscan=$( tty2std 'dab-scanner-rtlsdr -C 5A' ) # capture /dev/tty 9 | if ! grep -q ^audioservice <<< $dabscan; then 10 | echo ' 11 | ! No stations found. 12 | ' 13 | exit 14 | # -------------------------------------------------------------------- 15 | fi 16 | 17 | dirdabradio=$dirdata/dabradio 18 | mv $dirdabradio/img $dirshm &> /dev/null 19 | rm -rf $dirdabradio 20 | mkdir -p $dirdabradio/img 21 | mv $dirshm/img $dirdabradio &> /dev/null 22 | 23 | host=$( hostname -f ) 24 | services=$( sed -E -n '/^Ensemble|^audioservice/ {s/ *;/;/g; p}' <<< $dabscan ) 25 | while read service; do 26 | if [[ ${service:0:8} == Ensemble ]]; then 27 | ensemble=$( cut -d' ' -f2- <<< ${service/;*} | sed 's/\s*$//' ) 28 | mkdir "$dirdabradio/$ensemble" 29 | continue 30 | fi 31 | 32 | readarray -d';' -n4 -t field <<< $service 33 | name=${field[1]} 34 | channel=${field[2]} 35 | id=${field[3]} 36 | channel_id=${channel,,}_${id,,} 37 | echo "\ 38 | $name 39 | 48 kHz 160 kbit/s 40 | " > "$dirdabradio/$ensemble/rtsp:||$host:8554|$channel_id" 41 | list+="\ 42 | $channel_id: 43 | runOnDemand: /srv/http/bash/dab-start.sh $id $channel \$RTSP_PORT \$RTSP_PATH 44 | runOnDemandRestart: yes 45 | runOnDemandStartTimeout: 15s 46 | runOnDemandCloseAfter: 3s 47 | " 48 | done <<< $services 49 | 50 | fileyml=/etc/mediamtx/mediamtx.yml 51 | sed -i '1,/^paths:/ !d' $fileyml 52 | echo "$list" >> $fileyml 53 | 54 | chown -R http:http $dirdabradio 55 | dabradio=$( find -L $dirdabradio -type f ! -path '*/img/*' | wc -l ) 56 | sed -i -E 's/("dabradio": ).*/\1'$dabradio',/' $dirmpd/counts 57 | pushData mpdupdate '{ "dabradio": '$dabradio' }' 58 | -------------------------------------------------------------------------------- /srv/http/bash/dab-start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /srv/http/bash/common.sh 4 | 5 | device=$( tty2std 'timeout 0.1 rtl_test -t' ) 6 | if [[ $device == 'No supported devices '* ]]; then 7 | notify dabradio 'DAB Radio' 'No supported devices.' 8 | exit 9 | # -------------------------------------------------------------------- 10 | fi 11 | 12 | systemctl start dab 13 | 14 | killsubs() { 15 | kill $DABPID 16 | kill $FFMPID 17 | rm $MYPIPE $dirshm/webradio/DAB* 18 | } 19 | trap killsubs SIGINT 20 | 21 | MYPIPE=$( mktemp -u ) 22 | mkfifo $MYPIPE 23 | 24 | pidof -q dab-rtlsdr-3 && sleep 4 # if another radio is playing, give time to stop 25 | 26 | dab-rtlsdr-3 \ 27 | -S $1 \ 28 | -C $2 \ 29 | -i $dirshm/webradio \ 30 | > $MYPIPE & 31 | DABPID=$! 32 | 33 | ffmpeg \ 34 | -re \ 35 | -stream_loop -1 \ 36 | -ac 2 \ 37 | -ar 48000 \ 38 | -f s16le \ 39 | -i $MYPIPE \ 40 | -vn \ 41 | -b:a 160k \ 42 | -c:a aac \ 43 | -metadata title="DAB Radio" \ 44 | -f rtsp rtsp://localhost:$3/$4 \ 45 | &> /dev/null & 46 | FFMPID=$! 47 | for pid in $( pgrep $FFMPID ); do 48 | ionice -c 0 -n 0 -p $pid &> /dev/null 49 | renice -n -19 -p $pid &> /dev/null 50 | done 51 | 52 | wait $FFMPID 53 | -------------------------------------------------------------------------------- /srv/http/bash/lcdchar.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import sys 4 | import json 5 | 6 | with open( '/srv/http/data/system/lcdchar.json' ) as f: CONF = json.load( f ) 7 | locals().update( CONF ) # INF, COLS, CHARMAP, BACKLIGHT, [ ADDRESS, CHIP | P* ... ] 8 | rows = COLS < 20 and 2 or 4 9 | cmA00 = CHARMAP == 'A00' 10 | 11 | if INF == 'i2c': 12 | from RPLCD.i2c import CharLCD 13 | lcd = CharLCD( cols=COLS, rows=rows, charmap=CHARMAP 14 | , address=ADDRESS, i2c_expander=CHIP ) 15 | else: 16 | from RPLCD.gpio import CharLCD 17 | from RPi import GPIO 18 | GPIO.setwarnings( False ) 19 | lcd = CharLCD( cols=COLS, rows=rows, charmap=CHARMAP 20 | , numbering_mode=GPIO.BOARD, pin_rs=PIN_RS, pin_rw=PIN_RW, pin_e=PIN_E, pins_data=[ P0, P1, P2, P3 ] ) 21 | 22 | pause = ( 23 | 0b00000, 24 | 0b11011, 25 | 0b11011, 26 | 0b11011, 27 | 0b11011, 28 | 0b11011, 29 | 0b00000, 30 | 0b00000, 31 | ) 32 | play = ( 33 | 0b10000, 34 | 0b11000, 35 | 0b11100, 36 | 0b11110, 37 | 0b11100, 38 | 0b11000, 39 | 0b10000, 40 | 0b00000, 41 | ) 42 | stop = ( 43 | 0b00000, 44 | 0b11111, 45 | 0b11111, 46 | 0b11111, 47 | 0b11111, 48 | 0b11111, 49 | 0b00000, 50 | 0b00000, 51 | ) 52 | logol = ( 53 | 0b11111, 54 | 0b11011, 55 | 0b11011, 56 | 0b00000, 57 | 0b11011, 58 | 0b11011, 59 | 0b11111, 60 | 0b11111, 61 | ) 62 | logor = ( 63 | 0b01110, 64 | 0b10110, 65 | 0b10110, 66 | 0b01110, 67 | 0b01110, 68 | 0b10110, 69 | 0b11010, 70 | 0b11100, 71 | ) 72 | dot = ( 73 | 0b00000, 74 | 0b00000, 75 | 0b00000, 76 | 0b00011, 77 | 0b00011, 78 | 0b00000, 79 | 0b00000, 80 | 0b00000, 81 | ) 82 | char = [ pause, play, stop, logol, logor, dot ] 83 | for i in range( 6 ): 84 | lcd.create_char( i, char[ i ] ) 85 | 86 | ICON = { 87 | 'pause' : '\x00 ' 88 | , 'play' : '\x01 ' 89 | , 'stop' : '\x02 ' 90 | } 91 | RA = '\x03\x04' 92 | DOTS = '\x05 \x05 \x05' 93 | RN = '\r\n' 94 | 95 | SPACES = ' ' * ( ( COLS - 6 ) // 2 + 1 ) 96 | LOGO = rows > 2 and RN or '' 97 | LOGO += SPACES + RA + RN + SPACES +'rAudio' 98 | 99 | argvL = len( sys.argv ) 100 | if argvL == 2: # 1 argument 101 | val = sys.argv[ 1 ] 102 | if val == 'off': # backlight off 103 | lcd.backlight_enabled = False 104 | elif val == 'logo': 105 | lcd.write_string( LOGO ) 106 | elif val == 'clear': 107 | lcd.clear() 108 | else: # string 109 | lcd.write_string( val.replace( '\n', RN ) ) 110 | lcd.close() 111 | sys.exit() 112 | # -------------------------------------------------------------------- 113 | import math 114 | import time 115 | 116 | if cmA00: 117 | import unicodedata 118 | def normalize( str ): 119 | return ''.join( c for c in unicodedata.normalize( 'NFD', str ) 120 | if unicodedata.category( c ) != 'Mn' ) 121 | 122 | def backlightOff(): 123 | time.sleep( 60 ) 124 | lcd.backlight_enabled = False 125 | lcd.close() 126 | sys.exit() 127 | # -------------------------------------------------------------------- 128 | def second2hhmmss( sec ): 129 | hh = math.floor( sec / 3600 ) 130 | mm = math.floor( ( sec % 3600 ) / 60 ) 131 | ss = sec % 60 132 | HH = hh > 0 and str( hh ) +':' or '' 133 | mmt = str( mm ) 134 | MM = hh > 0 and ( mm > 9 and mmt +':' or '0'+ mmt +':' ) or ( mm > 0 and mmt +':' or '' ) 135 | sst = str( ss ) 136 | SS = mm > 0 and ( ss > 9 and sst or '0'+ sst ) or sst 137 | return HH + MM + SS 138 | 139 | with open( '/srv/http/data/shm/status.json' ) as f: STATUS = json.load( f ) 140 | for k in [ 'Album', 'Artist', 'file', 'station', 'Title' ]: 141 | if k in STATUS: 142 | v = STATUS[ k ] or DOTS 143 | if cmA00: STATUS[ k ] = normalize( v ) # character: accent with sequence code > single code 144 | STATUS[ k ] = v[ :COLS ] # set width 145 | else: 146 | STATUS[ k ] = '' 147 | locals().update( STATUS ) 148 | 149 | if webradio: 150 | if state != 'play': 151 | Artist = station 152 | Album = file 153 | else: 154 | if not Artist and not Title: Artist = station 155 | if not Album: Album = station or file 156 | 157 | if not Artist: Artist = DOTS 158 | if not Title: Title = DOTS 159 | if not Album: Album = DOTS 160 | if rows == 2: 161 | if state == 'play': lines = Title 162 | else: 163 | lines = Artist + RN + Title + RN + Album 164 | 165 | hhmmss = Time and second2hhmmss( round( float( Time ) ) ) or '' 166 | 167 | if state == 'stop': 168 | progress = ( hhmmss + ' ' * COLS )[ :COLS - 4 ] 169 | else: 170 | if elapsed is False: # can be 0 171 | elapsedhhmmss = '' 172 | slash = '' 173 | else: 174 | elapsed = int( elapsed ) 175 | elapsedhhmmss = second2hhmmss( elapsed ) 176 | slash = COLS > 16 and ' / ' or '/' 177 | if Time: hhmmss = slash + hhmmss 178 | progress = ( elapsedhhmmss + hhmmss + ' ' * COLS )[ :COLS - 4 ] 179 | 180 | lcd.write_string( lines + RN + ICON[ state ] + progress + RA ) 181 | 182 | if BACKLIGHT and state != 'play': backlightOff() 183 | 184 | if state != 'play' or elapsed is False: sys.exit() 185 | # -------------------------------------------------------------------- 186 | row = rows - 1 187 | starttime = time.time() 188 | elapsed += math.ceil( ( starttime * 1000 - timestamp ) / 1000000 ) 189 | PLAY = ICON[ 'play' ] 190 | 191 | while True: 192 | sl = 1 - ( ( time.time() - starttime ) % 1 ) 193 | lcd.cursor_pos = ( row, 0 ) 194 | elapsedhhmmss = second2hhmmss( elapsed ) 195 | lcd.write_string( PLAY + elapsedhhmmss + hhmmss ) 196 | elapsed += 1 197 | time.sleep( sl ) 198 | -------------------------------------------------------------------------------- /srv/http/bash/motd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # symlink: /etc/profile.d/motd.sh 4 | 5 | clear 6 | printf "\e[30m\e[46m%*s\n" $COLUMNS 7 | printf "%-${COLUMNS}s\n" " r A u d i o" 8 | printf "%*s\e[0m\n\n" $COLUMNS 9 | export PATH=/srv/http/bash:/srv/http/bash/settings:$PATH 10 | . /srv/http/bash/common.sh 11 | -------------------------------------------------------------------------------- /srv/http/bash/mpdidle.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /srv/http/bash/common.sh 4 | 5 | mpc idleloop | while read changed; do 6 | case $changed in 7 | mixer ) # for upmpdcli 8 | playerActive upnp && volumeGet push 9 | ;; 10 | playlist ) 11 | [[ ! -e $dirshm/pushplaylist && $( mpc status %consume% ) == on ]] && $dirbash/cmd.sh playlistpush 12 | ;; 13 | player ) 14 | if [[ ! -e $dirshm/radio && ! -e $dirshm/skip && ! -e $dirshm/cdstart ]]; then 15 | $dirbash/status-push.sh & # need to run in background for snapcast ssh 16 | fi 17 | ;; 18 | update ) 19 | if [[ ! -e $dirshm/listing ]]; then 20 | ! mpc | grep -q -m1 '^Updating' && $dirbash/cmd-list.sh 21 | fi 22 | ;; 23 | esac 24 | done 25 | -------------------------------------------------------------------------------- /srv/http/bash/power.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /srv/http/bash/common.sh 4 | 5 | $dirbash/cmd.sh playerstop 6 | logoLcdOled 7 | [[ -e $dirshm/relayson ]] && $dirbash/relays.sh off 8 | if [[ $1 == reboot ]]; then 9 | reboot=1 10 | audioCDplClear && $dirbash/status-push.sh 11 | startup=$( systemd-analyze | sed -n '/^Startup/ {s/.*= //; s/[^0-9]//g; p}' ) 12 | pushData power '{ "type": "reboot", "startup": '$startup' }' 13 | else 14 | audioCDplClear 15 | pushData power '{ "type": "off" }' 16 | fi 17 | ipserver=$( ipAddress ) 18 | if systemctl -q is-active nfs-server; then # server rAudio 19 | ipclients=$( grep -v $ipserver $filesharedip ) 20 | if [[ $ipclients ]]; then 21 | [[ ! $2 ]] && echo -1 && exit # $2 confirm proceed 22 | # -------------------------------------------------------------------- 23 | [[ $reboot ]] && msg='Reboot ...' || msg='Power off ...' 24 | for ip in $ipclients; do 25 | notify -ip $ip 'networks blink' 'Server rAudio' "$msg" 26 | done 27 | fi 28 | fi 29 | [[ -e $filesharedip ]] && sed -i "/$ipserver/ d" $filesharedip 30 | [[ -e $dirshm/btreceiver ]] && cp $dirshm/btreceiver $dirsystem 31 | touch $dirshm/power 32 | 33 | snapclientIP playerstop 34 | cdda=$( mpc -f %file%^%position% playlist | grep ^cdda: | cut -d^ -f2 ) 35 | [[ $cdda ]] && mpc -q del $cdda 36 | ply-image /srv/http/assets/img/splash.png &> /dev/null 37 | if mount | grep -q -m1 $dirnas; then 38 | umount -l $dirnas/* &> /dev/null 39 | sleep 3 40 | fi 41 | if [[ -d /sys/class/backlight/rpi_backlight ]]; then 42 | echo 1 > /sys/class/backlight/rpi_backlight/bl_power 43 | elif [[ -e $dirsystem/localbrowser ]]; then 44 | DISPLAY=:0 xset dpms force off 45 | fi 46 | [[ -e /boot/shutdown.sh ]] && /boot/shutdown.sh 47 | [[ $reboot ]] && reboot || poweroff 48 | -------------------------------------------------------------------------------- /srv/http/bash/powerbutton.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if grep -q 'poweroff,gpiopin=22' /boot/config.txt; then # audiophonic 4 | gpioset -t0 -c0 4=0 22=1 5 | gpiomon -q -b pull-down -e rising -c0 -n1 17 6 | gpioset -t0 -c0 4=1 7 | sleep 1 8 | gpioset -t0 -c0 4=0 9 | else 10 | . /srv/http/data/system/powerbutton.conf 11 | gpioset -t0 -c0 $led=1 # -t time -c chip pin=[1/0] 12 | gpiomon -q -b pull-up -e falling -c0 -n1 $sw # -q(quiet) -b bias -e edge -c chip -n nEvent pin 13 | fi 14 | 15 | /srv/http/bash/power.sh 16 | -------------------------------------------------------------------------------- /srv/http/bash/relays-timer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /srv/http/bash/common.sh 4 | 5 | killProcess relaystimer 6 | echo $$ > $dirshm/pidrelaystimer 7 | 8 | timer=$( getVar timer $dirsystem/relays.conf ) 9 | i=$timer 10 | while sleep 60; do 11 | grep -q -m1 ^state.*play $dirshm/status && i=$timer && continue 12 | 13 | (( i-- )) 14 | case $i in 15 | 1 ) pushData relays '{ "countdown": true }';; 16 | 0 ) $dirbash/relays.sh off && break;; 17 | esac 18 | done 19 | -------------------------------------------------------------------------------- /srv/http/bash/relays.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /srv/http/bash/common.sh 4 | . $dirsystem/relays.conf 5 | 6 | if [[ ! $1 ]]; then 7 | relayson=1 8 | pins=$on 9 | onoff=1 10 | delay=( $ond ) 11 | color=wh 12 | else 13 | killProcess relaystimer 14 | pins=$off 15 | onoff=0 16 | delay=( $offd ) 17 | color=gr 18 | fi 19 | . <( sed -E -e '/^\{$|^\}$/d; s/^ "//; s/,$//; s/": /=/; s/^/p/' $dirsystem/relays.json ) # faster than jq 20 | for pin in $pins; do 21 | ppin=p$pin 22 | order+=${!ppin}$'\n' 23 | done 24 | for pin in $pins; do 25 | gpioset -t0 -c0 $pin=$onoff 26 | line=$(( i + 1 )) 27 | message=$( sed "$line s|$||" <<< "<$color>$order" ) 28 | message=$( sed -z 's/\n/
    /g; s/
    $//' <<< $message ) 29 | message=$( quoteEscape $message ) 30 | [[ ! $relayson ]] && message="$message" 31 | notify 'relays blink' '' "$message" 32 | [[ ${delay[i]} ]] && sleep ${delay[i]} 33 | (( i++ )) 34 | done 35 | if [[ $relayson ]]; then 36 | done=true 37 | touch $dirshm/relayson 38 | [[ $timeron ]] && $dirbash/relays-timer.sh &> /dev/null & 39 | else 40 | done=false 41 | killProcess relaystimer 42 | rm -f $dirshm/relayson 43 | fi 44 | sleep 1 45 | pushData relays '{ "done": '$done' }' 46 | -------------------------------------------------------------------------------- /srv/http/bash/rotaryencoder.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /srv/http/bash/common.sh 4 | 5 | . $dirsystem/rotaryencoder.conf 6 | 7 | # play/pause 8 | dtoverlay gpio-key gpio=$pins label=PLAYCD keycode=200 9 | sleep 1 10 | devinputbutton=$( realpath /dev/input/by-path/*button* ) 11 | evtest $devinputbutton | while read line; do 12 | [[ $line =~ .*EV_KEY.*KEY_PLAYCD.*1 ]] && mpcPlayback 13 | done & 14 | 15 | dn=1%- 16 | up=1%+ 17 | fn_volume=$( < $dirshm/volumefunction ) 18 | if [[ -e $dirshm/btreceiver ]]; then 19 | mixer=$( < $dirshm/btmixer ) 20 | elif [[ -e $dirshm/amixercontrol ]]; then 21 | . $dirshm/output 22 | else # volumeMpd 23 | dn=-1 24 | up=+1 25 | fi 26 | dtoverlay rotary-encoder pin_a=$pina pin_b=$pinb relative_axis=1 steps-per-period=$step 27 | sleep 1 28 | devinputrotary=$( realpath /dev/input/by-path/*rotary* ) 29 | evtest $devinputrotary | while read line; do 30 | case ${line: -2} in 31 | ' 1' ) updn=$up;; 32 | '-1' ) updn=$dn;; 33 | * ) continue;; 34 | esac 35 | $fn_volume $updn "$mixer" $card 36 | volumeGet push 37 | done 38 | -------------------------------------------------------------------------------- /srv/http/bash/scrobble.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # track end - status-push.sh 4 | # stop / prevnext - cmd.sh mpcplayback / mpcskip 5 | # webradio - cmd.sh scrobble 6 | 7 | . /srv/http/bash/common.sh 8 | 9 | args2var "$1" 10 | 11 | . $dirsystem/scrobblekey # sharedsecret 12 | timestamp=$( date +%s ) 13 | apisig=$( echo -n "api_key${apikey}artist${ARTIST}methodtrack.scrobblesk${sk}timestamp${timestamp}track${TITLE}${sharedsecret}" \ 14 | | md5sum \ 15 | | cut -c1-32 ) 16 | response=$( curl -sfX POST \ 17 | --data "api_key=$apikey" \ 18 | --data-urlencode "artist=$ARTIST" \ 19 | --data "method=track.scrobble" \ 20 | --data "sk=$sk" \ 21 | --data "timestamp=$timestamp" \ 22 | --data-urlencode "track=$TITLE" \ 23 | --data "api_sig=$apisig" \ 24 | --data "format=json" \ 25 | http://ws.audioscrobbler.com/2.0 ) 26 | if [[ $? == 0 ]]; then 27 | [[ $response =~ error ]] && msg="Error: $( jq -r .message <<< $response )" || msg=$( quoteEscape $TITLE ) 28 | else 29 | msg='Server not reachable.' 30 | fi 31 | [[ $msg ]] && notify lastfm Scrobble "$msg" 32 | -------------------------------------------------------------------------------- /srv/http/bash/settings/addons-data.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /srv/http/bash/common.sh 4 | 5 | evalData() { 6 | local cmd 7 | cmd=$( grep '"'$1'"' <<< $addondata | cut -d'"' -f4 ) 8 | if [[ $1 == version ]]; then 9 | [[ $( < $diraddons/$addon ) < $cmd ]] && return 0 10 | else 11 | [[ $cmd && $( eval $cmd ) ]] && return 0 12 | fi 13 | } 14 | 15 | ######## 16 | data=$( curl -sfL https://github.com/rern/rAudio-addons/raw/main/addonslist.json ) 17 | if [[ $? == 0 ]]; then 18 | online=true 19 | echo "$data" > $diraddons/addonslist.json 20 | else 21 | online=false 22 | notify 'warning yl blink' Addons 'Server not reachable.' 23 | fi 24 | ######## 25 | [[ ! $data ]] && data=$( < $diraddons/addonslist.json ) 26 | addons=$( grep ': {$' <<< $data \ 27 | | tr -d '\t, ":{' \ 28 | | grep -Ev 'option|push' ) 29 | for addon in $addons; do 30 | addondata=$( sed -n "/$addon/,/}/ p" <<< $data ) 31 | evalData hide && hidden+=',"'$addon'"' 32 | evalData verify && notverified+=',"'$addon'"' 33 | if [[ -e $diraddons/$addon ]]; then 34 | installed+=',"'$addon'"' 35 | evalData version && update+=',"'$addon'"' 36 | fi 37 | done 38 | if [[ $update ]]; then 39 | pushData option '{ "addons": true }' 40 | touch $diraddons/update 41 | else 42 | rm -f $diraddons/update 43 | fi 44 | data=$( head -n -1 <<< $data ) 45 | data+=' 46 | , "status" : { 47 | "hidden" : [ '${hidden:1}' ] 48 | , "installed" : [ '${installed:1}' ] 49 | , "notverified" : [ '${notverified:1}' ] 50 | , "update" : [ '${update:1}' ] 51 | , "online" : '$online' 52 | } 53 | }' 54 | echo "$data" 55 | -------------------------------------------------------------------------------- /srv/http/bash/settings/addons.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /srv/http/bash/common.sh 4 | 5 | if [[ $1 == kill ]]; then 6 | file=$dirshm/script 7 | [[ -e $file ]] && pkill $( < $file ) && rm $file 8 | exit 9 | # -------------------------------------------------------------------- 10 | fi 11 | 12 | addonsjson=$diraddons/addonslist.json 13 | bar=' ' 14 | info=' i ' 15 | warn=' ! ' 16 | 17 | title() { 18 | echo "\ 19 |
    20 | $1 21 |
    " 22 | } 23 | getinstallzip() { 24 | echo "$bar Get files ..." 25 | installfile=$branch.tar.gz 26 | fileurl=$( jq -r .$alias.installurl $addonsjson | sed "s|raw/main/install.sh|archive/$installfile|" ) 27 | curl -sfLO $fileurl 28 | [[ $? != 0 ]] && echo -e "$warn Get files failed." && exit 29 | # -------------------------------------------------------------------- 30 | echo 31 | echo "$bar Install new files ..." 32 | filelist=$( bsdtar tf $installfile \ 33 | | grep /srv/ \ 34 | | sed -e '/\/$/ d' -e 's|^.*/srv/|/srv/|' ) # stdout as a block to avoid blank lines 35 | echo "$filelist" 36 | uninstallfile=$( grep uninstall_.*sh <<< $filelist ) 37 | if [[ $uninstallfile ]]; then 38 | bsdtar xf $installfile --strip 1 -C /usr/local/bin $uninstallfile 39 | chmod 755 /usr/local/bin/$uninstallfile 40 | fi 41 | tmpdir=/tmp/install 42 | rm -rf $tmpdir 43 | mkdir -p $tmpdir 44 | bsdtar xf $installfile --strip 1 -C $tmpdir 45 | rm $installfile $tmpdir/{.*,*} &> /dev/null 46 | cp -r $tmpdir/* / 47 | rm -rf $tmpdir 48 | } 49 | installstart() { # $1-'u'=update 50 | rm $0 51 | readarray -t args <<< $1 # lines to array: alias label branch opt1 opt2 ... 52 | alias=${args[0]} 53 | label=${args[1]} 54 | branch=${args[2]} 55 | args=( "${args[@]:3}" ) # 'opt' for script start at ${args[0]} 56 | title="$( jq -r .$alias.title $addonsjson )" 57 | [[ $label != Rank || $label != Import ]] && title "$bar $label $title ..." || title "$bar $title ..." 58 | } 59 | installfinish() { 60 | version=$( jq -r .$alias.version $addonsjson ) 61 | [[ $version != null ]] && echo $version > $diraddons/$alias 62 | echo " 63 | $bar Done. 64 |
    65 | " 66 | [[ $alias == r1 ]] && rm -f $dirshm/system 67 | [[ -e $dirmpd/updating ]] && $dirbash/cmd.sh mpcupdate 68 | } 69 | uninstallstart() { 70 | title="$( jq -r .$alias.title $addonsjson )" 71 | if [[ ! -e /usr/local/bin/uninstall_$alias.sh ]]; then 72 | echo $info $title not found. 73 | rm $diraddons/$alias &> /dev/null 74 | exit 1 75 | # -------------------------------------------------------------------- 76 | fi 77 | rm $0 78 | [[ $label != Update ]] && title "$bar Uninstall $title ..." 79 | } 80 | uninstallfinish() { 81 | rm $diraddons/$alias &> /dev/null 82 | [[ $label != Update ]] && title "$bar Done." 83 | } 84 | -------------------------------------------------------------------------------- /srv/http/bash/settings/camilla-bluetooth.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /srv/http/bash/common.sh 4 | 5 | type=$1 6 | mac=$( < $dirshm/$type ) 7 | filedefault=/etc/default/camilladsp 8 | getVar CONFIG $filedefault > $dircamilladsp/fileconfig 9 | filecurrent=$( getVar CONFIG $filedefault ) 10 | filemac=$dircamilladsp/$mac 11 | if [[ -e $filemac ]]; then 12 | filedevice=$( < $filemac ) 13 | else 14 | filedevice=$dircamilladsp/configs-bt/camilladsp.yml 15 | echo $filedevice > $filemac 16 | fi 17 | sed -i "s|^CONFIG.*|CONFIG=$filedevice|" $filedefault 18 | if [[ -e $filedevice ]]; then 19 | camillaDSPstart 20 | exit 21 | # -------------------------------------------------------------------- 22 | fi 23 | . <( bluealsa-aplay -L | awk '/channel.*Hz/ {print "format="$3"\nchannels="$4"\nsamplerate="$6}' ) 24 | format=$( sed 's/_3LE/LE3/; s/FLOAT_LE/FLOAT32LE/; s/_//g' <<< $format ) 25 | 26 | if [[ $type == btreceiver ]]; then 27 | sed -E -e '/playback:$/,/format:/ { 28 | s/(device: ).*/\1bluealsa/ 29 | s/(channels: ).*/\1'$channels'/ 30 | s/(format: ).*/\1'$format'/ 31 | }' "$filecurrent" > "$filedevice" 32 | else # btsender 33 | dbuspath=$( gdbus introspect \ 34 | --recurse \ 35 | --system \ 36 | --dest org.bluealsa \ 37 | --object-path /org/bluealsa \ 38 | | awk '/node.*source {$/ {print $2}' ) # /org/bluealsa/hci0/dev_A0_B1_C2_D3_E4_F5/a2dpsnk/source 39 | sed -E -e 's/(samplerate: ).*/\1'$samplerate'/ 40 | s/(chunksize: ).*/\14096/ 41 | s/(enable_rate_adjust: )/\1true/ 42 | s/(target_level: )/\18000/ 43 | s/(adjust_period: )/\13/ 44 | s/(enable_resampling: )/\1true/ 45 | ' -e '/capture:$/,/format:/ { 46 | /device: .*/ d 47 | /format:/ a\ dbus_path: '$dbuspath' 48 | s/(type: ).*/\1Bluez/ 49 | s/(channels: ).*/\1'$channels'/ 50 | s/(format: ).*/\1'$format'/ 51 | }' "$filecurrent" > "$filedevice" 52 | fi 53 | 54 | camillaDSPstart 55 | -------------------------------------------------------------------------------- /srv/http/bash/settings/camilla-data.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ! systemctl -q is-active camilladsp && echo notrunning && exit 4 | 5 | . /srv/http/bash/common.sh 6 | . $dirshm/output 7 | if grep -q configs-bt /etc/default/camilladsp; then 8 | bluetooth=true 9 | name=$( < $dirshm/btname ) 10 | fi 11 | data=' 12 | , "bluetooth" : '$bluetooth' 13 | , "btreceiver" : '$( exists $dirshm/btreceiver )' 14 | , "card" : '$card' 15 | , "cardname" : "'$name'" 16 | , "configname" : "'$( sed -n '/^CONFIG/ {s|.*/||; p}' /etc/default/camilladsp )'" 17 | , "control" : "'$mixer'" 18 | , "devices" : '$( < $dirshm/hwparams )' 19 | , "player" : "'$( < $dirshm/player )'" 20 | , "pllength" : '$( mpc status %length% )' 21 | , "state" : "'$( getVar state $dirshm/status )'" 22 | , "volume" : '$( [[ $mixer ]] && volumeGet )' 23 | , "volumemax" : '$( volumeMaxGet )' 24 | , "volumemute" : '$( getContent $dirsystem/volumemute 0 ) 25 | dirs=$( ls $dircamilladsp ) 26 | for d in $dirs; do 27 | [[ $bluetooth && $d == configs ]] && dir=configs-bt || dir=$d 28 | if [[ $dir == coeffs ]]; then 29 | dirs=$( ls $dircamilladsp/$dir | grep -v '\.wav$' ) 30 | ls+=', "'$d'": '$( line2array "$dirs" ) 31 | dirs=$( ls $dircamilladsp/$dir | grep '\.wav$' ) 32 | ls+=', "coeffswav": '$( line2array "$dirs" ) 33 | else 34 | dirs=$( ls $dircamilladsp/$dir ) 35 | dirs=$( line2array "$dirs" ) 36 | ls+=', "'$d'": '$dirs 37 | [[ $d == configs ]] && list=$dirs 38 | fi 39 | done 40 | ######## 41 | data+=' 42 | , "list" : { "camilla": '$list' } 43 | , "ls" : { '${ls:1}' }' 44 | 45 | data2json "$data" $1 -------------------------------------------------------------------------------- /srv/http/bash/settings/camilla-devices.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ### included by <<< player-conf.sh 4 | if [[ ! $dirbash ]]; then # if run directly 5 | . /srv/http/bash/common.sh 6 | . $dirshm/output 7 | CARD=$card 8 | NAME=$name 9 | fi 10 | 11 | $dirbash/cmd.sh playerstop # must stop for aplay --dump-hw-params 12 | systemctl stop camilladsp 13 | if grep -q -m1 configs-bt /etc/default/camilladsp; then 14 | DEVICES=( '{ "Bluez": "bluez" }' '{ "blueALSA": "bluealsa" }' ) 15 | else 16 | DEVICES=( '{ "Loopback": "hw:Loopback,0" }' "$( < $dirshm/devices )" ) 17 | fi 18 | for c in Loopback $CARD; do 19 | lines=$( tty2std "timeout 0.1 aplay -D hw:$c /dev/zero --dump-hw-params" ) 20 | CHANNELS+=( $( awk '/^CHANNELS/ {print $NF}' <<< $lines | tr -d ']\r' ) ) 21 | formats=$( sed -n '/^FORMAT/ {s/_3LE/LE3/; s/FLOAT_LE/FLOAT32LE/; s/^.*: *\|[_\r]//g; s/ /\n/g; p}' <<< $lines ) 22 | listformat= 23 | for f in FLOAT64LE FLOAT32LE S32LE S24LE3 S24LE S16LE; do 24 | grep -q $f <<< $formats && listformat+=', "'$f'"' 25 | done 26 | FORMATS+=( "[ ${listformat:1} ]" ) 27 | if [[ $c != Loopback ]]; then 28 | ratemax=$( sed -n -E '/^RATE/ {s/.* (.*)].*/\1/; p}' <<< $lines ) 29 | for r in 44100 48000 88200 96000 176400 192000 352800 384000 705600 768000; do 30 | (( $r > $ratemax )) && break || SAMPLINGS+=', "'$( sed 's/...$/,&/' <<< $r )'": '$r 31 | done 32 | fi 33 | done 34 | ######## > 35 | data=' 36 | "capture" : { 37 | "device" : '${DEVICES[0]}' 38 | , "channels" : '${CHANNELS[0]}' 39 | , "formats" : '${FORMATS[0]}' 40 | } 41 | , "playback" : { 42 | "device" : '${DEVICES[1]}' 43 | , "channels" : '${CHANNELS[1]}' 44 | , "formats" : '${FORMATS[1]}' 45 | , "samplings" : { '${SAMPLINGS:1}' } 46 | }' 47 | echo "{ $data }" | jq > $dirshm/hwparams 48 | ######## < 49 | if [[ -e $dirshm/btreceiver ]]; then 50 | $dirsettings/camilla-bluetooth.sh btreceiver 51 | else 52 | fileformat="$dirsystem/camilla-$NAME" 53 | [[ -s $fileformat ]] && format=$( getContent "$fileformat" ) || format=$( jq -r .[0] <<< ${FORMATS[1]} ) 54 | fileconf=$( getVar CONFIG /etc/default/camilladsp ) 55 | format0=$( getVar playback.format "$fileconf" ) 56 | card0=$( getVar playback.device "$fileconf" | cut -c4 ) 57 | [[ $format0 != $format ]] && changeformat=1 58 | [[ $card0 != $CARD ]] && changecard=1 59 | if [[ $changeformat || $changecard ]]; then 60 | config=$( < "$fileconf" ) 61 | if [[ $changeformat ]]; then 62 | config=$( sed -E '/playback:/,/format:/ s/^(\s*format: ).*/\1'$format'/' <<< $config ) 63 | echo $format > "$fileformat" 64 | fi 65 | [[ $changecard ]] && config=$( sed '/playback:/,/device:/ s/hw:./hw:'$CARD'/' <<< $config ) 66 | echo "$config" > "$fileconf" 67 | fi 68 | camillaDSPstart 69 | fi 70 | -------------------------------------------------------------------------------- /srv/http/bash/settings/camilla.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /srv/http/bash/common.sh 4 | 5 | dircoeffs=$dircamilladsp/coeffs 6 | dirconfigs=$dircamilladsp/configs 7 | 8 | saveConfig() { 9 | configfile=$( getVar CONFIG /etc/default/camilladsp ) 10 | config=$( echo '"GetConfig"' | websocat ws://127.0.0.1:1234 ) 11 | echo -e "$config " | sed '1 s/.*/---/; $d; s/\\"/"/g' > "$configfile" 12 | } 13 | 14 | args2var "$1" 15 | 16 | case $CMD in 17 | 18 | clippedreset ) 19 | echo $CLIPPED > $dirshm/clipped 20 | pushRefresh 21 | ;; 22 | coefdelete ) 23 | rm -f $dircoeffs/"$NAME" 24 | pushRefresh 25 | ;; 26 | coefrename ) 27 | mv -f $dircoeffs/{"$NAME","$NEWNAME"} 28 | pushRefresh 29 | ;; 30 | confcopy ) 31 | [[ $BT == true ]] && dirconfig+=-bt 32 | cp -f $dirconfigs/{"$NAME","$NEWNAME"} 33 | pushRefresh 34 | ;; 35 | confdelete ) 36 | [[ $BT == true ]] && dirconfig+=-bt 37 | rm -f $dirconfigs/"$NAME" 38 | pushRefresh 39 | ;; 40 | confrename ) 41 | [[ $BT == true ]] && dirconfig+=-bt 42 | mv -f $dirconfigs/{"$NAME","$NEWNAME"} 43 | pushRefresh 44 | ;; 45 | confswitch ) 46 | saveConfig 47 | sed -i -E "s|^(CONFIG=).*|\1$CONFIG|" /etc/default/camilladsp 48 | ;; 49 | restart ) 50 | systemctl restart camilladsp 51 | ;; 52 | saveconfig ) 53 | saveConfig 54 | ;; 55 | volume ) 56 | volume 57 | ;; 58 | 59 | esac 60 | 61 | -------------------------------------------------------------------------------- /srv/http/bash/settings/camilla_cooley_tukey.py: -------------------------------------------------------------------------------- 1 | # Adapted from https://jeremykun.com/2012/07/18/the-fast-fourier-transform/ 2 | import cmath 3 | import math 4 | 5 | try: 6 | import numpy.fft as npfft 7 | except ImportError: 8 | npfft = None 9 | 10 | 11 | def omega(p, q): 12 | return cmath.exp((2.0 * cmath.pi * 1j * q) / p) 13 | 14 | 15 | OMEGA41 = omega(4, -1) 16 | OMEGA81 = omega(8, -1) 17 | OMEGA82 = omega(8, -2) 18 | OMEGA83 = omega(8, -3) 19 | 20 | TWIDDLES = {} 21 | 22 | 23 | def get_twiddles(n): 24 | if n in TWIDDLES: 25 | return TWIDDLES[n] 26 | else: 27 | tw = [omega(n, -m) for m in range(int(n / 2))] 28 | TWIDDLES[n] = tw 29 | return tw 30 | 31 | 32 | def _fft4(signal): 33 | Fe0 = signal[0] + signal[2] 34 | Fe1 = signal[0] - signal[2] 35 | Fo0 = signal[1] + signal[3] 36 | Fo1 = signal[1] - signal[3] 37 | 38 | F11 = OMEGA41 * Fo1 39 | return ( 40 | Fe0 + Fo0, 41 | Fe1 + F11, 42 | Fe0 - Fo0, 43 | Fe1 - F11, 44 | ) 45 | 46 | 47 | def _fft8(signal): 48 | Feven = _fft4(signal[0::2]) 49 | Fodd = _fft4(signal[1::2]) 50 | t0 = Fodd[0] 51 | t1 = Fodd[1] * OMEGA81 52 | t2 = Fodd[2] * OMEGA82 53 | t3 = Fodd[3] * OMEGA83 54 | return ( 55 | Feven[0] + t0, 56 | Feven[1] + t1, 57 | Feven[2] + t2, 58 | Feven[3] + t3, 59 | Feven[0] - t0, 60 | Feven[1] - t1, 61 | Feven[2] - t2, 62 | Feven[3] - t3, 63 | ) 64 | 65 | 66 | def _fft(signal): 67 | n = len(signal) 68 | if n == 8: 69 | return _fft8(signal) 70 | elif n == 4: 71 | return _fft4(signal) 72 | elif n == 1: 73 | return signal 74 | else: 75 | Feven = _fft(signal[0::2]) 76 | Fodd = _fft(signal[1::2]) 77 | tw = get_twiddles(n) 78 | twiddled = [t * fo for fo, t in zip(Fodd, tw)] 79 | combined = [fe + ft for fe, ft in zip(Feven, twiddled)] + [ 80 | fe - ft for fe, ft in zip(Feven, twiddled) 81 | ] 82 | return combined 83 | 84 | 85 | def pyfft(signal): 86 | orig_len = len(signal) 87 | fft_len = 2 ** (math.ceil(math.log2(orig_len))) 88 | padding = [0.0 for _n in range(fft_len - orig_len)] 89 | signal.extend(padding) 90 | fftsig = _fft(signal) 91 | return fftsig 92 | 93 | 94 | def fft(signal): 95 | orig_len = len(signal) 96 | fft_len = 2 ** (math.ceil(math.log2(orig_len))) 97 | padding = [0.0 for _n in range(fft_len - orig_len)] 98 | signal.extend(padding) 99 | if npfft is not None: 100 | fftsig = npfft.fft(signal) 101 | else: 102 | fftsig = _fft(signal) 103 | return fftsig 104 | 105 | 106 | if __name__ == "__main__": 107 | # testing area, compare to numpy fft 108 | import numpy as np 109 | import numpy.fft as npfft 110 | import time 111 | 112 | data_test = [n for n in range(16)] 113 | pyf = pyfft(data_test) 114 | npf = npfft.fft(data_test) 115 | 116 | for n in range(len(pyf)): 117 | print(abs(pyf[n] - npf[n])) 118 | 119 | data = [float(n) for n in range(2**16)] 120 | 121 | start = time.time() 122 | nf = npfft.fft(data) 123 | print(f"numpy took {(time.time()-start)*1000} ms") 124 | start = time.time() 125 | pf = pyfft(data) 126 | print(f"python fft took {(time.time()-start)*1000} ms") 127 | start = time.time() 128 | af = fft(data) 129 | print(f"auto fft took {(time.time()-start)*1000} ms") 130 | 131 | for n in range(len(nf)): 132 | val = abs(pf[n] - nf[n]) 133 | if val > 0.001: 134 | print("bad!", n, val) 135 | -------------------------------------------------------------------------------- /srv/http/bash/settings/data-service.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /srv/http/bash/common.sh 4 | 5 | CMD=$1 6 | PKG=$1 7 | SERVICE=$1 8 | skip='register IPv6' 9 | 10 | configText() { 11 | local config l lines linesL next 12 | config="$( pacman -Q $PKG )" 13 | readarray -t lines <<< $( grep -Ev '^#|=$|^$' $1 | awk NF ) 14 | [[ ! $lines ]] && echo $config && return 15 | 16 | config+=$'\n'"# cat $1" 17 | linesL=${#lines[@]} 18 | for (( i=0; i < $linesL; i++ )); do # remove empty sections 19 | l=${lines[i]} 20 | next=${lines[i + 1]} 21 | if [[ ${l:0:1} == [ ]]; then 22 | [[ $next && ${next:0:1} != [ ]] && config+=$'\n'$l 23 | else 24 | config+=$'\n'$l 25 | fi 26 | done 27 | echo "$config" 28 | } 29 | 30 | case $CMD in 31 | ap ) 32 | PKG=iwd 33 | SERVICE=$PKG 34 | conf=$( configText /var/lib/iwd/ap/$( hostname ).ap ) 35 | systemctl -q is-active iwd && conf+=" 36 | # iwctl ap list 37 | $( iwctl ap list | perl -pe 's/\e\[[0-9;]*m//g' )" # remove stdout colors 38 | [[ $2 ]] && echo "$conf" && exit 39 | # -------------------------------------------------------------------- 40 | ;; 41 | bluealsa ) 42 | conf="\ 43 | # bluealsa-aplay -L 44 | $( bluealsa-aplay -L )" 45 | ;; 46 | bluez ) 47 | SERVICE=bluetooth 48 | conf=$( configText /etc/bluetooth/main.conf ) 49 | ;; 50 | camilladsp ) 51 | conf=$( configText /etc/default/camilladsp ) 52 | ;; 53 | dabradio ) 54 | PKG=mediamtx 55 | SERVICE=$PKG 56 | conf="\ 57 | # rtl_test -t 58 | $( tty2std 'timeout 0.1 rtl_test -t' )" 59 | ;; 60 | localbrowser ) 61 | PKG=firefox 62 | conf=$( configText $dirsystem/localbrowser.conf ) 63 | skip+='|FATAL: Module g2d_23 not found|XKEYBOARD keymap|Could not resolve keysym|Errors from xkbcomp|Failed to connect to session manager' 64 | ;; 65 | mpd ) 66 | conf=$( grep -Ev '^i|^#' $mpdconf ) 67 | for file in autoupdate buffer normalization outputbuffer pllength replaygain custom; do 68 | fileconf=$dirmpdconf/$file.conf 69 | [[ -e $fileconf ]] && conf+=$'\n'$( < $fileconf ) 70 | done 71 | conf=$( sort <<< $conf | sed 's/ *"/^"/' | column -t -s^ ) 72 | for file in curl cdio ffmpeg bluetooth camilladsp fifo httpd snapserver output soxr soxr-custom; do 73 | fileconf=$dirmpdconf/$file.conf 74 | [[ -e $fileconf ]] && conf+=$'\n'$( < $fileconf ) 75 | done 76 | conf="\ 77 | # $mpdconf 78 | $conf" 79 | skip+='|configuration file does not exist|wildmidi' 80 | ;; 81 | nfsserver ) 82 | PKG=nfs-utils 83 | SERVICE=nfs-server 84 | sharedip=$( grep -v $( ipAddress ) $filesharedip ) 85 | [[ ! $sharedip ]] && sharedip='(none)' 86 | conf=$( configText /etc/exports ) 87 | systemctl -q is-active nfs-server && conf+=" 88 | 89 | # Active clients: 90 | $sharedip" 91 | skip+='|Protocol not supported' 92 | ;; 93 | shairportsync ) 94 | PKG=shairport-sync 95 | SERVICE=$PKG 96 | ;; 97 | smb ) 98 | PKG=samba 99 | conf=$( configText /etc/samba/smb.conf ) 100 | ;; 101 | snapclient ) 102 | PKG=snapcast 103 | conf="\ 104 | $( configText /etc/default/snapclient ) 105 | 106 | # avahi-browse -kprt _snapcast._tcp (SnapServer list) 107 | " 108 | if [[ -e $dirsystem/snapclientserver ]]; then 109 | conf+='(SnapClient + SnapServer)' 110 | else 111 | name_ip=$( snapserverList | jq -r .[] ) 112 | [[ $name_ip ]] && conf+=$name_ip || conf+='(Not available)' 113 | fi 114 | ;; 115 | snapserver ) 116 | PKG=snapcast 117 | conf=$( configText /etc/snapserver.conf ) 118 | ;; 119 | spotifyd ) 120 | skip+='|No.*specified|no usable credentials' 121 | ;; 122 | upmpdcli ) 123 | skip+='|not creating entry for' 124 | ;; 125 | vuled ) 126 | PKG=cava 127 | SERVICE=$PKG 128 | ;; 129 | esac 130 | [[ ! $conf ]] && conf=$( configText /etc/$PKG.conf ) 131 | status=$( systemctl status $SERVICE \ 132 | | grep -E -v "$skip" \ 133 | | statusColor ) 134 | 135 | echo "\ 136 | $conf 137 | 138 | # systemctl status $SERVICE 139 | $status" 140 | -------------------------------------------------------------------------------- /srv/http/bash/settings/features-data.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # shareddata: 4 | # $nfsserver = server rAudio 5 | # [[ -L $dirmpd && ! $nfsserver ]] = clients 6 | # grep -q -m1 :/mnt/MPD/NAS /etc/fstab = clients with server rAudio 7 | # [[ -e $dirdata/sharedip ]] = server + clients 8 | 9 | . /srv/http/bash/common.sh 10 | 11 | data+=$( settingsActive camilladsp nfs-server shairport-sync smb snapserver spotifyd upmpdcli ) 12 | data+=$( settingsEnabled \ 13 | $dirmpdconf httpd.conf \ 14 | $dirsystem ap autoplay equalizer login loginsetting lyrics dabradio multiraudio scrobble snapclientserver volumelimit \ 15 | $dirshm nosound ) 16 | if systemctl -q is-enabled localbrowser; then 17 | systemctl -q is-active localbrowser && localbrowser=true || localbrowser=-1 18 | fi 19 | ########## 20 | data+=' 21 | , "hostname" : "'$( hostname )'" 22 | , "ip" : "'$( ipAddress )'" 23 | , "localbrowser" : '$localbrowser' 24 | , "nfsconnected" : '$( [[ -e $filesharedip && $( lineCount $filesharedip ) > 1 ]] && echo true )' 25 | , "shareddata" : '$( [[ -L $dirmpd ]] && grep -q nfsserver.*false <<< $data && echo true )' 26 | , "snapclient" : '$( ls $dirsystem/snapclien* &> /dev/null && echo true )' 27 | , "ssid" : "'$( iwgetid -r )'" 28 | , "stoptimer" : '$( exists $dirshm/pidstoptimer )' 29 | , "wlan" : '$( [[ -e $dirshm/wlan ]] && echo true ) 30 | 31 | data2json "$data" $1 32 | -------------------------------------------------------------------------------- /srv/http/bash/settings/networks-data.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /srv/http/bash/common.sh 4 | 5 | if rfkill | grep -q -m1 bluetooth && systemctl -q is-active bluetooth; then 6 | devicebt=true 7 | devices=$( bluetoothctl devices Paired | sort -k3 -fh ) 8 | fi 9 | if [[ $devices ]]; then 10 | while read dev; do 11 | mac=$( cut -d' ' -f2 <<< $dev ) 12 | info=$( bluetoothctl info $mac ) 13 | listbluetooth+=',{ 14 | "mac" : "'$mac'" 15 | , "name" : "'$( cut -d' ' -f3- <<< $dev )'" 16 | , "connected" : '$( grep -q -m1 'Connected: yes' <<< $info && echo true || echo false )' 17 | , "type" : "'$( awk '/UUID: Audio/ {print $3}' <<< $info | tr -d '\n' )'" 18 | }' 19 | done <<< $devices 20 | listbluetooth='[ '${listbluetooth:1}' ]' 21 | fi 22 | 23 | gateway=$( ip -j route | jq -r .[0].gateway ) 24 | 25 | wlandev=$( < $dirshm/wlan ) 26 | profiles=$( ls -p /etc/netctl | grep -v /$ ) 27 | current=$( iwgetid -r ) 28 | if [[ $profiles ]]; then 29 | while read profile; do 30 | ssid=$( quoteEscape $profile ) 31 | ! grep -q 'Interface="*'$wlandev "/etc/netctl/$profile" && continue 32 | if [[ $current == $profile ]]; then 33 | ip=$( ifconfig $wlandev | awk '/inet .* netmask/ {print $2}' ) 34 | dbm=$( awk '/'$wlandev'/ {print $4}' /proc/net/wireless | tr -d . ) 35 | if [[ ! $dbm || $dbm -gt -60 ]]; then 36 | icon=wifi 37 | elif (( $dbm < -67 )); then 38 | icon=wifi1 39 | else 40 | icon=wifi2 41 | fi 42 | listwlan=',{ 43 | "gateway" : "'$gateway'" 44 | , "icon" : "'$icon'" 45 | , "ip" : "'$ip'" 46 | , "ssid" : "'$ssid'" 47 | }' 48 | else 49 | notconnected+=',{ 50 | "ssid" : "'$ssid'" 51 | }' 52 | fi 53 | done <<< $profiles 54 | fi 55 | [[ $notconnected ]] && listwlan+="$notconnected" 56 | [[ $listwlan ]] && listwlan='[ '${listwlan:1}' ]' 57 | 58 | # lan 59 | ip=$( ifconfig | grep -A1 ^e | awk '/inet .* netmask/ {print $2}' ) 60 | if [[ $ip ]]; then 61 | listlan='{ 62 | "ADDRESS" : "'$ip'" 63 | , "GATEWAY" : "'$gateway'" 64 | , "DHCP" : '$( ip -j route | jq -c .[] | grep -q 'dev":"e.*dhcp' && echo true )' 65 | }' 66 | fi 67 | 68 | [[ -e $dirsystem/ap ]] && apconf=$( getContent $dirsystem/ap.conf ) 69 | ip=$( ipAddress ) 70 | [[ $ip ]] && hostname=$( avahi-resolve -a4 $ip | awk '{print $NF}' ) 71 | ########## 72 | data=' 73 | , "device" : { 74 | "bluetooth" : '$devicebt' 75 | , "lan" : '$( ifconfig | grep -q ^e && echo true )' 76 | , "wlan" : '$( rfkill | grep -q -m1 wlan && echo true )' 77 | } 78 | , "ap" : '$( exists $dirsystem/ap )' 79 | , "apconf" : '$apconf' 80 | , "apstartup" : '$( exists $dirshm/apstartup )' 81 | , "gateway" : "'$gateway'" 82 | , "hostname" : "'${hostname/.*}'" 83 | , "ip" : "'$ip'" 84 | , "list" : { 85 | "bluetooth" : '$listbluetooth' 86 | , "lan" : '$listlan' 87 | , "wlan" : '$listwlan' 88 | }' 89 | data2json "$data" $1 90 | -------------------------------------------------------------------------------- /srv/http/bash/settings/networks-scan.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /srv/http/bash/common.sh 4 | 5 | killProcess networksscan 6 | echo $$ > $dirshm/pidnetworksscan 7 | 8 | if [[ $1 == wlan ]]; then 9 | wlandev=$( < $dirshm/wlan ) 10 | ip link set $wlandev up 11 | 12 | # ESSID:"NAME" 13 | # Encryption key:on 14 | # Quality=37/70 Signal level=-73 dBm || Quality=0/100 Signal level=25/100 15 | # IE: IEEE 802.11i/WPA2 Version 1 16 | # IE: WPA Version 1 17 | scan=$( iwlist $wlandev scan ) 18 | [[ ! $scan ]] && exit 19 | # -------------------------------------------------------------------- 20 | scan=$( sed -E 's/^\s*|\s*$//g' <<< $scan \ 21 | | sed -E -n '/^ESSID|^Encryption|^IE.*WPA|^Quality/ { 22 | s/\\x00//g 23 | s/^Quality.*level.(.*) dBm/,{,"signal":\1/ 24 | s/^Encryption key:(.*)/,"encrypt":"\1"/ 25 | s/^ESSID:/,"ssid":/ 26 | s/^IE.*WPA.*/,"wpa":true/ 27 | p 28 | }' \ 29 | | tr -d '\n' \ 30 | | sed 's/{,/{/g; s/,{/\n&/g' \ 31 | | grep -E -v '^$|"ssid":""' \ 32 | | sed 's/"signal":,/"signal":-67,/; s/wpa.*wpa/wpa/; s/$/}/' ) # ,{...} > [ {...} ] 33 | echo '{ 34 | "scan" : [ '${scan:1}' ] 35 | , "current" : "'$( iwgetid -r )'" 36 | , "profiles" : '$( line2array "$( ls -p /etc/netctl | grep -v /$ )" )' 37 | }' 38 | exit 39 | # -------------------------------------------------------------------- 40 | fi 41 | bluetoothctl --timeout=10 scan on &> /dev/null 42 | devices=$( bluetoothctl devices \ 43 | | grep -v ' ..-..-..-..-..-..$' \ 44 | | sort -k3 -fh ) 45 | [[ ! $devices ]] && exit 46 | # -------------------------------------------------------------------- 47 | connected=$( bluetoothctl devices Connected ) 48 | paired=$( bluetoothctl devices Paired ) 49 | while read dev; do 50 | mac=$( cut -d' ' -f2 <<< $dev ) 51 | data+=',{ 52 | "mac" : "'$mac'" 53 | , "name" : "'$( cut -d' ' -f3- <<< $dev )'" 54 | , "current" : '$( grep -q -m1 $mac <<< $connected && echo true || echo false )' 55 | , "paired" : '$( grep -q -m1 $mac <<< $paired && echo true || echo false )' 56 | }' 57 | done <<< $devices 58 | 59 | echo "[ ${data:1} ]" 60 | -------------------------------------------------------------------------------- /srv/http/bash/settings/networks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /srv/http/bash/common.sh 4 | 5 | args2var "$1" 6 | 7 | netctlSwitch() { 8 | currentssid=$( iwgetid -r ) 9 | ip link set $wlandev down 10 | [[ $currentssid ]] && netctl switch-to "$ESSID" || netctl start "$ESSID" 11 | for i in {0..20}; do 12 | sleep 1 13 | [[ $( iwgetid -r ) == $ESSID ]] && connected=1 && break 14 | done 15 | if [[ $connected ]]; then 16 | netctl enable "$ESSID" &> /dev/null 17 | avahi-daemon --kill # flush cache and restart 18 | pushRefresh 19 | else 20 | notify wifi "$ESSID" 'Connecting failed.' 21 | if [[ $currentssid ]]; then 22 | echo "$backup" > "/etc/netctl/$currentssid" 23 | ip link set $wlandev down 24 | netctl start "$currentssid" 25 | notify wifi "$currentssid" Restored 26 | fi 27 | fi 28 | } 29 | wlanDevice() { 30 | local wlandev 31 | if test -e /sys/class/net/w*; then 32 | wlandev=$( ls /sys/class/net | grep ^w ) 33 | echo $wlandev | tee $dirshm/wlan 34 | ( sleep 1 && iw $wlandev set power_save off ) & 35 | else 36 | rm -f $dirshm/wlan 37 | fi 38 | } 39 | 40 | case $CMD in 41 | 42 | btrename ) 43 | bluetoothctl set-alias "$NEWNAME" 44 | amixer -D bluealsa scontrols | cut -d"'" -f2 > $dirshm/btmixer 45 | pushRefresh 46 | pushRefresh player 47 | [[ -e $dirsystem/camilladsp ]] && pushRefresh camilla 48 | ;; 49 | connect ) 50 | wlandev=$( < $dirshm/wlan ) 51 | if [[ $ADDRESS ]]; then 52 | ipOnline $ADDRESS && echo -1 && exit 53 | # -------------------------------------------------------------------- 54 | iptype=static 55 | else 56 | iptype=dhcp 57 | fi 58 | if [[ -e $dirsystem/ap ]]; then 59 | systemctl stop iwd 60 | rm -f $dirsystem/{ap,ap.conf} 61 | else 62 | currentssid=$( iwgetid -r ) 63 | [[ $currentssid == $ESSID ]] && backup=$( < "/etc/netctl/$currentssid" ) 64 | fi 65 | data='Interface='$wlandev' 66 | Connection=wireless 67 | IP='$iptype' 68 | ESSID="'$ESSID'"' 69 | if [[ $KEY ]]; then 70 | [[ $SECURITY ]] && security=wep || security=wpa 71 | data+=' 72 | Key="'$KEY'"' 73 | else 74 | security=none 75 | fi 76 | [[ $ADDRESS ]] && data+=' 77 | Address='$ADDRESS'/24 78 | Gateway='$GATEWAY 79 | data+=' 80 | Security='$security 81 | [[ $HIDDEN ]] && data+=' 82 | Hidden=yes' 83 | echo "$data" > "/etc/netctl/$ESSID" 84 | [[ $( iwgetid -r ) ]] && netctlSwitch 85 | ;; 86 | disconnect ) 87 | netctl stop "$SSID" 88 | systemctl stop wpa_supplicant 89 | ip link set $( < $dirshm/wlan ) up 90 | pushRefresh 91 | ;; 92 | lanedit ) 93 | if [[ $ADDRESS ]]; then 94 | ipOnline $ADDRESS && echo -1 && exit 95 | # -------------------------------------------------------------------- 96 | fi 97 | file=$( ls /etc/systemd/network/e* | head -1 ) 98 | if [[ $ADDRESS ]]; then # static 99 | sed -i -E -e '/^DHCP|^Address|^Gateway/ d 100 | ' -e '/^DNSSEC/ i\ 101 | Address='$ADDRESS'/24\ 102 | Gateway='$GATEWAY $file 103 | else # dhcp - reset 104 | sed -i -E -e '/^DHCP|^Address|^Gateway/ d 105 | ' -e '/^DNSSEC/ i\DHCP=yes' $file 106 | fi 107 | systemctl restart systemd-networkd 108 | avahi-daemon --kill # flush cache and restart 109 | for i in {0..9}; do 110 | [[ $( ifconfig | grep -A1 ^e | awk '/inet .* netmask/ {print $2}' ) ]] && break || sleep 1 111 | done 112 | pushRefresh 113 | ;; 114 | profileconnect ) 115 | wlandev=$( < $dirshm/wlan ) 116 | if [[ -e $dirsystem/ap ]]; then 117 | rm -f $dirsystem/{ap,ap.conf} 118 | systemctl stop iwd 119 | ifconfig $wlandev 0.0.0.0 120 | sleep 2 121 | fi 122 | netctlSwitch 123 | ;; 124 | profileforget ) 125 | if netctl is-active "$SSID" &> /dev/null; then 126 | netctl stop "$SSID" 127 | systemctl stop wpa_supplicant 128 | ip link set $( < $dirshm/wlan ) up 129 | fi 130 | netctl is-enabled "$SSID" &> /dev/null && netctl disable "$SSID" 131 | rm "/etc/netctl/$SSID" 132 | pushRefresh 133 | ;; 134 | usbbluetoothon ) # from usbbluetooth.rules 135 | ! systemctl -q is-active bluetooth && systemctl start bluetooth 136 | [[ ! -e $dirshm/startup ]] && exit # suppress on startup 137 | # -------------------------------------------------------------------- 138 | sleep 3 139 | pushRefresh features 140 | pushRefresh 141 | notify bluetooth 'USB Bluetooth' Ready 142 | ;; 143 | usbbluetoothoff ) # from usbbluetooth.rules 144 | ! rfkill | grep -q -m1 bluetooth && systemctl stop bluetooth 145 | notify bluetooth 'USB Bluetooth' Removed 146 | pushRefresh features 147 | pushRefresh 148 | ;; 149 | usbwifion ) 150 | wlanDevice 151 | [[ ! -e $dirshm/startup ]] && exit # suppress on startup 152 | # -------------------------------------------------------------------- 153 | notify wifi 'USB Wi-Fi' Ready 154 | pushRefresh 155 | ;; 156 | usbwifioff ) 157 | wlanDevice 158 | notify wifi 'USB Wi-Fi' Removed 159 | pushRefresh 160 | ;; 161 | wlandevice ) 162 | wlanDevice 163 | ;; 164 | 165 | esac 166 | -------------------------------------------------------------------------------- /srv/http/bash/settings/player-asound.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ### included by <<< player-conf.sh 4 | if [[ ! $dirbash ]]; then # if run directly 5 | . /srv/http/bash/common.sh 6 | . $dirshm/output 7 | CARD=$card 8 | NAME=$name 9 | fi 10 | 11 | if [[ -e $dirshm/btreceiver ]]; then 12 | BLUETOOTH=1 13 | systemctl -q is-active localbrowser && action=stop || action=start 14 | systemctl $action bluetoothbutton 15 | else 16 | systemctl stop bluetoothbutton 17 | fi 18 | if [[ -e $dirsystem/camilladsp ]]; then 19 | modprobe snd_aloop 20 | fileconf=$( getVar CONFIG /etc/default/camilladsp ) 21 | channels=$( getVar capture.channels "$fileconf" ) 22 | format=$( getVar capture.format "$fileconf" ) 23 | rate=$( getVar devices.samplerate "$fileconf" ) 24 | CAMILLADSP=1 25 | ######## 26 | ASOUNDCONF+=' 27 | pcm.!default { 28 | type plug 29 | slave.pcm camilladsp 30 | } 31 | pcm.camilladsp { 32 | type plug 33 | slave { 34 | pcm { 35 | type hw 36 | card Loopback 37 | device 0 38 | channels '$channels' 39 | format '$format' 40 | rate '$rate' 41 | } 42 | } 43 | } 44 | ctl.!default { 45 | type hw 46 | card Loopback 47 | } 48 | ctl.camilladsp { 49 | type hw 50 | card Loopback 51 | }' 52 | else 53 | systemctl stop camilladsp &> /dev/null 54 | rmmod snd-aloop &> /dev/null 55 | if [[ $BLUETOOTH ]]; then 56 | ######## 57 | ASOUNDCONF+=' 58 | pcm.bluealsa { 59 | type plug 60 | slave.pcm { 61 | type bluealsa 62 | device 00:00:00:00:00:00 63 | profile "a2dp" 64 | } 65 | }' 66 | fi 67 | if [[ -e $dirsystem/equalizer ]]; then 68 | if [[ $BLUETOOTH ]]; then 69 | slavepcm=bluealsa 70 | elif [[ $CARD != -1 ]]; then 71 | slavepcm='"plughw:'$CARD',0"' 72 | fi 73 | if [[ $slavepcm ]]; then 74 | EQUALIZER=1 75 | ######## 76 | ASOUNDCONF+=' 77 | pcm.!default { 78 | type plug 79 | slave.pcm plugequal 80 | } 81 | pcm.plugequal { 82 | type equal 83 | slave.pcm '$slavepcm' 84 | } 85 | ctl.equal { 86 | type equal 87 | }' 88 | fi 89 | fi 90 | if [[ -e $dirmpdconf/snapserver.conf ]]; then 91 | ASOUNDCONF+=' 92 | pcm.!default { 93 | type plug 94 | slave.pcm rate48000Hz 95 | } 96 | pcm.rate48000Hz { 97 | type rate 98 | slave { 99 | pcm writeFile 100 | format S16_LE 101 | rate 48000 102 | } 103 | } 104 | pcm.writeFile { 105 | type file 106 | slave.pcm null 107 | file "/tmp/snapfifo" 108 | format "raw" 109 | }' 110 | fi 111 | fi 112 | ######## > 113 | echo "$ASOUNDCONF" >> /etc/asound.conf # append after default lines set by player-devices.sh 114 | -------------------------------------------------------------------------------- /srv/http/bash/settings/player-data.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ! mpc &> /dev/null && echo notrunning && exit 4 | 5 | . /srv/http/bash/common.sh 6 | 7 | data+=$( settingsEnabled \ 8 | $dirsystem camilladsp custom dabradio devicewithbt equalizer soxr \ 9 | $dirmpdconf autoupdate.conf buffer.conf ffmpeg.conf normalization.conf outputbuffer.conf replaygain.conf ) 10 | 11 | crossfade=$( mpc crossfade | cut -d' ' -f2 ) 12 | mixers=$( getContent $dirshm/mixers ) 13 | [[ -e $dirshm/amixercontrol && ! ( -e $dirshm/btreceiver && ! -e $dirsystem/devicewithbt ) ]] && volume=( $( volumeGet valdb hw ) ) 14 | 15 | ########## 16 | data+=' 17 | , "asoundcard" : '$( getContent $dirsystem/asoundcard )' 18 | , "bluetooth" : '$( exists $dirshm/btreceiver )' 19 | , "btmixer" : "'$( getContent $dirshm/btmixer )'" 20 | , "counts" : '$( < $dirmpd/counts )' 21 | , "crossfade" : '$( [[ $( mpc crossfade | cut -d' ' -f2 ) != 0 ]] && echo true )' 22 | , "devices" : '$( getContent $dirshm/devices )' 23 | , "dop" : '$( grep -qs dop.*yes $dirmpdconf/output.conf && echo true )' 24 | , "lastupdate" : "'$( date -d "$( mpc stats | sed -n '/^DB Updated/ {s/.*: \+//; p }' )" '+%Y-%m-%d · %H:%M' )'" 25 | , "lists" : { 26 | "albumignore" : '$( exists $dirmpd/albumignore )' 27 | , "mpdignore" : '$( exists $dirmpd/mpdignorelist )' 28 | , "nonutf8" : '$( exists $dirmpd/nonutf8 )' 29 | } 30 | , "mixers" : '$mixers' 31 | , "mixertype" : '$( [[ $( getVar mixertype $dirshm/output ) != none ]] && echo true )' 32 | , "output" : '$( conf2json -nocap $dirshm/output )' 33 | , "player" : "'$( < $dirshm/player )'" 34 | , "pllength" : '$( mpc status %length% )' 35 | , "state" : "'$( mpcState )'" 36 | , "updatetime" : "'$( getContent $dirmpd/updatetime )'" 37 | , "updating_db" : '$( [[ -e $dirmpd/listing || -e $dirmpd/updating ]] && echo true )' 38 | , "version" : "'$( pacman -Q mpd 2> /dev/null | cut -d' ' -f2 )'" 39 | , "volume" : '${volume[0]}' 40 | , "volumedb" : '${volume[1]}' 41 | , "volumemax" : '$( volumeMaxGet ) 42 | 43 | filter=$( echo 'camilladsp equalizer crossfade soxr normalization replaygain mixertype ' | sed 's/ /.*true|/g; s/|$//' ) 44 | if [[ ${volume[1]/.00} != 0 ]] || grep -q -m1 -E $filter <<< $data; then 45 | novolume=false 46 | else 47 | novolume=true 48 | fi 49 | data+=' 50 | , "novolume" : '$novolume 51 | 52 | data2json "$data" $1 53 | -------------------------------------------------------------------------------- /srv/http/bash/settings/player-devices.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ### included by <<< player-conf.sh 4 | [[ ! $dirbash ]] && . /srv/http/bash/common.sh # if run directly 5 | 6 | ### aplay -l # depend on /etc/asound.conf 7 | # card 1: Headphones [bcm2835 Headphones], device 0: bcm2835 Headphones [bcm2835 Headphones] 8 | # 9 | ### cat /proc/asound/cards 10 | # 1 [Headphones ]: bcm2835_headpho - bcm2835 Headphones 11 | # bcm2835 Headphones 12 | #>C [CARD_ID ]: DEVICE_ID - DEVICE_NAME 13 | #> DEVICE_LONG_NAME 14 | #>hwaddress=hw:1,0 # or hw:Headphones:0 15 | 16 | ### cat /proc/asound/card1/id 17 | # Headphones 18 | 19 | ### cat /proc/asound/card1/*/info 20 | # card: 1 21 | # ... 22 | # id: bcm2835 Headphones 23 | # name: bcm2835 Headphones 24 | # ... 25 | 26 | outputdevice=$( getContent $dirsystem/output-device ) 27 | proccardn=$( ls -d /proc/asound/card[0-9] ) # not depend on /etc/asound.conf which might be broken from bad script 28 | usbdac=$( ls -d /proc/asound/card[0-9]/usbmixer 2> /dev/null | wc -l ) 29 | lastcard=$(( ${proccardn: -1} - usbdac ))# last card - not usb 30 | while read path; do 31 | info=$( sed 's/bcm2835/On-board/' $path/*/info ) 32 | name=$( grep -m1 ^name <<< $info | cut -d' ' -f2- ) 33 | [[ ! $name ]] && name=$( grep -m1 ^id <<< $info | cut -d' ' -f2- ) 34 | [[ $name == Loopback* ]] && continue 35 | 36 | CARD=${path: -1} 37 | if [[ $CARD == $lastcard && -e $dirsystem/audio-output ]]; then # last card - not on-board 38 | NAME=$( < $dirsystem/audio-output ) 39 | else 40 | NAME=$name 41 | fi 42 | if [[ -e $path/usbmixer ]]; then 43 | usbmixer=$( sed -n -E '/^Card/ {s/^Card: | at .*//g; p}' $path/usbmixer ) 44 | if [[ $usbmixer ]]; then 45 | NAME=$usbmixer 46 | if [[ $NAME == $outputdevice ]]; then 47 | usbcard=$CARD 48 | usbname=$NAME 49 | fi 50 | fi 51 | fi 52 | lastword=$( awk '{print $NF}' <<< $NAME ) 53 | [[ $lastword == *-* && $lastword =~ ^[a-z0-9-]+$ ]] && NAME=$( sed 's/ [^ ]*$//' <<< $NAME ) 54 | LISTDEVICE+=', "'$NAME'": "hw:'$CARD',0"' 55 | card_name+="$CARD^$NAME"$'\n' 56 | done <<< $proccardn 57 | 58 | if [[ $usbcard ]]; then 59 | CARD=$usbcard 60 | NAME=$usbname 61 | elif [[ ! -e $dirshm/usbdac && $outputdevice ]]; then # otherwise last card 62 | c_n=$( grep "$outputdevice$" <<< $card_name ) 63 | if [[ $c_n ]]; then 64 | CARD=${c_n/^*} 65 | NAME=$outputdevice 66 | else 67 | rm $dirsystem/output-device # remove if not exist any more 68 | fi 69 | fi 70 | ######## > 71 | echo -n "\ 72 | defaults.pcm.card $CARD 73 | defaults.ctl.card $CARD 74 | " > /etc/asound.conf 75 | 76 | amixer=$( amixer -c $CARD scontents ) 77 | if [[ $amixer ]]; then 78 | amixer=$( grep -A1 ^Simple <<< $amixer \ 79 | | sed 's/^\s*Cap.*: /^/' \ 80 | | tr -d '\n' \ 81 | | sed 's/--/\n/g' \ 82 | | grep -v "'Mic'" ) 83 | controls=$( grep -E 'volume.*pswitch|Master.*volume' <<< $amixer ) 84 | [[ ! $controls ]] && controls=$( grep volume <<< $amixer ) 85 | if [[ $controls ]]; then 86 | controls=$( cut -d"'" -f2 <<< $controls | sort -u ) 87 | while read; do # no var name - use default REPLY to preserve trailing/all spaces 88 | LISTMIXER+=', "'$REPLY'"' 89 | [[ $REPLY == Digital ]] && MIXER=Digital 90 | done <<< "$controls" 91 | mixerfile="$dirsystem/mixer-$NAME" 92 | if [[ -e $mixerfile ]]; then # manual 93 | MIXER=$( < "$mixerfile" ) 94 | elif [[ ! $MIXER ]]; then # not Digital 95 | MIXER=$( head -1 <<< $controls ) 96 | fi 97 | fi 98 | fi 99 | if [[ $LISTMIXER ]]; then 100 | ######## > 101 | echo "[ ${LISTMIXER:1} ]" > $dirshm/mixers 102 | echo "$MIXER" > $dirshm/amixercontrol 103 | MIXERTYPE=hardware 104 | else 105 | rm -f $dirshm/{amixercontrol,mixers} 106 | MIXERTYPE=none 107 | fi 108 | mixertypefile="$dirsystem/mixertype-$NAME" 109 | [[ -e $mixertypefile ]] && MIXERTYPE=$( < "$mixertypefile" ) 110 | ######## > 111 | echo ' 112 | card='$CARD' 113 | name="'$NAME'" 114 | mixer="'$MIXER'" 115 | mixertype='$MIXERTYPE > $dirshm/output 116 | echo "{ ${LISTDEVICE:1} }" > $dirshm/devices 117 | echo $CARD > $dirsystem/asoundcard 118 | -------------------------------------------------------------------------------- /srv/http/bash/settings/player-wm5102.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | card=RPiCirrus 4 | output=$1 5 | [[ $output == 'SPDIF Out' ]] && volume=100 || volume=50 6 | 7 | HPOUT1="\ 8 | HPOUT1L Input 1 9 | HPOUT1R Input 1 10 | HPOUT1L Input 1 Volume 11 | HPOUT1R Input 1 Volume 12 | HPOUT1 Digital Volume 13 | HPOUT1 Digital Switch" 14 | 15 | declare -A controls 16 | controls['HPOUT1 Digital']=$HPOUT1 17 | controls['HPOUT2 Digital']=$( sed 's/1/2/' <<< $HPOUT1 ) 18 | controls['Speaker Digital']=$( sed 's/HPOUT1 /Speaker /; s/HPOUT1/SPKOUT/' <<< $HPOUT1 ) 19 | controls['SPDIF Out']="\ 20 | $( sed -n '/Input/ {s/HPOUT1L/AIF2TX1/; s/HPOUT1R/AIF2TX2/; p}' <<< $HPOUT1 ) 21 | Input Source 22 | AIF Playback Switch 23 | TX Playback Switch 24 | SPDIF Out Switch" 25 | 26 | # Switch everything off 27 | control_all=$( printf "%s\n" "${controls[@]}" ) 28 | while read control; do 29 | [[ ${control: -1} =~ [12] ]] && val=None || val=off 30 | amixer -c $card -q cset "$control" $val 31 | done <<< $control_all 32 | 33 | while read control; do 34 | if [[ $control == *' Digital Volume' ]]; then 35 | val=$volume% 36 | else 37 | case ${control: -6} in 38 | Volume ) val=$volume%;; 39 | Switch ) val=on;; 40 | Source ) val=AIF;; 41 | * ) val=AIF1RX$( sed -E 's/.*(.)$/\1/; s/L/1/; s/R/2/' <<< ${control/ *} );; 42 | esac 43 | fi 44 | amixer -c $card -Mq cset "$control" $val 45 | done <<< ${controls["$output"]} 46 | -------------------------------------------------------------------------------- /srv/http/bash/settings/player.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /srv/http/bash/common.sh 4 | 5 | args2var "$1" 6 | 7 | linkConf() { 8 | ln -sf $dirmpdconf/{conf/,}$CMD.conf 9 | } 10 | amixer0dB() { 11 | if [[ -e $dirshm/amixercontrol ]]; then 12 | . $dirshm/output 13 | volumeAmixer 0dB "$mixer" $card 14 | volumeGet push hw 15 | fi 16 | } 17 | 18 | case $CMD in 19 | 20 | autoupdate | ffmpeg | normalization ) 21 | [[ $ON ]] && linkConf || rm $dirmpdconf/$CMD.conf 22 | systemctl restart mpd 23 | pushRefresh 24 | ;; 25 | buffer | outputbuffer ) 26 | if [[ $ON ]]; then 27 | if [[ $CMD == buffer ]]; then 28 | data='audio_buffer_size "'$AUDIO_BUFFER_SIZE'"' 29 | [[ $AUDIO_BUFFER_SIZE != 4096 ]] && link=1 30 | else 31 | data='max_output_buffer_size "'$MAX_OUTPUT_BUFFER_SIZE'"' 32 | [[ $MAX_OUTPUT_BUFFER_SIZE != 8192 ]] && link=1 33 | fi 34 | echo "$data" > $dirmpdconf/conf/$CMD.conf 35 | fi 36 | [[ $link ]] && linkConf || rm $dirmpdconf/$CMD.conf 37 | $dirsettings/player-conf.sh 38 | ;; 39 | crossfade ) 40 | [[ $ON ]] && sec=$SEC || sec=0 41 | mpc -q crossfade $sec 42 | pushRefresh 43 | ;; 44 | custom ) 45 | if [[ $ON ]]; then 46 | fileglobal=$dirmpdconf/conf/custom.conf 47 | fileoutput="$dirsystem/custom-output-$DEVICE" 48 | if [[ $GLOBAL ]]; then 49 | echo -e "$GLOBAL" > $fileglobal 50 | linkConf 51 | else 52 | rm -f $fileglobal 53 | fi 54 | [[ $OUTPUT ]] && echo -e "$OUTPUT" > "$fileoutput" || rm -f "$fileoutput" 55 | [[ $GLOBAL || $OUTPUT ]] && touch $dirsystem/custom || rm -f $dirsystem/custom 56 | $dirsettings/player-conf.sh 57 | if ! systemctl -q is-active mpd; then # config errors 58 | rm -f $fileglobal "$fileoutput" $dirmpd/custom.conf $dirsystem/custom 59 | $dirsettings/player-conf.sh 60 | echo -1 61 | fi 62 | else 63 | rm -f $dirmpdconf/custom.conf $dirsystem/custom 64 | $dirsettings/player-conf.sh 65 | fi 66 | ;; 67 | device ) 68 | echo $DEVICE > $dirsystem/output-device 69 | $dirsettings/player-conf.sh 70 | ;; 71 | devicewithbt ) 72 | enableFlagSet 73 | [[ -e $dirmpdconf/bluetooth.conf ]] && bluetooth=1 74 | [[ -e $dirmpdconf/output.conf ]] && output=1 75 | if [[ $ON ]]; then 76 | [[ $bluetooth && ! $output ]] && restart=1 77 | else 78 | [[ $bluetooth && $output ]] && restart=1 79 | fi 80 | if [[ $restart ]]; then 81 | $dirsettings/player-conf.sh 82 | else 83 | pushRefresh 84 | fi 85 | ;; 86 | dop ) 87 | name=$( getVar name $dirshm/output ) 88 | filedop=$dirsystem/dop-$name # OFF with args - value by index 89 | [[ $ON ]] && touch "$filedop" || rm -f "$filedop" 90 | $dirsettings/player-conf.sh 91 | ;; 92 | filetype ) 93 | type=$( mpd -V \ 94 | | sed -n '/\[ffmpeg/ {s/.*ffmpeg. //; s/ rtp.*//; p}' \ 95 | | tr ' ' '\n' \ 96 | | sort ) 97 | for i in {a..z}; do 98 | line=$( grep ^$i <<< $type | tr '\n' ' ' ) 99 | [[ $line ]] && list+=${line:0:-1}'
    ' 100 | done 101 | echo "${list:0:-4}" 102 | ;; 103 | mixer ) 104 | echo "$MIXER" > "$dirsystem/mixer-$DEVICE" 105 | $dirsettings/player-conf.sh 106 | ;; 107 | mixertype ) 108 | . $dirshm/output 109 | mpc -q stop 110 | filemixertype="$dirsystem/mixertype-$name" 111 | [[ $MIXERTYPE == hardware ]] && rm -f "$filemixertype" || echo $MIXERTYPE > "$filemixertype" 112 | if [[ $MIXERTYPE == software ]]; then # [sw] set to current [hw] 113 | [[ -e $dirshm/amixercontrol ]] && vol=$( volumeGet ) || vol=33 114 | mpc -q volume $vol 115 | else 116 | rm -f $dirsystem/replaygain-hw 117 | fi 118 | if [[ $mixer ]]; then # [hw] set to current [sw] || [sw/none] set 0dB 119 | if [[ $MIXERTYPE == hardware ]]; then 120 | vol=$( mpc status %volume% ) 121 | volumeAmixer $vol% "$mixer" $card 122 | else 123 | amixer0dB 124 | fi 125 | fi 126 | $dirsettings/player-conf.sh 127 | [[ $MIXERTYPE == none ]] && volumenone=true || volumenone=false 128 | pushData display '{ "volumenone": '$volumenone' }' 129 | ;; 130 | novolume ) 131 | amixer0dB 132 | echo none > "$dirsystem/mixertype-$name" 133 | mpc -q crossfade 0 134 | rm -f $dirmpdconf/{normalization,replaygain,soxr}.conf 135 | for feature in camilladsp equalizer; do 136 | [[ -e $dirsystem/$feature ]] && $dirsettings/features.sh "$feature 137 | OFF" 138 | done 139 | $dirsettings/player-conf.sh 140 | pushData display '{ "volumenone": true }' 141 | ;; 142 | replaygain ) 143 | if [[ $ON ]]; then 144 | echo 'replaygain "'$MODE'"' > $dirmpdconf/conf/replaygain.conf 145 | [[ $HARDWARE ]] && touch $dirsystem/replaygain-hw || rm -f $dirsystem/replaygain-hw 146 | linkConf 147 | else 148 | rm $dirmpdconf/replaygain.conf 149 | fi 150 | $dirsettings/player-conf.sh 151 | ;; 152 | soxr ) 153 | rm -f $dirmpdconf/soxr* $dirsystem/soxr 154 | if [[ $ON ]]; then 155 | if [[ $QUALITY == custom ]]; then 156 | data=' 157 | plugin "soxr" 158 | quality "custom" 159 | precision "'$PRECISION'" 160 | phase_response "'$PHASE_RESPONSE'" 161 | passband_end "'$PASSBAND_END'" 162 | stopband_begin "'$STOPBAND_BEGIN'" 163 | attenuation "'$ATTENUATION'" 164 | flags "'$FLAGS'"' 165 | else 166 | data=' 167 | plugin "soxr" 168 | quality "'$QUALITY'" 169 | thread "'$THREAD'"' 170 | fi 171 | echo "\ 172 | resampler {\ 173 | $data 174 | }" > $dirmpdconf/conf/$CMD.conf 175 | linkConf 176 | fi 177 | systemctl restart mpd 178 | pushRefresh 179 | ;; 180 | volume ) 181 | volume 182 | ;; 183 | volume0db ) 184 | amixer0dB 185 | ;; 186 | volume0dbbt ) 187 | btmixer=$( < $dirshm/btmixer ) 188 | volumeBlueAlsa 0dB "$btmixer" 189 | volumeGet push hw 190 | ;; 191 | volumepush ) 192 | [[ ! $BT ]] && hw=hw 193 | volumeGet push $hw 194 | ;; 195 | 196 | esac 197 | -------------------------------------------------------------------------------- /srv/http/bash/settings/system-data.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /srv/http/bash/common.sh 4 | 5 | throttled=$( vcgencmd get_throttled | cut -d= -f2 2> /dev/null ) # hex - called first to fix slip values 6 | temp=$( vcgencmd measure_temp | tr -dc [:digit:]. ) 7 | load=$( cut -d' ' -f1-3 /proc/loadavg | sed 's| | |g' )' '$temp'°C' 8 | availmem=$( free -h | awk '/^Mem/ {print $NF}' | sed -E 's|(.i)| \1B|' ) 9 | timezone=$( timedatectl | awk '/zone:/ {print $3}' ) 10 | date=$( date +'%F · %T' ) 11 | date+=" ${timezone/\// · }" 12 | since=$( uptime -s | cut -d: -f1-2 | sed 's/ / · /' ) 13 | uptime=$( uptime -p | sed -E 's/[ s]|up|ay|our|inute//g; s/,/ /g' ) 14 | uptime+=" since $since" 15 | for v in load availmem date uptime; do 16 | status+="${!v}
    " 17 | done 18 | if [[ $throttled && $throttled != 0x0 ]]; then 19 | binary=$( perl -e "printf '%020b', $throttled" ) # hex > bin 20 | # 20 bits: occurred > 11110000000000001111 < current 21 | occurred='occurred' 22 | it="CPU X" 23 | ito="${it/yl/gr} $occurred" 24 | iv="Under-voltage" 25 | declare -A warnings=( 26 | [0]=${ito/X/throttling} 27 | [1]=${ito/X/temperature limit} 28 | [2]=${ito/X/frequency capping} 29 | [3]="${iv//ora/yl} $occurred" 30 | [16]=${it/X/throttled} 31 | [17]=${it/X/temperature limit} 32 | [18]=${it/X/frequency capped} 33 | [19]=$iv 34 | ) 35 | for i in 19 18 17 16; do 36 | if [[ ${binary:$i:1} == 1 ]]; then 37 | status+="${warnings[$i]}
    " 38 | else 39 | j=$(( i - 16 )) 40 | [[ ${binary:$j:1} == 1 ]] && status+="${warnings[$j]}
    " 41 | fi 42 | done 43 | fi 44 | # for interval refresh 45 | [[ $1 == status ]] && echo $status && exit 46 | # -------------------------------------------------------------------- 47 | if [[ -e $dirshm/system ]]; then 48 | system=$( < $dirshm/system ) 49 | [[ -e $dirshm/rpi3plus ]] && rpi3plus=true 50 | else 51 | # cpu 52 | revision=$( grep ^Revision /proc/cpuinfo ) 53 | BB=${revision: -3:2} 54 | C=${revision: -4:1} 55 | # system 56 | kernel=$( uname -rm | sed -E 's| (.*)| \1|' ) 57 | model=$( tr -d '\000' < /proc/device-tree/model | sed -E 's/ Model //; s/ Plus/+/; s|( Rev.*)|\1|' ) 58 | if [[ $model == *BeagleBone* ]]; then 59 | soc=AM3358 60 | else 61 | [[ $C == 2 ]] && C+=$BB 62 | case $C in 63 | 0 ) 64 | cpu=ARM1176JZF-S 65 | soc=2835;; 66 | 1 ) 67 | cpu=Cortex-A7 68 | soc=2836;; 69 | 204 | 208 ) 70 | cpu=Cortex-A53 71 | soc=2837;; 72 | 20d | 20e ) 73 | cpu=Cortex-A53 74 | soc=2837B0;; 75 | 212 ) 76 | cpu=Cortex-A53 77 | soc=2710A1;; 78 | 3 ) 79 | cpu=Cortex-A72 80 | soc=2711;; 81 | 4 ) 82 | cpu=Cortex-A76 83 | soc=2712;; 84 | esac 85 | [[ $C != 0 ]] && cpu="4 x $cpu" 86 | [[ $soc == 2837B0 ]] && rpi3plus=true && touch $dirshm/rpi3plus 87 | soc=BCM$soc 88 | free=$( free -h | awk '/^Mem/ {print $2}' | sed -E 's|(.i)| \1B|' ) 89 | fi 90 | speed=$( lscpu | awk '/CPU max/ {print $NF}' | cut -d. -f1 ) 91 | (( $speed < 1000 )) && speed+=' MHz' || speed=$( calc 2 $speed/1000 )' GHz' 92 | system="\ 93 | rAudio $( getContent $diraddons/r1 )
    \ 94 | $kernel
    \ 95 | $model
    \ 96 | $soc $free
    \ 97 | $cpu @ $speed" 98 | echo $system > $dirshm/system 99 | fi 100 | # i2smodule 101 | if [[ -e $dirsystem/audio-aplayname && -e $dirsystem/audio-output ]]; then 102 | audioaplayname=$( < $dirsystem/audio-aplayname ) 103 | audiooutput=$( < $dirsystem/audio-output ) 104 | i2smodule=$( grep -q "$audiooutput.*$audioaplayname" /srv/http/assets/data/system-i2s.json && echo true ) 105 | fi 106 | 107 | data+=$( settingsActive bluetooth nfs-server rotaryencoder smb ) 108 | data+=$( settingsEnabled \ 109 | $dirsystem ap lcdchar mpdoled powerbutton relays soundprofile vuled \ 110 | $dirshm relayson ) 111 | ########## 112 | data+=' 113 | , "audio" : '$( grep -q -m1 ^dtparam=audio=on /boot/config.txt && echo true )' 114 | , "audioaplayname" : "'$audioaplayname'" 115 | , "audiocards" : '$( aplay -l 2> /dev/null | grep ^card | grep -q -v 'bcm2835\|Loopback' && echo true )' 116 | , "audiooutput" : "'$audiooutput'" 117 | , "hostname" : "'$( hostname )'" 118 | , "i2smodule" : '$i2smodule' 119 | , "ip" : "'$( ipAddress )'" 120 | , "lan" : '$( [[ $( lanDevice ) ]] && echo true )' 121 | , "list" : { "storage": '$( $dirsettings/system-storage.sh )' } 122 | , "rpi3plus" : '$rpi3plus' 123 | , "shareddata" : '$( [[ -L $dirmpd ]] && grep -q nfsserver.*false <<< $data && echo true )' 124 | , "status" : "'$status'" 125 | , "statusvf" : '$statusvf' 126 | , "system" : "'$system'" 127 | , "templimit" : '$( grep -q ^temp_soft_limit /boot/config.txt && echo true )' 128 | , "tft" : '$( grep -q -m1 'dtoverlay=.*rotate=' /boot/config.txt && echo true )' 129 | , "timezone" : "'$timezone'" 130 | , "timezoneoffset" : "'$( date +%z | sed -E 's/(..)$/:\1/' )'"' 131 | if [[ -e $dirshm/onboardwlan ]]; then 132 | ########## 133 | data+=' 134 | , "wlan" : '$( lsmod | grep -q -m1 brcmfmac && echo true )' 135 | , "wlanconnected" : '$( ip route | grep -q -m1 wlan0 && echo true ) 136 | ########## 137 | data+=' 138 | , "btconnected" : '$( exists $dirshm/btconnected ) 139 | fi 140 | 141 | data2json "$data" $1 142 | -------------------------------------------------------------------------------- /srv/http/bash/settings/system-databackup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /srv/http/bash/common.sh 4 | 5 | backupfile=$dirshm/backup.gz 6 | dirconfig=$dirdata/config 7 | rm -f $backupfile 8 | alsactl store 9 | files=( 10 | /boot/cmdline.txt 11 | /boot/config.txt 12 | /boot/shutdown.sh 13 | /boot/startup.sh 14 | /etc/exports 15 | /etc/fstab 16 | /etc/upmpdcli.conf 17 | /etc/conf.d/wireless-regdom 18 | /etc/default/snapclient 19 | /etc/modprobe.d/cirrus.conf 20 | /etc/modules-load.d/loopback.conf 21 | /etc/pacman.d/mirrorlist 22 | /etc/samba/smb.conf 23 | /etc/systemd/network/en.network 24 | /etc/systemd/network/eth.network 25 | /etc/systemd/timesyncd.conf 26 | /etc/X11/xorg.conf.d/99-calibration.conf 27 | /etc/X11/xorg.conf.d/99-raspi-rotate.conf 28 | /var/lib/alsa/asound.state 29 | /var/lib/iwd/ap/$( hostname ).ap 30 | /var/lib/snapserver/server.json 31 | ) 32 | for file in ${files[@]}; do 33 | if [[ -e $file ]]; then 34 | mkdir -p $dirconfig/$( dirname $file ) 35 | cp {,$dirconfig}$file 36 | fi 37 | done 38 | crossfade=$( mpc crossfade | cut -d' ' -f2 ) 39 | [[ $crossfade ]] && echo $crossfade > $dirsystem/crossfade 40 | hostname > $dirsystem/hostname 41 | timedatectl | awk '/zone:/ {print $3}' > $dirsystem/timezone 42 | profiles=$( ls -p /etc/netctl | grep -v / ) 43 | if [[ $profiles ]]; then 44 | cp -r /etc/netctl $dirconfig/etc 45 | while read profile; do 46 | if [[ $( netctl is-enabled "$profile" ) == enabled ]]; then 47 | echo $profile > $dirsystem/netctlprofile 48 | break 49 | fi 50 | done <<< $profiles 51 | fi 52 | mkdir -p $dirconfig/var/lib 53 | cp -r /var/lib/bluetooth $dirconfig/var/lib &> /dev/null 54 | xinitrcfiles=$( ls /etc/X11/xinit/xinitrc.d | grep -v 50-systemd-user.sh ) 55 | if [[ $xinitrcfiles ]]; then 56 | mkdir -p $dirconfig/etc/X11/xinit 57 | cp -r /etc/X11/xinit/xinitrc.d $dirconfig/etc/X11/xinit 58 | fi 59 | dirnasdata=/mnt/MPD/NAS/data 60 | [[ -e $dirnasdata && ! -L $dirnasdata/mpd ]] && cp -rL $dirnasdata $dirconfig 61 | 62 | services='bluetooth camilladsp iwd localbrowser mediamtx nfs-server powerbutton shairport-sync smb snapclient spotifyd upmpdcli' 63 | for service in $services; do 64 | systemctl -q is-active $service && enable+=" $service" || disable+=" $service" 65 | done 66 | [[ $enable ]] && echo $enable > $dirsystem/enable 67 | [[ $disable ]] && echo $disable > $dirsystem/disable 68 | 69 | bsdtar \ 70 | --exclude './addons' \ 71 | --exclude './embedded' \ 72 | --exclude './shm' \ 73 | -czf $backupfile \ 74 | -C /srv/http \ 75 | data \ 76 | 2> /dev/null && echo 1 77 | 78 | rm -rf $dirconfig 79 | rm -f $dirsystem/{crossfade,disable,enable,hostname,timezone} 80 | -------------------------------------------------------------------------------- /srv/http/bash/settings/system-datadefault.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /srv/http/bash/common.sh 4 | 5 | # data directories 6 | mkdir -p $dirdata/{addons,audiocd,bookmarks,camilladsp,lyrics,mpd,mpdconf,playlists,system,webradio,webradio/img} \ 7 | $dircamilladsp/{coeffs,configs,configs-bt,raw} \ 8 | /mnt/MPD/{NAS,SD,USB} 9 | ln -sf /dev/shm $dirdata 10 | ln -sf /mnt /srv/http/ 11 | chown -h http:http $dirshm /srv/http/mnt 12 | dirs=$( ls $dirdata ) 13 | for dir in $dirs; do 14 | printf -v dir$dir '%s' $dirdata/$dir 15 | done 16 | [[ $1 ]] && echo $1 > $diraddons/r1 17 | 18 | # camilladsp 19 | [[ ! -e /usr/bin/camilladsp ]] && rm -rf $dircamilladsp 20 | 21 | # display 22 | true='album albumartist artist bars buttons composer conductor count cover date fixedcover genre 23 | label latest nas playbackswitch playlists plclear plsimilar sd time usb volume webradio' 24 | false='albumbyartist albumyear audiocdplclear backonleft barsalways composername conductorname covervu 25 | hidecover progress radioelapsed tapaddplay tapreplaceplay vumeter' 26 | for i in $true; do 27 | lines+=' 28 | , "'$i'": true' 29 | done 30 | for i in $false; do 31 | lines+=' 32 | , "'$i'": false' 33 | done 34 | jq -S <<< {${lines:2}} > $dirsystem/display.json 35 | 36 | # localbrowser 37 | if [[ -e /usr/bin/firefox ]]; then 38 | timeout 1 firefox --headless &> /dev/null 39 | echo "\ 40 | rotate=0 41 | zoom=100 42 | screenoff=0 43 | onwhileplay= 44 | cursor=" > $dirsystem/localbrowser.conf 45 | fi 46 | 47 | # mirror 48 | sed -i '/^Server/ s|//.*mirror|//mirror|' /etc/pacman.d/mirrorlist 49 | 50 | # snapclient 51 | [[ -e /usr/bin/snapclient ]] && echo 'SNAPCLIENT_OPTS="--latency=800"' > /etc/default/snapclient 52 | 53 | # system 54 | hostnamectl set-hostname rAudio 55 | sed -i 's/#NTP=.*/NTP=pool.ntp.org/' /etc/systemd/timesyncd.conf 56 | sed -i 's/".*"/"00"/' /etc/conf.d/wireless-regdom 57 | timedatectl set-timezone UTC 58 | usermod -a -G root http # add user http to group root to allow /dev/gpiomem access 59 | rm -f /root/.bash_history 60 | 61 | # webradio 62 | curl -sL https://github.com/rern/rAudio-addons/raw/main/webradio/radioparadise.tar.xz | bsdtar xf - -C $dirwebradio 63 | dirPermissions 64 | $dirbash/cmd-list.sh 65 | -------------------------------------------------------------------------------- /srv/http/bash/settings/system-datareset.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /srv/http/bash/common.sh 4 | 5 | args2var "$1" 6 | 7 | ! playerActive mpd && $dirbash/cmd.sh playerstop 8 | # hostname 9 | $dirsettings/system.sh 'hostname 10 | rAudio 11 | CMD NAME' 12 | # accesspoint 13 | sed -i -E -e 's/(Passphrase=).*/\1raudioap/ 14 | ' -e 's/(Address=|Gateway=).*/\1192.168.5.1/ 15 | ' /var/lib/iwd/ap/rAudio.ap 16 | 17 | # localbrowser 18 | [[ -e /usr/bin/firefox ]] && rm -rf /root/.mozilla 19 | # mpd 20 | mpc -q clear 21 | mpc -q crossfade 0 22 | find $dirmpdconf -maxdepth 1 -type l -exec rm {} \; # mpd.conf symlink 23 | echo 'audio_buffer_size "4096"' > $dirmpdconf/conf/buffer.conf 24 | echo 'max_output_buffer_size "8192"' > $dirmpdconf/conf/outputbuffer.conf 25 | echo 'replaygain "album"' > $dirmpdconf/conf/rplaygain.conf 26 | # shairport-sync 27 | sed -i -E 's/(name = ").*/\1rAudio"/' /etc/shairport-sync.conf &> /dev/null 28 | # smb 29 | sed -i '/read only = no/ d' smbconf=/etc/samba/smb.conf &> /dev/null 30 | # upmpdcli 31 | sed -i -E -e 's/^(friendlyname = ).*/\1rAudio/ 32 | ' -e 's/(ownqueue = )./\10' /etc/upmpdcli.conf &> /dev/null 33 | 34 | # cmdline.txt 35 | cmdline=$( sed -E 's/^(.*repair=yes) .*/\1/' /boot/cmdline.txt ) 36 | if systemctl -q is-enabled localbrowser; then 37 | cmdline+=' isolcpus=3 console=tty3 quiet loglevel=0 logo.nologo vt.global_cursor_default=0' 38 | else 39 | cmdline+=' console=tty1' 40 | fi 41 | echo $cmdline > /boot/cmdline.txt 42 | # config.txt 43 | config="\ 44 | initramfs initramfs-linux.img followkernel 45 | disable_overscan=1 46 | disable_splash=1 47 | dtparam=audio=on" 48 | [[ -e /boot/kernel.img ]] && config+=" 49 | gpu_mem=32 50 | force_turbo=1 51 | gpu_mem=32 52 | hdmi_drive=2 53 | max_usb_current=1 54 | over_voltage=2" 55 | 56 | echo "$config" > /boot/config.txt 57 | # css color 58 | if [[ -e $dirsystem/color ]]; then 59 | rm $dirsystem/color 60 | $dirbash/cmd.sh color 61 | fi 62 | # lcd 63 | sed -i 's/fb1/fb0/' /etc/X11/xorg.conf.d/99-fbturbo.conf &> /dev/null 64 | # nas 65 | dirs=$( find $dirnas -mindepth 1 -maxdepth 1 -type d ) 66 | if [[ $dirs ]]; then 67 | while read dir; do 68 | umount -l "$dir" &> /dev/null 69 | rmdir "$dir" &> /dev/null 70 | done <<< $dirs 71 | fi 72 | sed -i '3,$ d' /etc/fstab 73 | 74 | systemctl -q disable bluetooth camilladsp mediamtx nfs-server powerbutton shairport-sync smb snapclient spotifyd upmpdcli &> /dev/null 75 | mv $dirdata/{addons,camilladsp,mpdconf} /tmp &> /dev/null 76 | [[ $KEEPLIBRARY ]] && mv $dirdata/{mpd,playlists,webradio} /tmp 77 | rm -rf $dirdata $dirshareddata \ 78 | /mnt/MPD/.mpdignore $dirnas/.mpdignore \ 79 | /etc/modules-load.d/{loopback,raspberrypi}.conf /etc/modprobe.d/cirrus.conf /etc/X11/xorg.conf.d/99-raspi-rotate.conf 80 | if [[ ! $KEEPNETWORK ]]; then 81 | profiles=$( ls -p /etc/netctl | grep -v / ) 82 | if [[ $profiles ]]; then 83 | while read profile; do 84 | [[ $( netctl is-enabled "$profile" ) == enabled ]] && netctl disable "$profile" 85 | rm "/etc/netctl/$profile" 86 | done <<< $profiles 87 | fi 88 | fi 89 | 90 | $dirsettings/system-datadefault.sh 91 | 92 | mv /tmp/{addons,camilladsp,mpdconf} $dirdata &> /dev/null 93 | [[ $KEEPLIBRARY ]] && mv -f /tmp/{mpd,playlists,webradio} $dirdata 94 | 95 | $dirbash/power.sh reboot 96 | -------------------------------------------------------------------------------- /srv/http/bash/settings/system-datarestore.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /srv/http/bash/common.sh 4 | 5 | [[ $1 == true ]] && libraryonly=1 6 | 7 | backupfile=$dirshm/backup.gz 8 | ! bsdtar tf "$backupfile" 2> /dev/null | grep -q -m1 ^data/system/display.json$ && exit -1 9 | # -------------------------------------------------------------------- 10 | dirconfig=$dirdata/config 11 | 12 | [[ $( mpcState ) == play ]] && $dirbash/cmd.sh playerstop 13 | [[ -e $dirmpd/listing ]] && killall cmd-list.sh 14 | mpc | grep -q ^Updating && systemctl restart mpd 15 | rm -rf $dirdata/{mpd,playlists,webradio} 16 | if [[ $libraryonly ]]; then 17 | bsdtar xpf $backupfile -C /srv/http data/{mpd,playlists,webradio} 18 | systemctl restart mpd 19 | exit 20 | # -------------------------------------------------------------------- 21 | fi 22 | find $dirmpdconf -maxdepth 1 -type l -exec rm {} \; # mpd.conf symlink 23 | bsdtar xpf $backupfile -C /srv/http 24 | dirPermissions 25 | [[ -e $dirsystem/color ]] && $dirbash/cmd.sh color 26 | partuuid=$( grep -m1 ^PARTUUID /etc/fstab | cut -d- -f1 ) 27 | for file in boot/cmdline.txt etc/fstab; do 28 | sed -i "s/PARTUUID=.*-/$partuuid-/" $dirconfig/$file 29 | done 30 | cp -rf $dirconfig/* / 31 | [[ -e $dirsystem/enable ]] && systemctl -q enable $( < $dirsystem/enable ) &> /dev/null 32 | [[ -e $dirsystem/disable ]] && systemctl -q disable $( < $dirsystem/disable ) &> /dev/null 33 | grep -q nfs-server $dirsystem/enable && $dirsettings/features.sh nfsserver 34 | name=$( < $dirsystem/hostname ) 35 | hostnamectl set-hostname $name 36 | sed -i -E 's/(name = ").*/\1'$name'"/' /etc/shairport-sync.conf 37 | sed -i -E 's/^(friendlyname = ).*/\1'$name'/' /etc/upmpdcli.conf 38 | if [[ -e $dirsystem/netctlprofile ]]; then 39 | profile=$( < $dirsystem/netctlprofile ) 40 | [[ $( netctl is-enabled "$profile" ) != enabled ]] && netctl enable "$profile" 41 | fi 42 | timedatectl set-timezone $( < $dirsystem/timezone ) 43 | [[ -e $dirsystem/crossfade ]] && mpc -q crossfade $( < $dirsystem/crossfade ) 44 | rm -rf $dirconfig $dirsystem/{crossfade,enable,disable,hostname,netctlprofile,timezone} 45 | dirs=$( ls -d $dirnas/*/ 2> /dev/null ) 46 | if [[ $dirs ]]; then 47 | while read dir; do 48 | umount -l "$dir" &> /dev/null 49 | rmdir "$dir" &> /dev/null 50 | done <<< $dirs 51 | fi 52 | mountpoints=$( grep $dirnas /etc/fstab | awk '{print $2}' ) 53 | if [[ $mountpoints ]]; then 54 | while read mountpoint; do 55 | mp=${mountpoint//\040/ } 56 | mkdir -p "$mp" 57 | chown mpd:audio "$mp" 58 | done <<< $mountpoints 59 | fi 60 | [[ -e /etc/modprobe.d/cirrus.conf ]] && touch /boot/cirrus 61 | 62 | $dirbash/power.sh reboot 63 | -------------------------------------------------------------------------------- /srv/http/bash/settings/system-mount.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /srv/http/bash/common.sh 4 | 5 | args2var "$1" 6 | 7 | ! ipOnline $IP && echo "$IP not reachable." && exit 8 | # -------------------------------------------------------------------- 9 | if [[ $PROTOCOL ]]; then 10 | mountpoint="$dirnas/$NAME" 11 | if grep -q "^$mountpoint$" <<< $( awk '{print $2}' /etc/fstab ); then 12 | echo "Name ${mountpoint/*\/} already exists" 13 | exit 14 | # -------------------------------------------------------------------- 15 | fi 16 | else # server rAudio client 17 | path=$( timeout 3 showmount --no-headers -e $IP 2> /dev/null ) 18 | [[ ${path/ *} != $dirnas ]] && echo ' Server rAudio not found.' && exit 19 | # -------------------------------------------------------------------- 20 | rserver=rserver 21 | mountpoint=$dirnas 22 | PROTOCOL=nfs 23 | SHARE=$dirnas 24 | fi 25 | share=$( sed 's|^[\\/]*||; s|\\|/|g' <<< $SHARE ) 26 | if [[ $PROTOCOL == cifs ]]; then 27 | source="//$IP/$share" 28 | options=noauto 29 | if [[ ! $USR ]]; then 30 | options+=,username=guest 31 | else 32 | options+=",username=$USR,password=$PASSWORD" 33 | fi 34 | options+=,uid=$( id -u mpd ),gid=$( id -g mpd ),iocharset=utf8 35 | else 36 | source="$IP:/$share" 37 | options=defaults,noauto,bg,soft,timeo=5 38 | fi 39 | [[ $OPTIONS ]] && options+=,$OPTIONS 40 | mountpointSet "$mountpoint" "${source// /\\040} ${mountpoint// /\\040} $PROTOCOL ${options// /\\040} 0 0" 41 | 42 | if [[ $SHAREDDATA ]]; then 43 | mv /mnt/MPD/{SD,USB} /mnt 44 | sed -i 's|/mnt/MPD/USB|/mnt/USB|' /etc/udevil/udevil.conf 45 | systemctl restart devmon@http 46 | mkdir -p $dirbackup $dirshareddata 47 | if [[ ! -e $dirshareddata/mpd ]]; then 48 | rescan=1 49 | sharedDataCopy $rserver 50 | fi 51 | sharedDataLink $rserver 52 | appendSortUnique $filesharedip $( ipAddress ) 53 | mpc -q clear 54 | systemctl restart mpd 55 | [[ $rescan ]] && $dirbash/cmd.sh "mpcupdate 56 | rescan 57 | 58 | CMD ACTION PATHMPD" 59 | pushData refresh '{ "page": "features", "shareddata": true }' 60 | fi 61 | pushRefresh system 62 | pushDirCounts nas 63 | -------------------------------------------------------------------------------- /srv/http/bash/settings/system-storage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /srv/http/bash/common.sh 4 | 5 | listItem() { # $1-icon, $2-mountpoint, $3-source, $4-mounted 6 | local apm hdapm icon info list mounted mountpoint size usf 7 | icon=$1 8 | mountpoint=$2 9 | source=$3 10 | mounted=$4 11 | if [[ $mounted == true ]]; then # timeout: limit if network shares offline 12 | size=$( timeout 1 df -H --output=used,size $mountpoint | awk '!/Used/ {print $1"B/"$2"B"}' ) 13 | [[ ${source:0:4} == /dev ]] && size+=" $( blkid -o value -s TYPE $source )" 14 | fi 15 | list=' 16 | "icon" : "'$icon'" 17 | , "mountpoint" : "'$( quoteEscape $mountpoint )'" 18 | , "size" : "'$size'" 19 | , "source" : "'$source'"' 20 | echo ", { 21 | $list 22 | }" 23 | } 24 | # sd 25 | mount | grep -q -m1 'mmcblk0p2 on /' && list+=$( listItem microsd /mnt/MPD/SD /dev/mmcblk0p2 true ) 26 | # usb 27 | usb=$( ls /dev/sd* 2> /dev/null ) 28 | if [[ $usb ]]; then 29 | while read source; do 30 | type=$( blkid -o value -s TYPE $source ) 31 | [[ ! $type ]] && continue 32 | 33 | mountpoint=$( df -l --output=target $source | tail -1 ) 34 | if [[ $mountpoint != /dev ]]; then 35 | mounted=true 36 | else 37 | mounted=false 38 | mountpoint="$dirusb/$( lsblk -no label $source )" 39 | fi 40 | [[ $mountpoint == $mountpointprev ]] && continue 41 | 42 | mountpointprev=$mountpoint 43 | list+=$( listItem usbdrive "$mountpoint" "$source" $mounted ) 44 | done <<< $usb 45 | fi 46 | # nas 47 | nas=$( grep -E '/mnt/MPD/NAS|/srv/http/data' /etc/fstab ) 48 | if [[ $nas ]]; then 49 | nas=$( awk '{print $1"^"$2}' <<< $nas | sed 's/\\040/ /g' | sort ) 50 | while read line; do 51 | source=${line/^*} 52 | mountpoint=${line/*^} 53 | mountpoint -q "$mountpoint" && mounted=true || mounted=false 54 | list+=$( listItem networks "$mountpoint" "$source" $mounted ) 55 | done <<< $nas 56 | fi 57 | echo "[ ${list:1} ]" 58 | -------------------------------------------------------------------------------- /srv/http/bash/shairport.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # shairport.service > this: 4 | # - /tmp/shairport-sync-metadata emits data 5 | 6 | . /srv/http/bash/common.sh 7 | 8 | dirairplay=$dirshm/airplay 9 | rm -f $dirairplay/{elapsed,pause,start} 10 | echo stop > $dirairplay/state 11 | 12 | pause() { 13 | echo pause > $dirairplay/state 14 | touch $dirairplay/pause 15 | elapsed=$( < $dirairplay/elapsed ) 16 | pushData airplay '{ "state": "pause", "elapsed": '$elapsed' }' 17 | } 18 | play() { 19 | [[ -e $dirairplay/pause ]] && rm $dirairplay/{elapsed,pause} 20 | echo play > $dirairplay/state 21 | $dirbash/status-push.sh 22 | } 23 | 24 | cat /tmp/shairport-sync-metadata | while read line; do 25 | [[ $line =~ 'encoding="base64"' || $line =~ ''.*'' ]] && continue # skip: no value / double codes 26 | 27 | ##### code - hex matched 28 | hex=$( sed -E 's|.*code>(.*) [next line] 30 | case $hex in 31 | 61736172|61736161 ) code=Artist && continue;; # asar|asaa 32 | 6d696e6d ) code=Title && continue;; # minm 33 | 6173616c ) code=Album && continue;; # asal 34 | 50494354 ) code=coverart && continue;; # PICT 35 | 70726772 ) code=progress && continue;; # prgr 36 | 63617073 ) code=state && continue;; # caps 37 | esac 38 | fi 39 | 40 | # no line with code found yet > [next line] 41 | [[ ! $code ]] && continue 42 | 43 | ##### value - base64 decode 44 | base64=$( tr -d '\000' <<< ${line/<*} ) # remove tags and null bytes 45 | # null or not base64 string - reset code= > [next line] 46 | if [[ ! $base64 || ! $base64 =~ ^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$ ]]; then 47 | code= 48 | continue 49 | fi 50 | 51 | if [[ $code == coverart ]]; then 52 | base64 -d <<< $base64 > $dirairplay/coverart.jpg 53 | pushData airplay '{ "coverart": "/data/shm/airplay/coverart.jpg" }' 54 | elif [[ $code == state ]]; then 55 | if [[ $base64 == 'AQ==' ]]; then # 1 56 | play 57 | elif [[ $base64 == 'Ag==' ]]; then # 2 58 | pause 59 | fi 60 | else 61 | data=$( base64 -d <<< $base64 2> /dev/null ) 62 | if [[ $code == progress ]]; then # format: start/elapsed/end @44100/s 63 | play 64 | progress=( ${data//\// } ) 65 | start=${progress[0]} 66 | current=${progress[1]} 67 | end=${progress[2]} 68 | elapsedms=$( awk 'BEGIN { printf "%.0f", '$(( current - start ))/44.1' }' ) 69 | elapsed=$(( ( elapsedms + 500 ) / 1000 )) 70 | Time=$(( ( end - start + 22050 ) / 44100 )) 71 | pushData airplay '{ "elapsed": '$elapsed', "Time": '$Time' }' 72 | timestamp=$( date +%s%3N ) 73 | starttime=$(( timestamp - elapsedms )) 74 | echo $elapsed > $dirairplay/elapsed 75 | echo $starttime > $dirairplay/start 76 | echo $timestamp > $dirairplay/timestamp 77 | echo $Time > $dirairplay/Time 78 | $dirbash/status-push.sh 79 | else 80 | echo $data > $dirairplay/$code 81 | pushData airplay '{ "'$code'": "'$( quoteEscape $data )'" }' 82 | fi 83 | fi 84 | code= # reset after $code + $data were set 85 | done 86 | 87 | [[ ! -e $dirairplay/pause ]] && pause # fix - if no state emits (script always exits) 88 | -------------------------------------------------------------------------------- /srv/http/bash/smbdfree.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # for /etc/samba/smb.conf 4 | df | grep /mnt/MPD/USB | sort | head -1 | awk '{print $2" "$4}' 5 | -------------------------------------------------------------------------------- /srv/http/bash/snapclient.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # as server - features.sh > this: 4 | # - force clients stop on disabled 5 | # as client - main.js > this: 6 | # - connect 7 | # - disconnect 8 | # as client + server - cmd.sh 9 | # - play > connect 10 | # - stop > disconnect 11 | 12 | 13 | . /srv/http/bash/common.sh 14 | 15 | if [[ $1 == stop ]]; then 16 | systemctl stop snapclient 17 | $dirbash/cmd.sh playerstop 18 | rm -f $dirshm/snapserverip 19 | else 20 | notify snapcast SnapServer "Connect $1 ..." 21 | echo $1 > $dirshm/snapserverip 22 | echo snapcast > $dirshm/player 23 | systemctl start snapclient 24 | fi 25 | $dirbash/status-push.sh 26 | -------------------------------------------------------------------------------- /srv/http/bash/spotifyd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # spotifyd.conf > this: 4 | # - spotifyd 'onevent' hook 5 | # env var: ($PLAYER_EVENT still not consistent - used for detect emitted events only) 6 | # $PLAYER_EVENT: load/preload/change/start/play/pause/volumeset 7 | # $TRACK_ID 8 | # $PLAY_REQUEST_ID 9 | # $POSITION_MS 10 | # $DURATION_MS 11 | # $VOLUME 12 | 13 | . /srv/http/bash/common.sh 14 | 15 | ##### start 16 | if ! playerActive spotify; then 17 | echo spotify > $dirshm/player 18 | $dirbash/cmd.sh playerstart 19 | exit 20 | # -------------------------------------------------------------------- 21 | fi 22 | [[ $PLAYER_EVENT == volumeset ]] && volumeGet push 23 | 24 | dirspotify=$dirshm/spotify 25 | for key in elapsed expire start state status token; do # var fileKEY=$dirspotify/KEY 26 | printf -v file$key '%s' $dirspotify/$key 27 | done 28 | # token 29 | if [[ -e $fileexpire && $( < $fileexpire ) > $( date +%s ) ]]; then 30 | token=$( < $filetoken ) 31 | else 32 | . $dirsystem/spotifykey # base64client, refreshtoken 33 | token=$( curl -s -X POST https://accounts.spotify.com/api/token \ 34 | -H "Authorization: Basic $base64client" \ 35 | -d grant_type=refresh_token \ 36 | -d refresh_token=$refreshtoken \ 37 | | grep access_token \ 38 | | cut -d'"' -f4 ) 39 | if [[ ! $token ]]; then 40 | notify spotify Spotify 'Access token renewal failed.' 41 | exit 42 | # -------------------------------------------------------------------- 43 | fi 44 | echo $token > $filetoken 45 | echo $(( $( date +%s ) + 3550 )) > $fileexpire # 10s before 3600s 46 | fi 47 | # data 48 | readarray -t status <<< $( curl -s -X GET https://api.spotify.com/v1/me/player/currently-playing \ 49 | -H "Authorization: Bearer $token" \ 50 | | jq '.item.album.name, 51 | .item.artists[0].name, 52 | .item.album.images[0].url, 53 | .is_playing, 54 | .item.duration_ms, 55 | .item.name, 56 | .progress_ms, 57 | .timestamp' ) # not -r: 1-to keep escaped characters 2-already quoted 58 | [[ ${status[3]} == true ]] && state=play || state=pause 59 | Time=$(( ( ${status[4]} + 500 ) / 1000 )) 60 | cat << EOF > $filestatus 61 | , "Album" : ${status[0]} 62 | , "Artist" : ${status[1]} 63 | , "coverart" : ${status[2]} 64 | , "sampling" : "48 kHz 320 kbit/s • Spotify" 65 | , "state" : "$state" 66 | , "Time" : $Time 67 | , "Title" : ${status[5]} 68 | EOF 69 | progress=${status[6]} 70 | timestamp=${status[7]} 71 | diff=$(( $( date +%s%3N ) - timestamp )) 72 | elapsed=$(( ( progress + 500 ) / 1000 )) 73 | start=$(( ( timestamp + diff - progress + 500 ) / 1000 )) 74 | cat << EOF > $filestate 75 | elapsed=$elapsed 76 | start=$start 77 | state=$state 78 | Time=$Time 79 | EOF 80 | 81 | $dirbash/status-push.sh 82 | -------------------------------------------------------------------------------- /srv/http/bash/startup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /srv/http/bash/common.sh 4 | 5 | wlandev=$( $dirsettings/networks.sh wlandevice ) 6 | 7 | # pre-configure >>>----------------------------------------------------------- 8 | if [[ -e /boot/expand ]]; then # run once 9 | id0=$( < /etc/machine-id ) 10 | rm /etc/machine-id 11 | systemd-machine-id-setup 12 | id1=$( < /etc/machine-id ) 13 | mv /var/log/journal/{$id0,$id1} 14 | rm /boot/expand 15 | partition=$( mount | grep ' on / ' | cut -d' ' -f1 ) 16 | [[ ${partition:0:7} == /dev/sd ]] && dev=${partition:0:-1} || dev=${partition:0:-2} 17 | if (( $( sfdisk -F $dev | awk 'NR==1{print $6}' ) != 0 )); then 18 | echo -e "d\n\nn\n\n\n\n\nw" | fdisk $dev &>/dev/null 19 | partprobe $dev 20 | resize2fs $partition 21 | fi 22 | revision=$( grep ^Revision /proc/cpuinfo ) 23 | if [[ ${revision: -3:2} == 12 ]]; then # zero 2 24 | systemctl enable getty@tty1 25 | systemctl disable --now bootsplash localbrowser 26 | sed -i 's/tty3 .*/tty1/' /boot/cmdline.txt 27 | mv /usr/bin/firefox{,.backup} 28 | fi 29 | fi 30 | 31 | backupfile=$( ls /boot/*.gz 2> /dev/null | head -1 ) 32 | if [[ -e $backupfile ]]; then 33 | mv "$backupfile" $dirshm/backup.gz 34 | $dirsettings/system-datarestore.sh 35 | fi 36 | 37 | if [[ $wlandev ]]; then 38 | if [[ -e /boot/wifi ]]; then 39 | ssid=$( getVar ESSID /boot/wifi ) 40 | sed -E -e '/^#|^\s*$/ d 41 | ' -e "s/\r//; s/^(Interface=).*/\1$wlandev/ 42 | " /boot/wifi > "/etc/netctl/$ssid" 43 | rm -f /boot/{accesspoint,wifi} $dirsystem/ap 44 | $dirsettings/networks.sh "profileconnect 45 | $ssid 46 | CMD ESSID" 47 | elif [[ -e /boot/accesspoint ]]; then 48 | mv -f /boot/accesspoint $dirsystem/ap 49 | fi 50 | fi 51 | # pre-configure <<<----------------------------------------------------------- 52 | 53 | logoLcdOled 54 | 55 | [[ -e $dirsystem/soundprofile ]] && $dirsettings/system.sh soundprofileset 56 | 57 | dirbacklight=/sys/class/backlight/rpi_backlight 58 | if [[ -d $dirbacklight ]]; then 59 | chmod 666 $dirbacklight/{brightness,bl_power} 60 | [[ -e $dirsystem/brightness ]] && cat $dirsystem/brightness > $dirbacklight/brightness 61 | fi 62 | 63 | mkdir -p $dirshm/{airplay,embedded,spotify,local,online,sampling,webradio} 64 | chmod -R 777 $dirshm 65 | chown -R http:http $dirshm 66 | echo mpd > $dirshm/player 67 | 68 | lsmod | grep -q -m1 brcmfmac && touch $dirshm/onboardwlan # initial status 69 | 70 | netctllist=$( netctl list ) 71 | if [[ -e $dirsystem/ap ]]; then 72 | ap=1 73 | else # if no connections, start accesspoint 74 | [[ $netctllist ]] && sec=30 || sec=5 # wlan || lan 75 | for (( i=0; i < $sec; i++ )); do # wait for connection 76 | ipaddress=$( ipAddress ) 77 | [[ $ipaddress ]] && break || sleep 1 78 | done 79 | if [[ $ipaddress ]]; then 80 | readarray -t lines <<< $( grep $dirnas /etc/fstab ) 81 | if [[ $lines ]]; then 82 | for line in "${lines[@]}"; do 83 | mp=$( awk '{print $2}' <<< $line ) 84 | for i in {1..10}; do 85 | mount "${mp//\\040/ }" && break || sleep 2 86 | done 87 | ! mountpoint -q "$mp" && notify networks NAS "Mount failed: $mp" 10000 88 | done 89 | fi 90 | if systemctl -q is-active nfs-server; then 91 | if [[ -s $filesharedip ]]; then 92 | sharedip=$( < $filesharedip ) 93 | for ip in $sharedip; do 94 | notify -ip $ip networks 'Server rAudio' Online 95 | done 96 | fi 97 | appendSortUnique $ipaddress $filesharedip 98 | fi 99 | if [[ $partition ]] && ipOnline 8.8.8.8; then 100 | $dirsettings/system.sh 'timezone 101 | auto 102 | CMD TIMEZONE' 103 | fi 104 | else 105 | if [[ $wlandev && ! $ap ]]; then 106 | if [[ $netctllist ]]; then 107 | [[ ! -e $dirsystem/wlannoap ]] && ap=1 108 | else 109 | ap=1 110 | fi 111 | [[ $ap ]] && touch $dirshm/apstartup 112 | fi 113 | fi 114 | fi 115 | 116 | [[ $ap ]] && $dirsettings/features.sh iwctlap 117 | 118 | if [[ -e $dirsystem/btreceiver ]]; then 119 | mac=$( < $dirsystem/btreceiver ) 120 | rm $dirsystem/btreceiver 121 | $dirsettings/networks-bluetooth.sh connect $mac 122 | fi 123 | 124 | if [[ -e $dirshm/btreceiver && -e $dirsystem/camilladsp ]]; then 125 | $dirsettings/camilla-bluetooth.sh btreceiver 126 | else # start mpd.service if not started by networks-bluetooth.sh 127 | $dirsettings/player-conf.sh 128 | fi 129 | 130 | if [[ -e $dirsystem/volumelimit ]]; then 131 | volumeLimit startup 132 | fi 133 | 134 | # after all sources connected ........................................................ 135 | if [[ ! -e $dirmpd/mpd.db || -e $dirmpd/updating ]]; then 136 | $dirbash/cmd.sh mpcupdate 137 | elif [[ -e $dirmpd/listing ]]; then 138 | $dirbash/cmd-list.sh &> /dev/null & 139 | fi 140 | # usb wlan || no wlan || not ap + not connected 141 | if (( $( rfkill | grep -c wlan ) > 1 )) || [[ ! $netctllist && ! $ap ]]; then 142 | rmmod brcmfmac_wcc brcmfmac &> /dev/null 143 | fi 144 | 145 | touch $dirshm/startup 146 | 147 | if grep -qs startup=true $dirsystem/autoplay.conf; then 148 | mpcPlayback play 149 | fi 150 | if [[ -e /boot/startup.sh ]]; then 151 | /boot/startup.sh 152 | fi 153 | 154 | udevil clean 155 | lsblk -no path,vendor,model | grep -v ' $' > $dirshm/lsblkusb 156 | if [[ ! -e $diraddons/update ]]; then 157 | data=$( curl -sfL https://github.com/rern/rAudio-addons/raw/main/addonslist.json ) 158 | if [[ $? == 0 ]]; then 159 | echo "$data" > $diraddons/addonslist.json 160 | rversion=$( sed -n '/"r1"/,/"version"/ {/version/!d; s/"//g; s/.*: //; p}' <<< $data ) 161 | if [[ $rversion != $( < $diraddons/r1 ) ]]; then 162 | touch $diraddons/update 163 | pushData option '{ "addons": true }' 164 | fi 165 | fi 166 | fi 167 | -------------------------------------------------------------------------------- /srv/http/bash/startx.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | /srv/http/bash/settings/features.sh startx 4 | -------------------------------------------------------------------------------- /srv/http/bash/status-bluetooth.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # RPi as renderer - status.sh > this: 4 | # - retreive current status from dbus 5 | 6 | . /srv/http/bash/common.sh 7 | 8 | dest=$( < $dirshm/bluetoothdest ) 9 | data=$( dbus-send \ 10 | --system \ 11 | --type=method_call \ 12 | --print-reply \ 13 | --dest=org.bluez $dest \ 14 | org.freedesktop.DBus.Properties.GetAll \ 15 | string:org.bluez.MediaPlayer1 \ 16 | | grep -E -A1 'string.*Status|string.*Title|string.*Album|string.*Artist|string.*Duration|string.*Position' \ 17 | | sed -E 's/^\s*string "|^\s*variant\s*string "|^\s*variant\s*uint32 //; s/"$//' \ 18 | | tr '\n' ^ \ 19 | | sed 's/\^--\^/\n/g; s/\^$//' ) 20 | Artist=$( grep ^Artist <<< $data | cut -d^ -f2 ) 21 | Title=$( grep ^Title <<< $data | cut -d^ -f2 ) 22 | Album=$( grep ^Album <<< $data | cut -d^ -f2 ) 23 | Position=$( grep ^Position <<< $data | cut -d^ -f2 ) 24 | Duration=$( grep ^Duration <<< $data | cut -d^ -f2 ) 25 | Status=$( grep ^Status <<< $data | cut -d^ -f2 ) 26 | case $Status in 27 | paused ) state=pause;; 28 | playing ) state=play;; 29 | stopped ) state=stop;; 30 | esac 31 | 32 | name=$( alphaNumeric $Artist$Album ) 33 | onlinefile=$( ls $dirshm/online/$name.* 2> /dev/null ) # jpg / png 34 | if [[ -e $onlinefile ]]; then 35 | coverart="${onlinefile:9}" 36 | else 37 | $dirbash/status-coverartonline.sh "cmd 38 | $Artist 39 | $Album 40 | CMD ARTIST ALBUM" &> /dev/null & 41 | fi 42 | [[ ! $Position ]] && elapsed=false || elapsed=$( calc 0 $Position/1000 ) 43 | [[ ! $Duration ]] && Time=false || Time=$( calc 0 $Duration/1000 ) 44 | timestamp=$( date +%s%3N ) 45 | 46 | data=' 47 | , "Artist" : "'$Artist'" 48 | , "Title" : "'$Title'" 49 | , "Album" : "'$Album'" 50 | , "coverart" : "'$coverart'" 51 | , "elapsed" : '$elapsed' 52 | , "sampling" : "Bluetooth" 53 | , "state" : "'$state'" 54 | , "Time" : '$Time' 55 | , "timestamp" : '$timestamp 56 | 57 | echo "$data" 58 | -------------------------------------------------------------------------------- /srv/http/bash/status-coverart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /srv/http/bash/common.sh 4 | 5 | args2var "$1" 6 | 7 | if playerActive upnp; then 8 | upnp=1 9 | name=$( alphaNumeric $ARTIST$ALBUM ) 10 | localfile=$dirshm/local/${name,,} 11 | else 12 | filename=$( basename "$FILE" ) 13 | path="/mnt/MPD/$FILE" 14 | [[ -f "$path" ]] && path=$( dirname "$path" ) 15 | localfile=$dirshm/local/$( alphaNumeric $path ) 16 | fi 17 | # found cover file 18 | if [[ -f $localfile ]]; then 19 | cat $localfile 20 | exit 21 | # -------------------------------------------------------------------- 22 | fi 23 | # found embedded 24 | embeddedname=$( alphaNumeric $filename ).jpg 25 | embeddedfile=$dirshm/embedded/$embeddedname 26 | [[ -f "$embeddedfile" ]] && echo ${embeddedfile:9} && exit 27 | # -------------------------------------------------------------------- 28 | # found online 29 | covername=$( alphaNumeric $ARTIST$ALBUM ) 30 | onlinefile=$( ls -X $dirshm/online/${covername,,}.{jpg,png} 2> /dev/null | head -1 ) 31 | [[ -f $onlinefile ]] && echo ${onlinefile:9} && exit 32 | # -------------------------------------------------------------------- 33 | ##### cover file 34 | [[ $upnp ]] && coverfile=$( $dirbash/status-coverartupnp.py ) || coverfile=$( coverFileGet "$path" ) 35 | if [[ $coverfile ]]; then 36 | echo "$coverfile" | tee $localfile 37 | $dirbash/cmd.sh coverfileslimit 38 | exit 39 | # -------------------------------------------------------------------- 40 | fi 41 | ##### embedded 42 | kid3-cli -c "cd \"$path\"" \ 43 | -c "select \"$filename\"" \ 44 | -c "get picture:$embeddedfile" &> /dev/null # suppress '1 space' stdout 45 | if [[ -f $embeddedfile ]]; then 46 | echo ${embeddedfile:9} 47 | exit 48 | # -------------------------------------------------------------------- 49 | fi 50 | [[ ! $ARTIST || ! $ALBUM ]] && exit 51 | # -------------------------------------------------------------------- 52 | ##### online 53 | $dirbash/status-coverartonline.sh "cmd 54 | $ARTIST 55 | $ALBUM 56 | CMD ARTIST ALBUM" &> /dev/null & 57 | -------------------------------------------------------------------------------- /srv/http/bash/status-coverartonline.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /srv/http/bash/common.sh 4 | 5 | args2var "$1" 6 | 7 | ### 1 - lastfm ################################################## 8 | if [[ $MODE != webradio || -e $dirshm/radio ]]; then # not webradio || radioparadise / radiofrance 9 | param="album=${ALBUM//&/ and }" 10 | method='method=album.getInfo' 11 | else 12 | artist_title=1 13 | param="track=${ALBUM//&/ and }" # $ALBUM = track 14 | method='method=track.getInfo' 15 | fi 16 | apikey=$( grep -m1 apikeylastfm /srv/http/assets/js/main.js | cut -d"'" -f2 ) 17 | data=$( curl -sfG -m 5 \ 18 | --data-urlencode "artist=$ARTIST" \ 19 | --data-urlencode "$param" \ 20 | --data "$method" \ 21 | --data "api_key=$apikey" \ 22 | --data "format=json" \ 23 | http://ws.audioscrobbler.com/2.0 ) 24 | if [[ $? == 0 && $data ]]; then 25 | [[ $artist_title ]] && album=$( jq -r '.track.album // empty' <<< $data ) || album=$( jq -r '.album // empty' <<< $data ) 26 | [[ $album ]] && image=$( jq -r '.image // empty' <<< $album ) 27 | if [[ $image ]]; then 28 | extralarge=$( jq -r '.[3]."#text" // empty' <<< $image ) 29 | [[ $extralarge ]] && url=$( sed 's|/300x300/|/_/|' <<< $extralarge ) # get larger size than 300x300 30 | fi 31 | fi 32 | ### 2 - coverartarchive.org ##################################### 33 | if [[ ! $url ]]; then 34 | mbid=$( jq -r '.mbid // empty' <<< $album ) 35 | if [[ $mbid ]]; then 36 | imgdata=$( curl -sfL -m 10 https://coverartarchive.org/release/$mbid ) 37 | [[ $? != 0 ]] && exit 38 | # -------------------------------------------------------------------- 39 | url=$( jq -r '.images[0].image // empty' <<< $imgdata ) 40 | fi 41 | fi 42 | if [[ $DEBUG ]]; then 43 | [[ ! $url ]] && url="(Not found: $ARTIST - $ALBUM)" 44 | echo coverart: $url 45 | exit 46 | # -------------------------------------------------------------------- 47 | fi 48 | [[ ! $url ]] && exit 49 | # -------------------------------------------------------------------- 50 | ext=${url/*.} 51 | if [[ $DISCID ]]; then 52 | cover=$diraudiocd/$DISCID.$ext 53 | else 54 | [[ $MODE ]] && prefix=$MODE || prefix=online 55 | name=$( alphaNumeric $ARTIST$ALBUM ) 56 | cover=$dirshm/$prefix/${name,,}.$ext 57 | fi 58 | curl -sfL $url -o $cover 59 | [[ ${cover:0:4} == /srv ]] && cover=${cover:9} 60 | pushData cover '{ "cover": "'$cover'" }' 61 | -------------------------------------------------------------------------------- /srv/http/bash/status-coverartupnp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import socket 4 | import upnpp 5 | 6 | device = socket.gethostname() +'-UPnP/AV' 7 | srv = upnpp.findTypedService( device, 'avtransport', True ) # AVTransport service 8 | if srv: 9 | retdata = upnpp.runaction( srv, 'GetMediaInfo', ['0'] ) 10 | metadata = retdata[ 'CurrentURIMetaData' ] 11 | if metadata: 12 | dirc = upnpp.UPnPDirContent() 13 | dirc.parse( metadata ) 14 | if dirc.m_items.size(): 15 | mprops = dirc.m_items[0].m_props 16 | if 'upnp:albumArtURI' in mprops: print( mprops['upnp:albumArtURI'] ) 17 | -------------------------------------------------------------------------------- /srv/http/bash/status-dab.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /srv/http/bash/common.sh 4 | 5 | . $dirshm/radio 6 | album='DAB Radio' 7 | artist=$( head -1 $file ) 8 | filelabel=$dirshm/webradio/DABlabel.txt 9 | filecover=$dirshm/webradio/DABslide.jpg 10 | filetitle=$dirshm/webradio/DABtitle 11 | 12 | while true; do 13 | # title 14 | label=$( awk NF $filelabel ) 15 | if [[ ! $label ]] || cmp -s $filelabel $filetitle; then 16 | [[ ! $label ]] && pushData mpdradio '{ "Title": "" }' 17 | sleep 10 18 | continue 19 | fi 20 | 21 | cp -f $filelabel $filetitle 22 | title=$( < $filetitle ) 23 | elapsed=$( mpcElapsed ) 24 | data=' 25 | "Album" : "'$album'" 26 | , "Artist" : "'$artist'" 27 | , "elapsed" : '$elapsed' 28 | , "Title" : "'$title'"' 29 | pushData mpdradio "{ $data }" 30 | # coverart 31 | coverart= 32 | if [[ $( awk NF $filecover ) ]]; then 33 | name=$( alphaNumeric $title ) 34 | coverfile=/srv/http/data/shm/webradio/$name.jpg 35 | if ! cmp -s $filecover $coverfile; then # change later than title or multiple covers 36 | cp -f $filecover $coverfile 37 | cover="${coverfile:9}" 38 | sed -i -E "s/^(coverart=).*/\1$cover/" $dirshm/status 39 | fi 40 | fi 41 | pushData cover '{ "cover": "'$cover'" }' 42 | radioStatusFile 43 | sleep 10 44 | done 45 | -------------------------------------------------------------------------------- /srv/http/bash/status-push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | [[ -e /dev/shm/usbdac_rules ]] && exit # debounce usbdac.rules 4 | # -------------------------------------------------------------------- 5 | . /srv/http/bash/common.sh 6 | 7 | isChanged() { 8 | for k in $@; do 9 | prev=$( sed -n -E '/^'$k'/ {s/.*="*|"*$//g; p}' <<< $statusprev ) 10 | [[ $prev != ${!k} ]] && return 0 11 | done 12 | } 13 | 14 | killProcess statuspush 15 | echo $$ > $dirshm/pidstatuspush 16 | 17 | if [[ $1 == statusradio ]]; then # from status-radio.sh radioStatusFile 18 | state=play 19 | statusradio=1 20 | else 21 | status=$( $dirbash/status.sh | jq ) 22 | for k in Artist Album Composer Conductor elapsed file player station state Time timestamp Title volume webradio; do 23 | filter+='|^ "'$k'"' 24 | done 25 | statuslines=$( grep -E "${filter:1}" <<< $status ) 26 | statusnew=$( sed -E 's/^ *"|,$//g; s/" *: */=/' <<< $statuslines | tee $dirshm/statusnew ) 27 | statusprev=$( cat $dirshm/status 2> /dev/null ) 28 | . <( echo "$statusnew" ) 29 | isChanged Artist Title Album && trackchanged=1 30 | if [[ $webradio == true ]]; then 31 | [[ ! $trackchanged && $state == play ]] && exit 32 | # -------------------------------------------------------------------- 33 | else 34 | isChanged state elapsed && statuschanged=1 35 | [[ ! $trackchanged && ! $statuschanged ]] && exit 36 | # -------------------------------------------------------------------- 37 | fi 38 | ######## 39 | pushData mpdplayer "$status" 40 | if [[ -e $dirsystem/scrobble ]]; then 41 | cp -f $dirshm/status{,prev} 42 | timestampnew=$( grep ^timestamp <<< $statusnew | cut -d= -f2 ) 43 | fi 44 | mv -f $dirshm/status{new,} 45 | fi 46 | 47 | if systemctl -q is-active localbrowser && grep -q onwhileplay=true $dirsystem/localbrowser.conf; then 48 | export DISPLAY=:0 49 | [[ $( mpcState ) == play ]] && xset -dpms || xset +dpms 50 | fi 51 | 52 | clientip=$( snapclientIP ) 53 | if [[ $clientip ]]; then 54 | status=$( $dirbash/status.sh snapclient ) 55 | status='{ '${status/,}' }' 56 | for ip in $clientip; do 57 | pushWebsocket $ip mpdplayer $status 58 | done 59 | fi 60 | 61 | if [[ -e $dirsystem/lcdchar ]]; then 62 | [[ ! $statusradio ]] && jq <<< "{ ${statuslines%,} }" > $dirshm/status.json # remove trailing , 63 | systemctl restart lcdchar 64 | fi 65 | 66 | [[ $state == play ]] && start_stop=start || start_stop=stop 67 | [[ -e $dirsystem/mpdoled ]] && systemctl $start_stop mpd_oled 68 | [[ -e $dirsystem/vuled || -e $dirsystem/vumeter ]] && systemctl $start_stop cava 69 | [[ -e $dirsystem/vumeter && $state != play ]] && pushData vumeter '{ "val": 0 }' 70 | 71 | [[ -e $dirsystem/librandom && $webradio == false ]] && $dirbash/cmd.sh pladdrandom & 72 | 73 | [[ ! -e $dirsystem/scrobble ]] && exit 74 | # -------------------------------------------------------------------- 75 | [[ ! $trackchanged && ! -e $dirshm/elapsed ]] && exit # track changed || prev/next/stop 76 | # -------------------------------------------------------------------- 77 | . $dirshm/statusprev 78 | [[ $state == stop || $webradio == true || ! $Artist || ! $Title || $Time -lt 30 ]] && exit 79 | # -------------------------------------------------------------------- 80 | if [[ $player != mpd ]]; then 81 | ! grep -q $player=true $dirsystem/scrobble.conf && exit 82 | # -------------------------------------------------------------------- 83 | if [[ $state =~ ^(play|pause)$ ]]; then # renderers prev/next 84 | elapsed=$(( ( timestampnew - timestamp ) / 1000 )) 85 | (( $elapsed < $Time )) && echo $elapsed > $dirshm/elapsed 86 | fi 87 | fi 88 | if [[ -e $dirshm/elapsed ]];then 89 | elapsed=$( < $dirshm/elapsed ) 90 | rm $dirshm/elapsed 91 | (( $elapsed < 240 && $elapsed < $(( Time / 2 )) )) && exit 92 | # -------------------------------------------------------------------- 93 | fi 94 | $dirbash/scrobble.sh "cmd 95 | $Artist 96 | $Title 97 | CMD ARTIST TITLE"&> /dev/null & 98 | -------------------------------------------------------------------------------- /srv/http/bash/status-radio.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /srv/http/bash/common.sh 4 | 5 | . $dirshm/radio 6 | id=$( basename ${file/-*} ) 7 | [[ ${id:0:13} == francemusique ]] && id=${id:13} 8 | [[ ! $id ]] && id=francemusique 9 | 10 | case $id in 11 | flac ) id=0;; 12 | mellow ) id=1;; 13 | rock ) id=2;; 14 | global ) id=3;; 15 | fip ) id=7;; # FIP 16 | fipelectro ) id=74;; # Electro 17 | fipgroove ) id=66;; # Groove 18 | fiphiphop ) id=95;; # Hip-Hop 19 | fipjazz ) id=65;; # Jazz 20 | fipmetal ) id=77;; # Metal 21 | fipnouveautes ) id=70;; # Nouveautés 22 | fippop ) id=78;; # Pop 23 | fipreggae ) id=71;; # Reggae 24 | fiprock ) id=64;; # Rock 25 | fipworld ) id=69;; # Monde 26 | 27 | francemusique ) id=4;; # France Musique 28 | baroque ) id=408;; # La Baroque 29 | classiqueplus ) id=402;; # Classique Plus 30 | concertsradiofrance ) id=403;; # Concerts Radio France 31 | easyclassique ) id=401;; # Classique Easy 32 | labo ) id=407;; # Musique de Films 33 | lacontemporaine ) id=406;; # La Contemporaine 34 | lajazz ) id=405;; # La Jazz 35 | ocoramonde ) id=404;; # Ocora Musiques du Monde 36 | opera ) id=409;; # Opéra 37 | esac 38 | 39 | i=0 40 | metadataGet() { 41 | if [[ $id < 4 ]]; then 42 | radioparadise=1 43 | icon=radioparadise 44 | json=$( curl -sGk -m 5 --data-urlencode "chan=$id" https://api.radioparadise.com/api/now_playing ) 45 | else 46 | icon=radiofrance 47 | if [[ $id == 95 ]]; then # openapi: only needed by hiphop (no coverart for openapi) 48 | hiphop=1 49 | query='{ "query": "{ live( station: FIP_HIP_HOP ) { song { end track { title albumTitle mainArtists } } } }" }' 50 | json=$( curl -s 'https://openapi.radiofrance.fr/v1/graphql' \ 51 | -H 'Accept-Encoding: gzip, deflate, br' \ 52 | -H 'Content-Type: application/json' \ 53 | -H 'Accept: application/json' \ 54 | -H 'Connection: keep-alive' \ 55 | -H 'DNT: 1' \ 56 | -H 'Origin: https://openapi.radiofrance.fr' \ 57 | -H "x-token: 0390600a-5407-4e86-b439-24e5d48427dc" \ 58 | --compressed \ 59 | --data-binary "$query" ) 60 | else # api: current until switched to openapi ( except hophop) 61 | json=$( curl -sGk -m 5 https://api.radiofrance.fr/livemeta/pull/$id ) 62 | fi 63 | fi 64 | if [[ ! $json || ${json:0:1} != '{' || $json == *,\"error\":* ]]; then 65 | (( i++ )) 66 | if [[ $i == 1 ]]; then 67 | notify "$icon blink" Metadata 'Retry ...' 68 | pushData mpdradio '{ "Artist": "", "Title": "", "Album": "" }' 69 | elif [[ $i == 10 ]]; then 70 | notify $icon Metadata 'Not available' 71 | systemctl stop radio 72 | exit 73 | # -------------------------------------------------------------------- 74 | fi 75 | sleep 1 76 | metadataGet 77 | return 78 | fi 79 | 80 | if [[ $radioparadise ]]; then 81 | readarray -t metadata <<< $( jq -r .artist,.title,.album,.cover,.time <<< $json | sed 's/^null$//' ) 82 | countdown=${metadata[4]} # countdown 83 | else 84 | if [[ $hiphop ]]; then 85 | song=$( jq -r '.data.live.song // empty' <<< $json ) 86 | if [[ ! $song ]]; then 87 | sleep 5 88 | metadataGet 89 | return 90 | fi 91 | 92 | track=$( jq .track <<< $song ) 93 | artists=$( jq -r '.mainArtists[] // empty' <<< $track ) 94 | readarray -t metadata <<< "\ 95 | ${artists//$'\n'/, } 96 | $( jq -r .title <<< $track ) 97 | $( jq -r .albumTitle <<< $track )" 98 | end=$( jq -r .end <<< $song ) 99 | else 100 | levels=$( jq '.levels[0] // empty' <<< $json ) 101 | position=$( jq .position <<< $levels ) 102 | item=$( jq .items[$position] <<< $levels ) 103 | step=$( jq .steps[$item] <<< $json ) 104 | readarray -t metadata <<< $( jq -r .authors,.title,.titreAlbum,.visual <<< $step | sed 's/^null$//' ) 105 | end=$( jq -r .end <<< $step ) 106 | fi 107 | now=$( date +%s ) 108 | countdown=$(( end - now )) 109 | fi 110 | dataprev="$artist $title $album" 111 | artist=$( quoteEscape ${metadata[0]} ) 112 | title=$( quoteEscape ${metadata[1]} ) 113 | album=$( quoteEscape ${metadata[2]} ) 114 | coverurl=${metadata[3]} 115 | 116 | if [[ ! $title || "$artist $title $album" == $dataprev ]]; then 117 | sleep 5 118 | metadataGet 119 | return 120 | fi 121 | 122 | [[ ! $artist ]] && artist=$( jq -r '.composers // empty' <<< $step ) 123 | if [[ ! -e $dirsystem/vumeter ]]; then 124 | if [[ $coverurl ]]; then 125 | name=$( alphaNumeric $artist$title ) 126 | ext=${coverurl/*.} 127 | coverfile=$dirshm/webradio/$name.$ext 128 | curl -s $coverurl -o $coverfile 129 | else 130 | name=$( alphaNumeric $artist$album ) 131 | coverfile=$( ls -X $dirshm/webradio/${name,,}.{jpg,png} 2> /dev/null | head -1 ) 132 | fi 133 | fi 134 | [[ -e $coverfile ]] && coverart=${coverfile:9} || coverart= 135 | data=' 136 | "player" : "mpd" 137 | , "Album" : "'$album'" 138 | , "Artist" : "'$artist'" 139 | , "elapsed" : '$( mpcElapsed webradio )' 140 | , "pllength" : '$( mpc status %length% )' 141 | , "state" : "play" 142 | , "Title" : "'$title'"' 143 | if [[ $coverart ]]; then 144 | data+=' 145 | , "coverart" : "'$coverart'"' 146 | else 147 | $dirbash/status-coverartonline.sh "cmd 148 | $artist 149 | $album 150 | webradio 151 | CMD ARTIST ALBUM MODE" &> /dev/null & 152 | fi 153 | pushData mpdradio "{ $data }" 154 | if [[ -e $dirsystem/lcdchar ]]; then 155 | data+=' 156 | , "Time" : false 157 | , "timestamp" : '$( date +%s%3N )' 158 | , "webradio" : true' 159 | echo "{ $data }" > $dirshm/status.json 160 | fi 161 | [[ -e $dirsystem/scrobble ]] && cp -f $dirshm/status{,prev} 162 | radioStatusFile 163 | [[ $coverart ]] && $dirbash/cmd.sh coverfileslimit 164 | # next fetch 165 | [[ ! $countdown || $countdown -lt 0 ]] && countdown=0 166 | sleep $(( countdown + 5 )) # add 5s delay 167 | metadataGet 168 | } 169 | 170 | metadataGet 171 | -------------------------------------------------------------------------------- /srv/http/bash/stoptimer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /srv/http/bash/common.sh 4 | 5 | . $dirsystem/stoptimer.conf 6 | 7 | killProcess stoptimer 8 | echo $$ > $dirshm/pidstoptimer 9 | 10 | killProcess relaystimer 11 | 12 | sleep $(( min * 60 )) 13 | 14 | rm $dirshm/pidstoptimer 15 | . <( grep -E '^card|^mixer' $dirshm/output ) 16 | volume=$( volumeGet ) 17 | 18 | $dirbash/cmd.sh "volume 19 | $volume 20 | 0 21 | $mixer 22 | $card 23 | CMD CURRENT TARGET CONTROL CARD" 24 | $dirbash/cmd.sh playerstop 25 | $dirbash/cmd.sh "volumesetat 26 | 0 27 | $mixer 28 | $card 29 | CMD TARGET CONTROL CARD" 30 | 31 | if [[ $poweroff ]]; then 32 | $dirbash/power.sh 33 | elif [[ -e $dirshm/relayson ]]; then 34 | $dirbash/relays.sh off 35 | fi 36 | -------------------------------------------------------------------------------- /srv/http/bash/tageditor.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /srv/http/bash/common.sh 4 | 5 | args2var "$1" 6 | 7 | path="/mnt/MPD/$FILE" 8 | argslast=${args[@]: -1} # CMD ALBUM ALBUMARTIST ... FILE - omit unchanged 9 | [[ -f $path ]] && istrack=1 10 | 11 | if [[ $FILE != *.cue ]]; then 12 | KEYS=( ${argslast:4:-5} ) # remove CMD and FILE 13 | for K in "${KEYS[@]}"; do 14 | k=${K,,} 15 | v=${!K} 16 | [[ $v == '*' ]] && continue 17 | 18 | [[ $v ]] && v=$( quoteEscape $v ) 19 | [[ ! $istrack ]] && all='/*.*' 20 | kid3-cli -c "set $k \"$v\"" "$path"$all 21 | done 22 | [[ $istrack ]] && dirupdate=$( dirname "$FILE" ) || dirupdate=$FILE 23 | else 24 | if [[ $istrack ]]; then 25 | sed -i -E '/^\s+TRACK '$TRACK'/ { 26 | n; s/^(\s+TITLE).*/\1 "'$TITLE'"/ 27 | n; s/^(\s+PERFORMER).*/\1 "'$ARTIST'"/ 28 | } 29 | ' "$path" 30 | else 31 | [[ $ALBUM ]] && data="\ 32 | TITLE $ALBUM" 33 | [[ $ALBUMARTIST ]] && data+=" 34 | PERFORMER $ALBUMARTIST" 35 | for k in COMPOSER CONDUCTOR GENRE DATE; do 36 | data+=" 37 | REM $k ${!k}" 38 | done 39 | data+=" 40 | $( sed -E '/^TITLE|^PERFORMER|^REM/ d; s/^(\s+PERFORMER ).*/\1'$ARTIST'/' "$path" )" 41 | echo "$data" > "$path" 42 | fi 43 | dirupdate=$( dirname "$FILE" ) 44 | fi 45 | 46 | $dirbash/cmd.sh "mpcupdate 47 | update 48 | $dirupdate 49 | CMD ACTION PATHMPD" 50 | -------------------------------------------------------------------------------- /srv/http/bash/vu.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /srv/http/bash/common.sh 4 | 5 | pin_0=$( sed 's/$/=0/; s/ /=0\n/g' $dirsystem/vuled.conf ) 6 | gpioset -t0 -c0 $pin_0 7 | [[ $1 == stop ]] && exit 8 | # -------------------------------------------------------------------- 9 | [[ -e $dirsystem/vumeter ]] && vumeter=1 && j=0 10 | if [[ -e $dirsystem/vuled ]]; then 11 | vuled=1 12 | pL=$( wc -l <<< $pin_0 ) 13 | on=( "$pin_0" ) 14 | for (( i=1; i < $pL + 1; i++ )); do 15 | on+=( "$( sed '1,'$i' s/0$/1/' <<< $pin_0 )" ) 16 | done 17 | fi 18 | cava -p /etc/cava.conf | while read vu; do 19 | v=${vu:0:-1} 20 | if [[ $vuled ]]; then 21 | if (( v > 0 )); then 22 | ir=$(( pL - 1 )) # i - roundup 23 | vr=$(( v + ir )) # v - roundup 24 | v=$(( vr / pL )) 25 | fi 26 | gpioset -t0 -c0 ${on[v]} # p1=0 p2=1 ... : 0-off, 1-on 27 | fi 28 | if [[ $vumeter ]]; then 29 | (( j++ )) 30 | if (( $j == 10 )); then # framerate throttle - push @10 signal 31 | pushData vumeter '{"val":'$v'}' 32 | j=0 33 | fi 34 | fi 35 | done 36 | -------------------------------------------------------------------------------- /srv/http/bash/websocket.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import asyncio 4 | import json 5 | import subprocess 6 | import websockets 7 | 8 | CLIENTS = set() 9 | IP_CLIENT = dict() 10 | 11 | async def cmd( websocket ): 12 | async for args in websocket: 13 | jargs = json.loads( args ) 14 | if 'channel' in jargs: # broadcast 15 | websockets.broadcast( CLIENTS, args ) # { "channel": "CAHNNEL", "data": { ... } } 16 | elif 'filesh' in jargs: # FILE.sh "a\nb\nc" 17 | filesh = '/srv/http/bash/'+ jargs[ 'filesh' ][ 0 ] 18 | jargs[ 'filesh' ][ 0 ] = filesh 19 | subprocess.Popen( jargs[ 'filesh' ] ) # { "filesh": [ "FILE.sh", "a\nb\nc..." ] } 20 | elif 'json' in jargs: # save to NAME.json and broadcast 21 | jargsjson = jargs[ 'json' ] # { "json": { ... }, "name": "NAME" } 22 | jargsname = jargs[ 'name' ] 23 | data = '{ "channel": "'+ jargsname +'", "data": '+ json.dumps( jargsjson ) +' }' 24 | websockets.broadcast( CLIENTS, data ) 25 | pathfile = '/srv/http/data/system/'+ jargsname 26 | with open( pathfile +'.json', 'w' ) as f: 27 | json.dump( jargsjson, f, indent=2 ) 28 | elif 'client' in jargs: 29 | ip = websocket.remote_address[ 0 ] 30 | if jargs[ 'client' ] == 'add': # { "client": "add" } 31 | CLIENTS.add( websocket ) 32 | if ip in IP_CLIENT: 33 | CLIENTS.discard( IP_CLIENT[ ip ] ) 34 | IP_CLIENT[ ip ] = websocket 35 | else: # { "client": "" } 36 | await websocket.send( str( IP_CLIENT ) ) 37 | # refresh CLIENTS 38 | for IP in IP_CLIENT: 39 | if IP == ip: continue 40 | 41 | if subprocess.call( [ 'ping', '-c', '1', '-w','1', IP ] ) != 0: 42 | CLIENTS.discard( IP_CLIENT[ IP ] ) 43 | IP_CLIENT.pop( IP, None ) 44 | elif 'status' in jargs: # { "status": "snapclient" } - from status.sh 45 | status = subprocess.run( [ '/srv/http/bash/status.sh', jargs[ 'status' ] ], capture_output=True, text=True ) 46 | status = status.stdout.replace( '\n', '\\n' ) 47 | await websocket.send( status ) 48 | elif 'ping' in jargs: 49 | await websocket.send( 'pong' ) 50 | 51 | async def main(): 52 | async with websockets.serve( cmd, '0.0.0.0', 8080, max_size=10485760, ping_interval=None, ping_timeout=None ): 53 | await asyncio.Future() # run forever 54 | 55 | asyncio.run( main() ) 56 | -------------------------------------------------------------------------------- /srv/http/cmd.php: -------------------------------------------------------------------------------- 1 | cmd ?? $argv[ 1 ]; // $argv - sort : from cmd-list.sh 4 | $sudo = '/usr/bin/sudo '; 5 | $dirbash = $sudo.'/srv/http/bash/'; 6 | $dirsettings = $dirbash.'settings/'; 7 | $dirdata = '/srv/http/data/'; 8 | $dirshm = $dirdata.'shm/'; 9 | 10 | switch( $CMD ) { 11 | 12 | case 'bash': 13 | $args = $post->args ?? ''; 14 | if ( is_array( $args ) ) $args = escape( implode( "\n", $args ) ); 15 | $command = $dirbash.$post->filesh.' "'.$args.'"'; 16 | $result = shell_exec( $command ); 17 | echo rtrim( $result ); 18 | break; 19 | case 'camilla': // formdata from camilla.js 20 | fileUploadSave( $dirdata.'camilladsp/'.$post->dir.'/'.$_FILES[ 'file' ][ 'name' ] ); 21 | exec( $dirsettings.'camilla-data.sh pushrefresh' ); 22 | break; 23 | case 'countmnt': 24 | include 'function.php'; 25 | echo json_encode( countMnt() ); 26 | break; 27 | case 'datarestore': // formdata from system.js 28 | fileUploadSave( $dirshm.'backup.gz' ); 29 | $libraryonly = $post->libraryonly ?? ''; 30 | exec( $dirsettings.'system-datarestore.sh '.$libraryonly, $output, $result ); 31 | if ( $result != 0 ) echo 'Restore failed'; 32 | break; 33 | case 'giftype': // formdata from common.js 34 | $tmpfile = $_FILES[ 'file' ][ 'tmp_name' ]; 35 | $animated = exec( $sudo.'/usr/bin/gifsicle -I '.$tmpfile.' | grep -q -m1 "image #1" && echo 1 || echo 0' ); 36 | echo $animated; 37 | if ( $animated ) move_uploaded_file( $tmpfile, $dirshm.'local/tmp.gif' ); 38 | break; 39 | case 'imagereplace': // $.post from function.js 40 | if ( ! is_writable( dirname( $post->file ) ) ) exit( '-1' ); 41 | //---------------------------------------------------------------------------------- 42 | exec( 'rm -f "'.substr( $post->file, 0, -4 ).'".*' ); // remove existing *.jpg, *.png, *.gif 43 | if ( substr( $post->file, -4 ) === 'jpg' ) { 44 | $base64 = preg_replace( '/^.*,/', '', $post->data ); // data:imgae/jpeg;base64,... > ... 45 | file_put_contents( $post->file, base64_decode( $base64 ) ); 46 | } else { 47 | copy( $post->data, $post->file ); 48 | } 49 | $args = escape( implode( "\n", [ $post->type, $post->file, $post->current, 'CMD TARGET CURRENT' ] ) ); 50 | shell_exec( $dirbash.'cmd-coverart.sh "'.$args.'"' ); 51 | break; 52 | case 'login': // $.post from features.js 53 | $filelogin = $dirdata.'system/login'; 54 | $pwd = $post->pwd; 55 | if ( file_exists( $filelogin ) ) { 56 | $password = rtrim( file_get_contents( $filelogin ), "\n" ); 57 | if ( ! password_verify( $pwd, $password ) ) exit( '-1' ); 58 | //---------------------------------------------------------------------------------- 59 | } 60 | $filesetting = $filelogin.'setting'; 61 | if ( isset( $post->disable ) ) { 62 | unlink( $filelogin ); 63 | unlink( $filesetting ); 64 | exec( $dirsettings.'features.sh login' ); 65 | } else if ( ! isset( $post->loginsetting ) ) { 66 | session_start(); 67 | $_SESSION[ 'login' ] = 1; 68 | } else { 69 | $pwd = $post->pwdnew ?: $pwd; 70 | $hash = password_hash( $pwd, PASSWORD_BCRYPT, [ 'cost' => 12 ] ); 71 | file_put_contents( $filelogin, $hash ); 72 | if ( $post->loginsetting === 'true' ) { // no boolean 73 | touch( $filesetting ); 74 | } else { 75 | unlink( $filesetting ); 76 | } 77 | exec( $dirsettings.'features.sh login' ); 78 | } 79 | break; 80 | case 'logout': // $.post from main.js 81 | session_start(); 82 | session_destroy(); 83 | break; 84 | case 'sort': // from cmd-list.sh 85 | include 'function.php'; 86 | $modes = explode( ' ', $argv[ 2 ] ); 87 | foreach( $modes as $mode ) { 88 | $file = '/srv/http/data/mpd/'.$mode; 89 | if ( ! file_exists( $file ) ) continue; 90 | 91 | $lines = file( $file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES ); 92 | $data = []; 93 | foreach( $lines as $l ) $data[] = stripSort( $l ).'^x^'.$l; 94 | usort( $data, function( $a, $b ) { 95 | return strnatcasecmp( $a, $b ); 96 | } ); 97 | $list = ''; 98 | foreach( $data as $d ) $list .= mb_substr( $d, 0, 1, 'UTF-8' ).'^^'.explode( '^x^', $d )[ 1 ]."\n"; 99 | file_put_contents( $file, $list ); 100 | } 101 | break; 102 | case 'timezonelist': // $.post from system.js 103 | $list = timezone_identifiers_list(); 104 | $option = ''; 105 | foreach( $list as $key => $zone ) { 106 | $datetime = new DateTime( 'now', new DateTimeZone( $zone ) ); 107 | $offset = $datetime->format( 'P' ); 108 | $zonename = preg_replace( [ '/_/', '/\//' ], [ ' ', ' · ' ], $zone ); 109 | $option .= ''; 110 | } 111 | echo $option; 112 | break; 113 | 114 | } 115 | 116 | function escape( $string ) { 117 | return preg_replace( '/(["`])/', '\\\\\1', $string ); // \1 inside function - $1 normal 118 | } 119 | function fileUploadSave( $filepath ) { 120 | if ( $_FILES[ 'file' ][ 'error' ] != UPLOAD_ERR_OK ) exit( '-1' ); 121 | //---------------------------------------------------------------------------------- 122 | move_uploaded_file( $_FILES[ 'file' ][ 'tmp_name' ], $filepath ); 123 | } 124 | -------------------------------------------------------------------------------- /srv/http/common.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | rAudio 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | '; 31 | include 'login.php'; 32 | exit; 33 | //---------------------------------------------------------------------------------- 34 | } 35 | } 36 | $equalizer = file_exists( '/srv/http/data/system/equalizer' ); 37 | $localhost = in_array( $_SERVER[ 'REMOTE_ADDR' ], ['127.0.0.1', '::1'] ); 38 | 39 | // plugin: css / js filename with version 40 | $cssfiles = array_slice( scandir( '/srv/http/assets/css/plugin' ), 2 ); 41 | foreach( $cssfiles as $file ) { 42 | $name = explode( '-', $file )[ 0 ]; 43 | $cfiles[ $name ] = $file; 44 | } 45 | $jsfiles = array_slice( scandir( '/srv/http/assets/js/plugin' ), 2 ); 46 | foreach( $jsfiles as $file ) { 47 | $name = explode( '-', $file )[ 0 ]; 48 | $jfiles[ $name ] = $file; 49 | } 50 | if ( ! $page ) { // main 51 | $cssp = []; 52 | $css = [ ...$css, 'main', 'hovercursor' ]; 53 | $jsp = [ 'jquery', 'pica', 'qrcode' ]; 54 | $js = [ 'common', 'context', 'main', 'function', 'passive', 'shortcut' ]; 55 | if ( $equalizer ) { 56 | $cssp[] = 'select2'; 57 | $css = [ ...$css, 'select2', 'equalizer' ]; 58 | $jsp[] = 'select2'; 59 | } 60 | $title = 'STATUS'; 61 | } else { // settings 62 | $cssp = []; 63 | $css[] = 'settings'; 64 | $jsp = [ 'jquery', $networks ? 'qrcode' : 'select2' ]; 65 | $js = [ 'common', 'settings', $page ]; 66 | if ( ! $guide && ! $networks && ! $addonsprogress ) { 67 | $cssp[] = 'select2'; 68 | $css[] = 'select2'; 69 | } 70 | if ( $addons ) $css[] = 'addons'; 71 | $icon = $page; 72 | $pagetitle = strtoupper( $page ); 73 | if ( $addonsprogress ) { 74 | $icon = 'addons'; 75 | $pagetitle = 'Addons-Progress'; 76 | } else if ( $camilla ) { 77 | $icon = 'camilladsp'; 78 | $pagetitle = 'CamillaDSP'; 79 | $css = [ ...$css, 'camilla', 'equalizer' ]; 80 | $jsp = [ ...$jsp, 'camilladsp_plot', 'complex', 'plotly' ]; 81 | } else if ( $guide ) { 82 | $icon = 'help'; 83 | $pagetitle = 'User Guide'; 84 | $jsp = []; 85 | $js = [ 'guide' ]; 86 | } 87 | $title = $pagetitle; 88 | } 89 | $add_guide = $addonsprogress || $guide; 90 | $keyboard = $localhost && ! $add_guide; 91 | if ( $keyboard ) foreach( [ 'cssp', 'css', 'jsp', 'js' ] as $ea ) $$ea[] = 'simplekeyboard'; 92 | 93 | $html = ''; 94 | $htmlcss = ''; 96 | foreach( $css as $c ) $html.= $htmlcss.$c.'.css'.$hash.'">'; 97 | $html .= ' 98 | 99 | 100 | '; 101 | if ( ! $add_guide ) { 102 | $pageicon = $page ? icon( $page.' page-icon' ) : ''; 103 | $html .= ' 104 |
    105 |
    '.$logosvg.'
    106 | 107 | '; 108 | } 109 | if ( $keyboard ) $html.= ' 110 |
    111 | '; 112 | echo $html; 113 | 114 | $scripts = ''; 115 | $htmljs = ''; 117 | foreach( $js as $j ) $scripts.= $htmljs.$j.'.js'.$hash.'">'; 118 | 119 | function htmlBottom() { 120 | global $htmlbar, $scripts; 121 | echo ' 122 |
    '.$htmlbar.'
    123 | 124 | '.$scripts.' 125 | 126 | 127 | '; 128 | } 129 | function icon( $icon, $id = '', $cmd = '' ) { 130 | $htmlid = $id ? ' id="'.$id.'"' : ''; 131 | $htmlcmd = $cmd ? ' data-cmd="'.$cmd.'"' : ''; 132 | return ''; 133 | } 134 | -------------------------------------------------------------------------------- /srv/http/login.php: -------------------------------------------------------------------------------- 1 | 27 | 28 | 29 | 30 |
    31 |
    32 | 33 |
    Wrong password.
    34 |
    OK
    35 |
    36 |
    37 | 38 |
    39 | 40 |
    rAudio
    41 | 42 |
    Login 43 |
    44 | 45 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /srv/http/settings.php: -------------------------------------------------------------------------------- 1 | '.icon( $icon.' page-icon pagerefresh' ).''.$title.' 6 | '.icon( 'close close', 'close' ).icon( 'help helphead' ).icon( 'gear' ).' 7 | '; 8 | if ( ! $guide ) $htmlhead.= ' 9 |
    10 | '; 11 | echo $htmlhead; 12 | if ( $addonsprogress ) { 13 | include 'settings/'.$page.'.php'; 14 | exit; 15 | //---------------------------------------------------------------------------------- 16 | } 17 | 18 | $iconlabel = $features || $system; 19 | $prefix = ''; 20 | $htmlbar = ''; 21 | $isenabled = ' is currently enabled.'; 22 | if ( $camilla ) { 23 | $tabs = [ 'filters', 'mixers', 'processors', 'pipeline', 'devices' ]; 24 | $prefix = 'tab'; 25 | } else if ( $guide ) { 26 | $tabs = [ 'library', 'playback', 'playlist', 'settings' ]; 27 | } else { 28 | $tabs = [ 'features', 'player', 'networks', 'system', 'addons' ]; 29 | } 30 | foreach ( $tabs as $tab ) $htmlbar.= '
    '.icon( $tab ).''.ucfirst( $tab ).'
    '; 31 | if ( $guide ) { 32 | include 'settings/guide.php'; 33 | exit; 34 | //---------------------------------------------------------------------------------- 35 | } 36 | if ( ! $addons ) { 37 | include 'settings/function.php'; 38 | include 'settings/'.$page.'.php'; // addons: by addons.js 39 | } 40 | echo ' 41 |
    42 | '; 43 | htmlBottom(); 44 | -------------------------------------------------------------------------------- /srv/http/settings/guide.php: -------------------------------------------------------------------------------- 1 | 28 | 29 | '.icon( 'back', 'prev' ).icon( 'arrow-right', 'next' ).''; 31 | htmlBottom(); 32 | -------------------------------------------------------------------------------- /srv/http/settings/networks.php: -------------------------------------------------------------------------------- 1 |
    2 | [ 'add', 'ap', 'bluetooth', 'btsender', 'lan', 'search', 'wifi', 'wifi1' ] 5 | , 'labels' => [ 6 | 'Access Point' => 'ap' 7 | , 'Bluetooth' => 'bluetooth' 8 | ] 9 | , 'menus' => [] 10 | , 'tabs' => [ 'features', 'system' ] 11 | ] ); 12 | // ---------------------------------------------------------------------------------- 13 | $head = [ 14 | 'title' => 'Bluetooth' 15 | , 'status' => 'bluez' 16 | , 'button' => 'search btscan' 17 | , 'list' => true 18 | , 'help' => <<< EOF 19 | $B->search Available devices 20 | 21 | rAudio as sender: (or pairing non-audio devices) 22 | • Pair: 23 | · On receiver: Turn on Discovery / Pairing mode 24 | · On rAudio: $B->search Scan to connect » Select to pair 25 | • Connect / Disconnect: 26 | · On receiver: Turn on / off 27 | • Playback controls with buttons: 28 | · Main - Play / Pause 29 | · Up / Down (long-press) - Previous / Next 30 | 31 | rAudio as receiver: 32 | • Pair: 33 | · On rAudio: $T->system$L->bluetooth ■ Discoverable by senders 34 | · On sender: Search » Select rAudio to pair 35 | • Connect / Disconnect: 36 | · On sender 37 | 38 | Note: 39 | Forget / remove should be done on both rAudio and sender 40 | 41 | $B->bluetooth$B->btsender Context menu 42 | EOF 43 | ]; 44 | $body = [ '
      ' ]; 45 | htmlSection( $head, $body, 'bluetooth' ); 46 | // ---------------------------------------------------------------------------------- 47 | $head = [ 48 | 'title' => 'Wi-Fi' 49 | , 'status' => 'wl' 50 | , 'button' => [ 'add wladd', 'search wlscan' ] 51 | , 'list' => true 52 | , 'help' => <<< EOF 53 | $B->add Manual connect 54 | $B->search Available networks 55 | 56 | Note: 57 | · Avoid double quotes " in Wi-Fi name and password. 58 | · Access points with 1 bar $B->wifi1 might be unstable. 59 | 60 | $B->wifi$B->ap Context menu 61 | EOF 62 | ]; 63 | $body = [ '
        ' ]; 64 | htmlSection( $head, $body, 'wlan' ); 65 | // ---------------------------------------------------------------------------------- 66 | $head = [ 67 | 'title' => 'Wired LAN' 68 | , 'status' => 'lan' 69 | , 'button' => 'add lanadd' 70 | , 'list' => true 71 | , 'help' => <<< EOF 72 | $B->add Manual connect 73 | $B->lan Context menu 74 | EOF 75 | ]; 76 | $body = [ '
          ' ]; 77 | htmlSection( $head, $body, 'wlan' ); 78 | // ---------------------------------------------------------------------------------- 79 | ?> 80 |
          81 | 'Bluetooth' 85 | , 'button' => 'bluetooth blink scanning-bt' 86 | , 'back' => true 87 | ]; 88 | $body = [ '
            ' ]; 89 | htmlSection( $head, $body, 'scanbluetooth', 'hide' ); 90 | // ---------------------------------------------------------------------------------- 91 | $head = [ 92 | 'title' => 'Wi-Fi' 93 | , 'button' => 'wifi blink scanning-wifi' 94 | , 'back' => true 95 | ]; 96 | $body = [ '
              ' ]; 97 | htmlSection( $head, $body, 'scanwlan', 'hide' ); 98 | // ---------------------------------------------------------------------------------- 99 | $head = [ 'title' => 'Web User Interface' ]; 100 | $body = [ 101 | htmlSectionStatus( 102 | 'ap' 103 | , 'Access Point
              Password' 104 | , '
              105 |
              Access rAudio directly without Wi-Fi router: 106 | • Connect Access Point with the password or scan QR code 107 | • Access point setting: '.$T->features.$L->accesspoint.' 108 | 109 | Note: No internet connection.
              ' 110 | ) 111 | , htmlSectionStatus( 112 | 'url' 113 | , 'Browser URL' 114 | , '
              115 |
              • Open URL with any web browsers or scan QR code
              ' 116 | ) 117 | ]; 118 | htmlSection( $head, $body, 'webui' ); 119 | // ---------------------------------------------------------------------------------- 120 | htmlMenu( [ 'connect', 'disconnect', 'edit', 'forget', 'rename', 'info' ] ); 121 | --------------------------------------------------------------------------------