├── tests ├── mocks │ ├── simple.ini │ └── credentials.ini ├── meson.build ├── stdb_ds_test.c ├── ini_reader_test.c └── strings_basic.c ├── src ├── version.h.in ├── credentials_lastfm.h.in ├── credentials_librefm.h.in ├── credentials_listenbrainz.h.in ├── daemon.c ├── smpris.h ├── ini.h ├── md5.h ├── ini_base.h ├── sstrings.h ├── sevents.h ├── scrobbler.h ├── utils.h ├── signon.c ├── structs.h ├── listenbrainz_api.h ├── stb_ds.h ├── curl.h └── configuration.h ├── .gitmodules ├── utils ├── get-players.sh ├── current-playing.sh ├── get-status.sh └── monitor.sh ├── .gitignore ├── units └── systemd-user.service.in ├── doc ├── mpris-scrobbler-config.5.scd ├── mpris-scrobbler-signon.1.scd ├── mpris-scrobbler.1.scd └── mpris-scrobbler-credentials.5.scd ├── .builds ├── alpine.yml ├── debian.yml └── archlinux.yml ├── LICENSE ├── meson_options.txt ├── .travis.yml ├── meson.build └── README.md /tests/mocks/simple.ini: -------------------------------------------------------------------------------- 1 | ; comment 2 | [test] 3 | key = value 4 | -------------------------------------------------------------------------------- /src/version.h.in: -------------------------------------------------------------------------------- 1 | #ifndef VERSION_HASH 2 | #define VERSION_HASH "@GIT_VERSION@" 3 | #endif 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "tests/lib/snow"] 2 | path = tests/lib/snow 3 | url = https://github.com/mortie/snow.git 4 | -------------------------------------------------------------------------------- /utils/get-players.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | dbus-send --print-reply --type=method_call --dest=org.freedesktop.DBus / org.freedesktop.DBus.ListNames | grep mpris 3 | -------------------------------------------------------------------------------- /src/credentials_lastfm.h.in: -------------------------------------------------------------------------------- 1 | #ifndef LASTFM_API_KEY 2 | #define LASTFM_API_KEY "@lastfm_api_key@" 3 | #endif 4 | #ifndef LASTFM_API_SECRET 5 | #define LASTFM_API_SECRET "@lastfm_api_secret@" 6 | #endif 7 | -------------------------------------------------------------------------------- /src/credentials_librefm.h.in: -------------------------------------------------------------------------------- 1 | #ifndef LIBREFM_API_KEY 2 | #define LIBREFM_API_KEY "@librefm_api_key@" 3 | #endif 4 | #ifndef LIBREFM_API_SECRET 5 | #define LIBREFM_API_SECRET "@librefm_api_secret@" 6 | #endif 7 | -------------------------------------------------------------------------------- /utils/current-playing.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | dbus-send --print-reply --type=method_call --dest=org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 org.freedesktop.DBus.Properties.Get string:org.mpris.MediaPlayer2.Player string:Metadata 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | src/version.h 2 | mpris-scrobbler.service 3 | mpris-scrobbler-signon 4 | mpris-scrobbler 5 | cov-int/ 6 | tags 7 | vgcore.* 8 | *.config 9 | *.files 10 | *.includes 11 | *.creator* 12 | *.orig 13 | .idea/ 14 | build/ 15 | -------------------------------------------------------------------------------- /utils/get-status.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | dbus-send --print-reply --type=method_call --dest=org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 org.freedesktop.DBus.Properties.Get string:org.mpris.MediaPlayer2.Player string:PlaybackStatus 3 | 4 | -------------------------------------------------------------------------------- /src/credentials_listenbrainz.h.in: -------------------------------------------------------------------------------- 1 | #ifndef LISTENBRAINZ_API_KEY 2 | #define LISTENBRAINZ_API_KEY "@listenbrainz_api_key@" 3 | #endif 4 | #ifndef LISTENBRAINZ_API_SECRET 5 | #define LISTENBRAINZ_API_SECRET "@listenbrainz_api_secret@" 6 | #endif 7 | -------------------------------------------------------------------------------- /utils/monitor.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | dbus-monitor type='signal',interface='org.freedesktop.DBus.Properties',member='PropertiesChanged',path='/org/mpris/MediaPlayer2' type='signal',interface='org.freedesktop.DBus',member='NameOwnerChanged',path='/org/freedesktop/DBus' 3 | -------------------------------------------------------------------------------- /tests/mocks/credentials.ini: -------------------------------------------------------------------------------- 1 | [one] 2 | enabled = true 3 | username = tester 4 | token = f00b4r 5 | session = kerfuffle 6 | 7 | [two] 8 | enabled = 1 9 | username = s!mb4 10 | token = test!321@@321 11 | session = rwar32test 12 | 13 | [and-three] 14 | enabled = false 15 | token = f00d-ale-c0ffee-41f-6419418c768a 16 | 17 | -------------------------------------------------------------------------------- /units/systemd-user.service.in: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description = daemon to scrobble tracks loaded from the MPRIS DBus interface to compatible services 3 | Documentation=man:DAEMONNAME(1) 4 | Requires = dbus.socket 5 | 6 | [Service] 7 | Type = simple 8 | Environment="XDG_DATA_HOME=%h/.local/share" 9 | ExecStart = BINPATH`'DAEMONNAME -vv 10 | ExecReload = /bin/kill -HUP $MAINPID 11 | CPUQuota = 1% 12 | Restart = on-failure 13 | RestartSec = 30 14 | PassEnvironment = PROXY 15 | 16 | [Install] 17 | WantedBy=default.target 18 | -------------------------------------------------------------------------------- /doc/mpris-scrobbler-config.5.scd: -------------------------------------------------------------------------------- 1 | MPRIS-SCROBBLER-CONFIG(5) 2 | 3 | # NAME 4 | 5 | *mpris-scrobbler-config* - The MPRIS scrobbler daemon config file 6 | 7 | # SYNOPSIS 8 | _$XDG\_CONFIG\_HOME/mpris-scrobbler/config_ 9 | 10 | # DESCRIPTION 11 | 12 | This is the configuration file of the *mpris-scrobbler* daemon and is used to store the list of players 13 | that are to be ignored when loading MPRIS information. 14 | 15 | The file format supports multiple value assignments in the form of: 16 | 17 | ``` 18 | ignore = PlayerName 19 | ``` 20 | 21 | or 22 | 23 | ``` 24 | ignore = org.mpris.MediaPlayer2.ServiceName 25 | ``` 26 | The player name and service name values are case sensitive. 27 | -------------------------------------------------------------------------------- /.builds/alpine.yml: -------------------------------------------------------------------------------- 1 | image: alpine/edge 2 | packages: 3 | - build-base 4 | - dbus-dev 5 | - curl-dev 6 | - libevent-dev 7 | - json-c-dev 8 | - meson 9 | - ninja 10 | - scdoc 11 | sources: 12 | - https://git.sr.ht/~mariusor/mpris-scrobbler 13 | secrets: 14 | - 3dcea276-38d6-4a7e-85e5-20cbc903e1ea 15 | tasks: 16 | - build_debug: | 17 | test ${BUILD_SUBMITTER} != "git.sr.ht" && complete-build 18 | cd mpris-scrobbler 19 | meson setup build/ 20 | ninja -C build/ 21 | - build_release: | 22 | cd mpris-scrobbler 23 | meson setup -Dbuildtype=release build/ 24 | ninja -C build/ 25 | - tests: | 26 | cd mpris-scrobbler 27 | git submodule init 28 | git submodule update 29 | cd tests/ 30 | meson setup -Dbuildtype=debug build/ 31 | meson test -C build/ -v --test-args " --no-maybes --cr" 32 | -------------------------------------------------------------------------------- /.builds/debian.yml: -------------------------------------------------------------------------------- 1 | image: debian/unstable 2 | arch: arm64 3 | packages: 4 | - build-essential 5 | - m4 6 | - git 7 | - libdbus-1-dev 8 | - libcurl4-openssl-dev 9 | - libevent-dev 10 | - libjson-c-dev 11 | - meson 12 | - ninja-build 13 | sources: 14 | - https://git.sr.ht/~mariusor/mpris-scrobbler 15 | tasks: 16 | - build_debug: | 17 | test ${BUILD_SUBMITTER} != "git.sr.ht" && complete-build 18 | cd mpris-scrobbler 19 | meson setup build/ 20 | ninja -C build/ 21 | - build_release: | 22 | cd mpris-scrobbler 23 | meson setup -Dbuildtype=release build/ 24 | ninja -C build/ 25 | - tests: | 26 | cd mpris-scrobbler 27 | git submodule init 28 | git submodule update 29 | cd tests/ 30 | meson setup -Dbuildtype=debug -Db_sanitize=address,undefined build/ 31 | meson test -C build/ -v --test-args " --no-maybes --cr" 32 | 33 | -------------------------------------------------------------------------------- /tests/meson.build: -------------------------------------------------------------------------------- 1 | project('mpris-scrobbler-tests', 'c') 2 | 3 | srcdir = include_directories('../src') 4 | snowdir = include_directories('lib/snow') 5 | 6 | args = ['-Wall', '-Wextra', '-DSNOW_ENABLED'] 7 | 8 | stretchy_test = executable('test_stdb_ds', 9 | ['stdb_ds_test.c'], 10 | c_args: args, 11 | include_directories: [srcdir, snowdir], 12 | ) 13 | 14 | ini_parser_test = executable('test_ini_parser', 15 | ['ini_reader_test.c'], 16 | c_args: args, 17 | include_directories: [srcdir, snowdir], 18 | ) 19 | 20 | strings_test = executable('strings_test', 21 | ['strings_basic.c'], 22 | c_args: args, 23 | include_directories: [srcdir, snowdir], 24 | ) 25 | test('Test stretchy buffers functionality', stretchy_test) 26 | test('Test ini parser functionality', ini_parser_test) 27 | test('Test custom strings functionality', strings_test) 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Marius Orcsik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.builds/archlinux.yml: -------------------------------------------------------------------------------- 1 | image: archlinux 2 | environment: 3 | DEBUGINFOD_URLS: https://debuginfod.archlinux.org 4 | packages: 5 | - base-devel 6 | - dbus 7 | - curl 8 | - libevent 9 | - json-c 10 | - meson 11 | - ninja 12 | sources: 13 | - https://git.sr.ht/~mariusor/mpris-scrobbler 14 | secrets: 15 | - 3dcea276-38d6-4a7e-85e5-20cbc903e1ea 16 | tasks: 17 | - build_debug: | 18 | test ${BUILD_SUBMITTER} != "git.sr.ht" && complete-build 19 | cd mpris-scrobbler 20 | meson setup build/ 21 | ninja -C build/ 22 | - build_release: | 23 | cd mpris-scrobbler 24 | meson setup -Dbuildtype=release build/ 25 | ninja -C build/ 26 | - tests: | 27 | cd mpris-scrobbler 28 | git submodule init 29 | git submodule update 30 | cd tests/ 31 | meson setup -Dbuildtype=debug -Db_sanitize=address,undefined build/ 32 | meson test -C build/ -v --test-args " --no-maybes --cr" 33 | - push_to_github: | 34 | set -a +x 35 | ssh-keyscan -H github.com >> ~/.ssh/known_hosts 36 | 37 | cd mpris-scrobbler 38 | git remote add hub git@github.com:mariusor/mpris-scrobbler 39 | git push hub --force --all 40 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | # -*- mode: meson -*- 2 | 3 | option('version', type: 'string', description: '''override the version of the build''') 4 | option('rootprefix', type: 'string', description: '''override the root prefix''') 5 | option('unitdir', type: 'string', value: 'lib/systemd/user') 6 | option('lastfm_api_key', type: 'string', value: '296ff3cb843e11f006b40317fc375fec', 7 | description: ''' The API key obtained from last.fm ''') 8 | option('lastfm_api_secret', type: 'string', value: '2d6dfbd92a476aca1091a0bfbf753993', 9 | description: ''' The API secret obtained from last.fm ''') 10 | option('librefm_api_key', type: 'string', value: '299dead99beef992', 11 | description: ''' The API key obtained from libre.fm ''') 12 | option('librefm_api_secret', type: 'string', value: 'c0ffee1511fe', 13 | description: ''' The API secret obtained from libre.fm ''') 14 | option('listenbrainz_api_key', type: 'string', value: '8L6O_eyMFyUWVW0SxWllqg', 15 | description: ''' The API key obtained from listenbrainz.org ''') 16 | option('listenbrainz_api_secret', type: 'string', value: '2OWfXf0r06ubXtZPxYTWBQ', 17 | description: ''' The API secret obtained from listenbrainz.org ''') 18 | option('libeventdebug', type: 'boolean', value: false) 19 | option('libcurldebug', type: 'boolean', value: false) 20 | option('libdbusdebug', type: 'boolean', value: false) 21 | -------------------------------------------------------------------------------- /doc/mpris-scrobbler-signon.1.scd: -------------------------------------------------------------------------------- 1 | MPRIS-SCROBBLER-SIGNON(1) 2 | 3 | # NAME 4 | 5 | *mpris-scrobbler-signon* - The MPRIS scrobbler sign-on utility 6 | 7 | # SYNOPSIS 8 | 9 | mpris-scrobbler-signon [OPTIONS...] [COMMAND] [SERVICE] 10 | 11 | # DESCRIPTION 12 | 13 | *mpris-scrobbler-signon* is the utility command used to execute the authentication steps required 14 | by each audioscrobbler service. 15 | 16 | The credentials are stored in the *mpris-scrobbler-credentials*(5) file. 17 | 18 | # COMMANDS 19 | 20 | *token* 21 | Get the authentication token for SERVICE. 22 | 23 | This step requires that the *xdg-open*(1) executable exists on the machine, so it can open a 24 | browser window with the authorization page of the SERVICE. 25 | 26 | *session* 27 | Activate a new session for SERVICE. It requires that a valid token was previously acquired for SERVICE. 28 | 29 | *enable* 30 | Activate SERVICE for submitting tracks. 31 | 32 | *disable* 33 | Deactivate SERVICE for submitting tracks. 34 | 35 | # SERVICES 36 | 37 | *mpris-scrobbler* supports the following service labels. For a full description check the *SERVICES* section of *mpris-scrobbler*(5): 38 | 39 | 1. *listenbrainz*: listenbrainz.org[1] 40 | 41 | 2. *librefm*: libre.fm[2] 42 | 43 | 3. *lastfm*: last.fm[3] 44 | 45 | # OPTIONS 46 | 47 | *-h*, *--help* 48 | Print a short help message and exit. 49 | 50 | *-q*, *--quiet* 51 | Do not print any debugging messages. 52 | 53 | *-v*, *-vv*, *-vvv*, *--verbose=[1..3]* 54 | Increase print verbosity to level: 55 | 56 | *1* Info messages. 57 | 58 | *2* Debug messages. 59 | 60 | *3* Tracing messages. 61 | 62 | The default is to display only error and warning messages. 63 | 64 | *-u*, *--url=* 65 | Use to a custom URL to connect to the SERVICE. 66 | 67 | This option is valid only when using the *libre.fm* or *listenbrainz.org* services which provide the code to create a local instance of the service. 68 | 69 | # NOTES 70 | 71 | 1. *listenbrainz.org* 72 | https://listenbrainz.org 73 | 74 | 2. *libre.fm* 75 | https://libre.fm 76 | 77 | 3. *last.fm* 78 | https://last.fm 79 | 80 | # SEE ALSO 81 | 82 | *mpris-scrobbler*(1), *mpris-scrobbler-credentials*(5) 83 | -------------------------------------------------------------------------------- /src/daemon.c: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Marius Orcsik 3 | */ 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #ifndef PATH_MAX 10 | // NOTE(marius): musl seems to not have this defined for all cases, Alpine release build 11 | // would fail in CI 12 | #define PATH_MAX 4096 13 | #endif 14 | #define STB_DS_IMPLEMENTATION 15 | #include "stb_ds.h" 16 | #include "sstrings.h" 17 | #include "structs.h" 18 | #include "utils.h" 19 | #include "api.h" 20 | #include "smpris.h" 21 | #include "scrobbler.h" 22 | #include "scrobble.h" 23 | #include "sdbus.h" 24 | #include "sevents.h" 25 | #include "configuration.h" 26 | 27 | #define HELP_MESSAGE "MPRIS scrobbler daemon, version %s\n" \ 28 | "Usage:\n %s\t\tStart daemon\n" \ 29 | HELP_OPTIONS \ 30 | "" 31 | 32 | 33 | static void print_help(const char *name) 34 | { 35 | fprintf(stdout, HELP_MESSAGE, get_version(), name); 36 | } 37 | 38 | int main (const int argc, char *argv[]) 39 | { 40 | int status = EXIT_FAILURE; 41 | struct configuration config = {0}; 42 | 43 | // TODO(marius): make this asynchronous to be requested when submitting stuff 44 | struct parsed_arguments arguments = {0}; 45 | parse_command_line(&arguments, daemon_bin, argc, argv); 46 | if (arguments.has_help) { 47 | print_help(arguments.name); 48 | status = EXIT_SUCCESS; 49 | goto _exit; 50 | } 51 | 52 | load_configuration(&config, APPLICATION_NAME); 53 | load_pid_path(&config); 54 | _trace("main::writing_pid: %s", config.pid_path); 55 | config.wrote_pid = write_pid(config.pid_path); 56 | 57 | #if 0 58 | print_application_config(&config); 59 | #endif 60 | 61 | const size_t count = config.credentials_count; 62 | if (count == 0) { _warn("main::load_credentials: no credentials were loaded"); } 63 | 64 | struct state state = {0}; 65 | 66 | if (!state_init(&state, &config)) { 67 | _error("main::unable to initialize"); 68 | goto _free_state; 69 | } 70 | 71 | event_base_dispatch(state.events.base); 72 | status = EXIT_SUCCESS; 73 | 74 | _free_state: 75 | state_destroy(&state); 76 | configuration_clean(&config); 77 | _exit: 78 | 79 | return status; 80 | } 81 | -------------------------------------------------------------------------------- /doc/mpris-scrobbler.1.scd: -------------------------------------------------------------------------------- 1 | MPRIS-SCROBBLER(1) 2 | 3 | # NAME 4 | 5 | *mpris-scrobbler* - The MPRIS scrobbler user daemon 6 | 7 | # SYNOPSIS 8 | 9 | mpris-scrobbler [OPTIONS...] 10 | 11 | # DESCRIPTION 12 | 13 | *mpris-scrobbler* is a minimalistic user daemon for submitting the current playing songs to 14 | any of the supported scrobbling services. 15 | 16 | It can interact with any media-player that conforms to the *MPRIS D-Bus Interface Specification*[1]. 17 | 18 | # SERVICES 19 | 20 | *mpris-scrobbler* supported services are: 21 | 22 | *listenbrainz.org* 23 | Music service associated with the Music Brainz audio fingerprinting service. 24 | 25 | *libre.fm* 26 | Open source variant of the original audioscrobbler.com service. 27 | 28 | *last.fm* 29 | The evolution of the closed audioscrobbler.com service under the CBS umbrella. 30 | 31 | # OPTIONS 32 | 33 | *-h*, *--help* 34 | Print a short help message and version number and exit. 35 | 36 | *-q*, *--quiet* 37 | Do not print any debugging messages. 38 | 39 | *-v*, *-vv*, *-vvv*, *--verbose=[1..3]* 40 | Increase print verbosity to level: 41 | 42 | *1* Info messages. 43 | 44 | *2* Debug messages. 45 | 46 | *3* Tracing messages. 47 | 48 | The default is to display only error and warning messages. 49 | 50 | # SIGNALS 51 | 52 | *SIGTERM* 53 | Upon receiving this signal the *mpris-scrobbler* daemon will try to cleanly exit. 54 | 55 | *SIGINT* 56 | The *mpris-scrobbler* daemon treats this signal the same as *SIGTERM*. 57 | 58 | *SIGHUP* 59 | Reloads the credentials file[3], then reloads the current playing track if possible and submits it to the loaded services. 60 | 61 | # ENVIRONMENT 62 | 63 | _$XDG\_CONFIG\_HOME_, _$XDG\_DATA\_HOME_, _$XDG\_CACHE\_HOME_, _$XDG\_RUNTIME\_DIR_ 64 | The *mpris-scrobbler* daemon uses these variables in accordance to the 65 | *XDG Base Directory specification*[2] to find its configuration and save its PID file. 66 | 67 | # NOTES 68 | 69 | 1. *MPRIS D-Bus Interface Specification* 70 | https://specifications.freedesktop.org/mpris-spec/latest/ 71 | 72 | 2. *XDG Base Directory specification* 73 | http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html 74 | 75 | 3. *mpris-scrobbler-credentials*(5) 76 | 77 | # SEE ALSO 78 | 79 | *mpris-scrobbler-signon*(1), *mpris-scrobbler-credentials*(5) 80 | -------------------------------------------------------------------------------- /tests/stdb_ds_test.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define STB_DS_IMPLEMENTATION 4 | #include "stb_ds.h" 5 | 6 | describe(stretchy_buffers) { 7 | it("Pushing string to stretchy buffer") { 8 | char** arr = NULL; 9 | 10 | asserteq(arr, NULL); 11 | asserteq(arrlen(arr), 0); 12 | 13 | char* first = calloc(12, sizeof(char)); 14 | 15 | arrput(arr, first); 16 | 17 | assertneq(arr, NULL); 18 | 19 | asserteq(arrlen(arr), 1); 20 | 21 | for (unsigned int i = 0; i < arrlen(arr); i++) { 22 | asserteq(arr[i], first); 23 | } 24 | asserteq(arrpop(arr), first); 25 | 26 | arrfree(arr); 27 | free(first); 28 | }; 29 | 30 | it ("Pushing multiple strings to stretchy buffer") { 31 | char** arr = NULL; 32 | 33 | asserteq(arr, NULL); 34 | asserteq(arrlen(arr), 0); 35 | 36 | for(int i = 0; i < 10; i++) { 37 | char *new_el = calloc(12, sizeof(char)); 38 | arrput(arr, new_el); 39 | assertneq(new_el, NULL); 40 | free(new_el); 41 | } 42 | asserteq(arrlen(arr), 10); 43 | 44 | arrfree(arr); 45 | }; 46 | 47 | it ("Popping items from a stretchy buffer") { 48 | char** arr = NULL; 49 | 50 | asserteq(arr, NULL); 51 | asserteq(arrlen(arr), 0); 52 | 53 | for(int i = 0; i < 10; i++) { 54 | char *new_el = "aaaaa"; 55 | arrput(arr, new_el); 56 | assertneq(new_el, NULL); 57 | } 58 | // checking array reached desired length 59 | asserteq(arrlen(arr), 10); 60 | 61 | // checking shrinking by one element 62 | char *popped = arrpop(arr); 63 | asserteq(arrlen(arr), 9); 64 | assertneq(popped, NULL); 65 | 66 | // checking shrinking by one element 67 | popped = arrpop(arr); 68 | asserteq(arrlen(arr), 8); 69 | assertneq(popped, NULL); 70 | 71 | // checking shrinking by one element 72 | popped = arrpop(arr); 73 | asserteq(arrlen(arr), 7); 74 | assertneq(popped, NULL); 75 | 76 | // checking shrinking to zero elements 77 | arrdeln(arr, 0, 7); 78 | asserteq(arrlen(arr), 0); 79 | assertneq(arr, NULL); 80 | 81 | arrfree(arr); 82 | }; 83 | 84 | }; 85 | 86 | snow_main(); 87 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: c 2 | 3 | dist: trusty 4 | 5 | group: edge 6 | 7 | compiler: 8 | - clang 9 | - gcc-7 10 | - gcc-8 11 | 12 | before_install: 13 | - sudo pip3 install meson==0.44 14 | - git submodule init 15 | - git submodule update 16 | 17 | script: 18 | - meson --buildtype=debugoptimized --unity on build/ 19 | - ninja -C build/ 20 | - cd tests 21 | - meson build/ 22 | - meson test -C build/ -v --wrap 'valgrind --leak-check=full --error-exitcode=77' --test-args " --no-maybes --cr" 23 | 24 | after_script: 25 | - test -f /home/travis/build/mariusor/mpris-scrobbler/cov-int/scm_log.txt && cat /home/travis/build/mariusor/mpris-scrobbler/cov-int/scm_log.txt 26 | - test -f /home/travis/build/mariusor/mpris-scrobbler/tests/build/meson-logs/testlog-valgrind.txt && cat /home/travis/build/mariusor/mpris-scrobbler/tests/build/meson-logs/testlog-valgrind.txt 27 | 28 | env: 29 | global: 30 | secure: Yat5EL8wnIGlwvbsDDLx/U6xWHYCGWf4B1Oup+ESDBPlW11MbVB1duHBh3MCkHJAEC4BLnMzJoSaV6PhEyjzNugZ30XOf94aLHQCypM88s5vMB3tet5rPWjNNoaFiLcloqE11Xa2aEWSyqJe4LvXs7hJpxI0kT2rt2uVyOrTLRnXlSgRGvg5MFLDYLlHbCE/9KJzjPjQVdwdG1bRsPZCA/FG+XpFbL6QXvvGcYp+ZiEolZdonAvV6aA7aAIOkBkUwIlBvjz9qN368khq/tDSovoFyOBIqWnIECT1tevhQThv/iUMF2sCQWBICUqanwNhrmqSUp99IdMBZscB6l5H5rMfh67dzSab1EWRLwF7gmgsCKgxNLnLrCTg0vp+03VgRsz48v4blZMU1ucZWMNT98Rx1dobu9q71YF2/vDKHKXktiOWdiFCSeNFxXhgrpVGFOo781KJNiRf9B3ZtgmWMPeCe4tcmYtoF/tNQkI0KsHfdaXIIGJoAafUmTZF3u5uO3Msyf1NqvcsV/Rq6t1tw1y9OvB6Dus/iN/DZVh/gelq0dB1dfud5sCczd4SzddwLoULN1iXczClvRfU6nu1flHm7NzTcn6aQk7yJA10HyWSEgRaeBYTzCED2Qi5+fWmEG2gJMk8fhftBEqEa32OsAE6N+Vg9arnll0+3fZuUUM= 31 | 32 | notifications: 33 | webhooks: 34 | urls: 35 | - "https://scalar.vector.im/api/neb/services/hooks/dHJhdmlzLWNpLyU0MGxwYm0lM0FtYXRyaXgub3JnLyUyMWlSckNJUHVyU0tDcHhTdWl3SCUzQW1hdHJpeC5vcmc" 36 | on_success: change 37 | on_failure: always 38 | on_start: never 39 | 40 | addons: 41 | apt: 42 | sources: 43 | - sourceline: ppa:saiarcot895/chromium-dev 44 | - sourceline: ppa:ubuntu-toolchain-r/test 45 | packages: 46 | - gcc-7 47 | - gcc-8 48 | - dbus 49 | - libdbus-1-dev 50 | - libjson-c2 51 | - libjson-c-dev 52 | - curl 53 | - libevent-dev 54 | - python3-pip 55 | - ninja-build 56 | - valgrind 57 | coverity_scan: 58 | project: 59 | name: mariusor/mpris-scrobbler 60 | version: git 61 | description: Build submitted via Travis CI 62 | notification_email: marius@habarnam.ro 63 | build_command_prepend: export CC=clang; meson --buildtype=debugoptimized -Dversion=coverity-test --unity on cov-build/ 64 | build_command: ninja -C cov-build/ 65 | branch_pattern: coverity 66 | -------------------------------------------------------------------------------- /doc/mpris-scrobbler-credentials.5.scd: -------------------------------------------------------------------------------- 1 | MPRIS-SCROBBLER-CREDENTIALS(5) 2 | 3 | # NAME 4 | 5 | *mpris-scrobbler-credentials* - The MPRIS scrobbler daemon credentials file 6 | 7 | # SYNOPSIS 8 | 9 | _$XDG\_DATA\_HOME/mpris-scrobbler/credentials_ 10 | 11 | # DESCRIPTION 12 | 13 | This is the configuration file of the *mpris-scrobbler* daemon and is used to store the authentication 14 | credentials for the services that are enabled. The credentials are stored in plain text, so it's important 15 | that the file is not world readable. 16 | 17 | The file format supports multiple sections, each of them corresponding to a scrobbling service and which can 18 | contain multiple value assignments. 19 | 20 | A section is introduced by a line containing the service name enclosed in square brackets, so 21 | ``` 22 | [librefm] 23 | ``` 24 | would introduce a service called *librefm*. 25 | 26 | A value assignment is a single line that has the name of the value, an equals sign, and a setting for the value, so 27 | ``` 28 | enabled = true 29 | ``` 30 | would set the value named *enabled* in the current service section to *true*. Any spaces preceding and succeeding the name and the value are ignored. 31 | 32 | Any line starting with *;* is ignored, as is any blank line. 33 | 34 | # SERVICES 35 | 36 | The recognized service labels are: 37 | 38 | *listenbrainz* 39 | Used to store configuration settings related to the open *listenbrainz.org* platform. 40 | 41 | *librefm* 42 | Used to store configuration settings related to the open *libre.fm* platform. 43 | 44 | *lastfm* 45 | Used to store configuration settings related to the proprietary *last.fm* platform. 46 | 47 | # OPTIONS 48 | 49 | _enabled=_ 50 | Boolean value used to show if current service is active or not. Value can be *0*|*1* or 51 | *enabled*|*disabled* 52 | 53 | _username=_ 54 | String value containing the user name of the authenticated session. 55 | 56 | _token=_ 57 | String value containing the application *token* that the current session is authenticated with. 58 | 59 | _session=_ 60 | String value containing the application *session key* that the current user is authenticated with. 61 | 62 | _url=_ 63 | String value containing the _custom_ URL for the current service's end-point. The only services 64 | that support this option are *libre.fm* and *listenbrainz.org*. 65 | 66 | # EXAMPLE 67 | 68 | ``` 69 | [librefm] 70 | enabled = true 71 | username = 72 | token = 73 | session = 74 | 75 | [listenbrainz] 76 | enabled = true 77 | token = 78 | ``` 79 | 80 | # FILES 81 | 82 | _$XDG\_DATA\_HOME/mpris-scrobbler/credentials_ 83 | 84 | 85 | # SEE ALSO 86 | 87 | *mpris-scrobbler*(1) *mpris-scrobbler-signon*(1) 88 | -------------------------------------------------------------------------------- /src/smpris.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Marius Orcsik 3 | */ 4 | #ifndef MPRIS_SCROBBLER_SMPRIS_H 5 | #define MPRIS_SCROBBLER_SMPRIS_H 6 | 7 | #define MPRIS_PLAYBACK_STATUS_PLAYING "Playing" 8 | #define MPRIS_PLAYBACK_STATUS_PAUSED "Paused" 9 | #define MPRIS_PLAYBACK_STATUS_STOPPED "Stopped" 10 | 11 | static struct mpris_properties *mpris_properties_new(void) 12 | { 13 | struct mpris_properties *properties = calloc(1, sizeof(struct mpris_properties)); 14 | return properties; 15 | } 16 | 17 | static bool mpris_properties_is_playing(const struct mpris_properties *s) 18 | { 19 | return ( 20 | (NULL != s) && _eq(MPRIS_PLAYBACK_STATUS_PLAYING, s->playback_status) 21 | ); 22 | } 23 | 24 | static bool mpris_properties_is_paused(const struct mpris_properties *s) 25 | { 26 | return ( 27 | (NULL != s) && _eq(MPRIS_PLAYBACK_STATUS_PAUSED, s->playback_status) 28 | ); 29 | } 30 | 31 | static bool mpris_properties_is_stopped(const struct mpris_properties *s) 32 | { 33 | return ( 34 | (NULL != s) && _eq(MPRIS_PLAYBACK_STATUS_STOPPED, s->playback_status) 35 | ); 36 | } 37 | 38 | static bool mpris_player_is_playing (const struct mpris_player *player) 39 | { 40 | return mpris_properties_is_playing(&player->properties); 41 | } 42 | 43 | static enum playback_state get_mpris_playback_status(const struct mpris_properties *p) 44 | { 45 | enum playback_state state = stopped; 46 | if (mpris_properties_is_playing(p)) { 47 | state = playing; 48 | } 49 | if (mpris_properties_is_paused(p)) { 50 | state = paused; 51 | } 52 | if (mpris_properties_is_stopped(p)) { 53 | state = stopped; 54 | } 55 | return state; 56 | } 57 | 58 | static bool mpris_player_is_valid_name(char *name) 59 | { 60 | return (strlen(name) > 0); 61 | } 62 | 63 | static bool mpris_player_is_valid(const struct mpris_player *player) 64 | { 65 | return strlen(player->mpris_name) > 1 && strlen(player->name) > 0 && NULL != player->scrobbler; 66 | } 67 | 68 | static bool mpris_metadata_equals(const struct mpris_metadata *s, const struct mpris_metadata *p) 69 | { 70 | bool result = ( 71 | (!_is_zero(s->title) && !_is_zero(p->title) && _eq(s->title, p->title)) && 72 | (!_is_zero(s->album) && !_is_zero(p->album) && _eq(s->album, p->album)) && 73 | (!_is_zero(s->artist) && !_is_zero(p->artist) && _eq(s->artist, p->artist)) && 74 | (s->length == p->length) && 75 | (s->track_number == p->track_number) /*&& 76 | (s->start_time == p->start_time)*/ 77 | ); 78 | _trace2("mpris::check_metadata(%p:%p) %s", s, p, result ? "same" : "different"); 79 | 80 | return result; 81 | } 82 | 83 | static bool mpris_properties_equals(const struct mpris_properties *sp, const struct mpris_properties *pp) 84 | { 85 | if (NULL == sp) { return false; } 86 | if (NULL == pp) { return false; } 87 | if (sp == pp) { return true; } 88 | 89 | const bool result = mpris_metadata_equals(&sp->metadata, &pp->metadata) && 90 | strlen(sp->playback_status)+strlen(pp->playback_status) > 0 && 91 | _eq(sp->playback_status, pp->playback_status); 92 | 93 | _trace2("mpris::check_properties(%p:%p) %s", sp, pp, result ? "same" : "different"); 94 | return result; 95 | } 96 | 97 | static inline bool mpris_event_changed_playback_status(const struct mpris_event *ev) 98 | { 99 | return ev->loaded_state & mpris_load_property_playback_status; 100 | } 101 | 102 | static inline bool mpris_event_changed_track(const struct mpris_event *ev) 103 | { 104 | return ev->loaded_state > mpris_load_property_position; 105 | } 106 | 107 | static inline bool mpris_event_changed_volume(const struct mpris_event *ev) 108 | { 109 | return ev->loaded_state & mpris_load_property_volume; 110 | } 111 | 112 | static inline bool mpris_event_changed_position(const struct mpris_event *ev) 113 | { 114 | return ev->loaded_state & mpris_load_property_position; 115 | } 116 | 117 | #endif // MPRIS_SCROBBLER_SMPRIS_H 118 | -------------------------------------------------------------------------------- /src/ini.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Marius Orcsik 3 | */ 4 | #ifndef MPRIS_SCROBBLER_INI_H 5 | #define MPRIS_SCROBBLER_INI_H 6 | 7 | #include "ini_base.h" 8 | 9 | #define DEFAULT_GROUP_NAME "base" 10 | #define COMMENT_SEMICOLON ';' 11 | #define COMMENT_HASH '#' 12 | #define GROUP_OPEN '[' 13 | #define GROUP_CLOSE ']' 14 | #define EQUALS '=' 15 | #define EOL_LINUX '\n' 16 | #define EOL_OSX '\r' 17 | #define SPACE ' ' 18 | 19 | enum char_position { 20 | char_first = -1, 21 | char_last = 1, 22 | }; 23 | 24 | static int pos_char_lr(const char what, const char buff[], size_t buff_size, enum char_position pos) 25 | { 26 | int result = -1; 27 | 28 | assert(buff); 29 | 30 | char cur_char; 31 | int i = 0; 32 | 33 | while ((cur_char = buff[i]) != '\0') { 34 | if (i >= (int)buff_size) { break; } 35 | if (cur_char == what) { 36 | if (pos == char_last) { 37 | if (buff[i+1] != cur_char) { 38 | break; 39 | } else { 40 | continue; 41 | } 42 | } else { 43 | break; 44 | } 45 | } 46 | i++; 47 | } 48 | result = i; 49 | 50 | return result; 51 | } 52 | 53 | static long int first_pos_char(const char what, const char buff[], const size_t buff_size) 54 | { 55 | return pos_char_lr(what, buff, buff_size, char_first); 56 | } 57 | 58 | static long int last_pos_char(const char what, const char buff[], const size_t buff_size) 59 | { 60 | return pos_char_lr(what, buff, buff_size, char_last); 61 | } 62 | 63 | static int ini_parse(const char *buff, const size_t buff_size, struct ini_config *config) 64 | { 65 | int result = -1; 66 | 67 | assert(buff); 68 | assert(config); 69 | 70 | long int pos = 0; 71 | char cur_char; 72 | struct ini_group *group = NULL; 73 | 74 | while ((cur_char = buff[pos]) != '\0') { 75 | if (pos >= (int)buff_size) { break; } 76 | 77 | const char *cur_buff = &buff[pos]; 78 | 79 | const long int rem_buff_size = (long int)buff_size - pos; 80 | 81 | const long int line_len = first_pos_char(EOL_LINUX, cur_buff, (size_t)rem_buff_size); 82 | if (line_len < 0) { break; } 83 | 84 | // nothing special move to next char 85 | pos += line_len + 1; 86 | 87 | if (line_len == 0) { continue; } 88 | 89 | char line[1024] = {0}; 90 | memcpy(line, cur_buff, (size_t)line_len); 91 | 92 | /* comment */ 93 | if (cur_char == COMMENT_SEMICOLON || cur_char == COMMENT_HASH) { continue; } 94 | /* add new group */ 95 | if (cur_char == GROUP_OPEN) { 96 | const long int grp_end_pos = first_pos_char(GROUP_CLOSE, line, (size_t)line_len); 97 | if (grp_end_pos <= 0) { continue; } 98 | 99 | const long int name_len = grp_end_pos - 1; 100 | char name[1024] = {0}; 101 | memcpy(name, line + 1, (size_t)name_len); 102 | 103 | group = ini_group_new(name); 104 | ini_config_append_group(config, group); 105 | } 106 | if (NULL == group) { 107 | // if there isn't a group we create a default one 108 | group = ini_group_new(DEFAULT_GROUP_NAME); 109 | ini_config_append_group(config, group); 110 | } 111 | /* add new key = value pair to current group */ 112 | assert(NULL != group); 113 | 114 | const long int equal_pos = first_pos_char(EQUALS, line, (size_t)line_len); 115 | 116 | struct grrr_string *key_str = _grrrs_new_empty(1024); 117 | key_str->len = (uint32_t)equal_pos; 118 | memcpy(key_str->data, line, key_str->len); 119 | grrrs_trim(key_str->data, NULL); 120 | 121 | long int val_pos = equal_pos; 122 | long int val_len = (long int)line_len - equal_pos - 1; // subtract the eol 123 | 124 | const long int n_space_pos = last_pos_char(SPACE, line + val_pos, (size_t)line_len - key_str->len); 125 | 126 | if (n_space_pos >= 0) { 127 | val_pos += n_space_pos + 1; 128 | val_len -= n_space_pos; 129 | } 130 | if (val_len > 0) { 131 | struct grrr_string *val_str = _grrrs_new_empty(1024); 132 | val_str->len = (uint32_t)line_len; 133 | memcpy(val_str->data, line+val_pos, val_str->len); 134 | grrrs_trim(val_str->data, NULL); 135 | 136 | struct ini_value *value = ini_value_new(key_str->data, val_str->data); 137 | ini_group_append_value(group, value); 138 | free(val_str); 139 | } 140 | free(key_str); 141 | if (result < 0) { 142 | result = 0; 143 | } 144 | result++; 145 | } 146 | 147 | return result; 148 | } 149 | 150 | #endif // MPRIS_SCROBBLER_INI_H 151 | -------------------------------------------------------------------------------- /src/md5.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | 6 | // Implementation of https://en.wikipedia.org/wiki/MD5#Pseudocode 7 | // 8 | // shifts specifies the per-round shift amounts 9 | const uint32_t shifts[] = {7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 10 | 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 11 | 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 4, 12 | 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 13 | 4, 11, 16, 23, 6, 10, 15, 21, 6, 10, 15, 14 | 21, 6, 10, 15, 21, 6, 10, 15, 21}; 15 | 16 | // Constants are the integer part of the sines of integers (in radians) * 2^32. 17 | const uint32_t k[64] = { 18 | 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee , 19 | 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501 , 20 | 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be , 21 | 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821 , 22 | 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa , 23 | 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8 , 24 | 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed , 25 | 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a , 26 | 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c , 27 | 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70 , 28 | 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05 , 29 | 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665 , 30 | 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039 , 31 | 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1 , 32 | 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1 , 33 | 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391 }; 34 | 35 | #define LEFTROTATE(x, c) (((x) << (c)) | ((x) >> (32 - (c)))) 36 | 37 | static void to_bytes(const uint32_t val, uint8_t *bytes) 38 | { 39 | bytes[0] = (uint8_t) val; 40 | bytes[1] = (uint8_t) (val >> 8); 41 | bytes[2] = (uint8_t) (val >> 16); 42 | bytes[3] = (uint8_t) (val >> 24); 43 | } 44 | 45 | static uint32_t to_int32(const uint8_t *bytes) 46 | { 47 | return (uint32_t) bytes[0] 48 | | ((uint32_t) bytes[1] << 8) 49 | | ((uint32_t) bytes[2] << 16) 50 | | ((uint32_t) bytes[3] << 24); 51 | } 52 | 53 | static void md5(const uint8_t *message, const size_t length, uint8_t *digest) 54 | { 55 | // These vars will contain the hash 56 | uint32_t a0, b0, c0, d0; 57 | 58 | uint8_t *msg = NULL; 59 | 60 | size_t new_len, offset; 61 | uint32_t w[16] = {0}; 62 | 63 | // Initialize variables - simple count in nibbles: 64 | a0 = 0x67452301; 65 | b0 = 0xefcdab89; 66 | c0 = 0x98badcfe; 67 | d0 = 0x10325476; 68 | 69 | //Pre-processing: 70 | //append "1" bit to message 71 | //append "0" bits until message length in bits ≡ 448 (mod 512) 72 | //append length mod (2^64) to message 73 | 74 | for (new_len = length + 1; new_len % (512 / 8) != 448 / 8; new_len++); 75 | 76 | msg = (uint8_t*)malloc(new_len + 8); 77 | memcpy(msg, message, length); 78 | msg[length] = 0x80; // append the "1" bit; most significant bit is "first" 79 | 80 | for (offset = length + 1; offset < new_len; offset++) { 81 | msg[offset] = 0x0; // append "0" bits 82 | } 83 | 84 | // append the len in bits at the end of the buffer. 85 | to_bytes((uint32_t)length * 8, msg + new_len); 86 | // length >> 29 == length * 8 >> 32, but avoids overflow. 87 | to_bytes((uint32_t)length >> 29, msg + new_len + 4); 88 | 89 | // Process the message in successive 512-bit chunks: 90 | //for each 512-bit chunk of message: 91 | for (offset = 0; offset < new_len; offset += (512 / 8)) { 92 | // break chunk into sixteen 32-bit words w[i], 0 <= i <= 15 93 | for (size_t i = 0; i < 16; i++) { 94 | w[i] = to_int32(msg + offset + i * 4); 95 | } 96 | 97 | // Initialize hash value for this chunk: 98 | uint32_t a = a0; 99 | uint32_t b = b0; 100 | uint32_t c = c0; 101 | uint32_t d = d0; 102 | 103 | // Main loop: 104 | for(size_t i = 0; i < 64; i++) { 105 | uint32_t f, g; 106 | if (i < 16) { 107 | f = (b & c) | ((~b) & d); 108 | g = (uint32_t)i; 109 | } else if (i < 32) { 110 | f = (d & b) | ((~d) & c); 111 | g = (5*i + 1) % 16; 112 | } else if (i < 48) { 113 | f = b ^ c ^ d; 114 | g = (3*i + 5) % 16; 115 | } else { 116 | f = c ^ (b | (~d)); 117 | g = (7*i) % 16; 118 | } 119 | 120 | uint32_t temp = d; 121 | d = c; 122 | c = b; 123 | b = b + LEFTROTATE((a + f + k[i] + w[g]), shifts[i]); 124 | a = temp; 125 | } 126 | 127 | // Add this chunk's hash to result so far: 128 | a0 += a; 129 | b0 += b; 130 | c0 += c; 131 | d0 += d; 132 | } 133 | 134 | // cleanup 135 | free(msg); 136 | 137 | //digest[16] = a0 append b0 append c0 append d0 // (Output is in little-endian) 138 | to_bytes(a0, digest); 139 | to_bytes(b0, digest + 4); 140 | to_bytes(c0, digest + 8); 141 | to_bytes(d0, digest + 12); 142 | } 143 | -------------------------------------------------------------------------------- /src/ini_base.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Marius Orcsik 3 | */ 4 | #ifndef MPRIS_SCROBBLER_INI_BASE_H 5 | #define MPRIS_SCROBBLER_INI_BASE_H 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | typedef struct grrr_string* string; 13 | 14 | struct ini_value { 15 | struct ini_group *parent; 16 | string key; 17 | string value; 18 | }; 19 | 20 | struct ini_group { 21 | string name; 22 | struct ini_value **values; 23 | }; 24 | 25 | struct ini_config { 26 | struct ini_group **groups; 27 | }; 28 | 29 | static void ini_value_free(struct ini_value *value) 30 | { 31 | if (NULL == value) { return; } 32 | if (NULL != value->key) { free(value->key); } 33 | if (NULL != value->value) { free(value->value); } 34 | free(value); 35 | } 36 | 37 | static void ini_group_free(struct ini_group *group) 38 | { 39 | if (NULL == group) { return; } 40 | 41 | if (NULL != group->name) { free(group->name); } 42 | if (NULL != group->values) { 43 | const size_t count = arrlen(group->values); 44 | for (int i = (int)count - 1; i >= 0; i--) { 45 | ini_value_free(group->values[i]); 46 | (void)arrpop(group->values); 47 | group->values[i] = NULL; 48 | } 49 | assert(arrlen(group->values) == 0); 50 | arrfree(group->values); 51 | group->values = NULL; 52 | } 53 | free(group); 54 | } 55 | 56 | static void ini_config_clean (struct ini_config *conf) 57 | { 58 | if (NULL == conf->groups) { goto _free_sb; } 59 | 60 | const size_t count = arrlen(conf->groups); 61 | for (int i = (int)count - 1; i >= 0; i--) { 62 | ini_group_free(conf->groups[i]); 63 | (void)arrpop(conf->groups); 64 | conf->groups[i] = NULL; 65 | } 66 | assert(arrlen(conf->groups) == 0); 67 | _free_sb: 68 | arrfree(conf->groups); 69 | conf->groups = NULL; 70 | } 71 | 72 | static void ini_config_free(struct ini_config *conf) 73 | { 74 | if (NULL == conf) { return; } 75 | 76 | ini_config_clean(conf); 77 | 78 | free(conf); 79 | } 80 | 81 | static struct ini_value *ini_value_new(const char *key, const char *value) 82 | { 83 | struct ini_value *val = calloc(1, sizeof(struct ini_value)); 84 | 85 | if (NULL != key) { 86 | val->key = _grrrs_new_from_cstring(key); 87 | } 88 | if (NULL != value) { 89 | val->value = _grrrs_new_from_cstring(value); 90 | } 91 | 92 | return val; 93 | } 94 | 95 | static struct ini_group *ini_group_new(const char *group_name) 96 | { 97 | struct ini_group *group = calloc(1, sizeof(struct ini_group)); 98 | group->values = NULL; 99 | 100 | if (NULL != group_name) { 101 | group->name = _grrrs_new_from_cstring(group_name); 102 | } 103 | 104 | return group; 105 | } 106 | 107 | static struct ini_config *ini_config_new(void) 108 | { 109 | struct ini_config *conf = calloc(1, sizeof(struct ini_config)); 110 | conf->groups = NULL; 111 | 112 | return conf; 113 | } 114 | 115 | static void ini_group_append_value (struct ini_group *group, struct ini_value *value) 116 | { 117 | if (NULL == group) { return; } 118 | if (NULL == value) { return; } 119 | 120 | arrput(group->values, value); 121 | } 122 | 123 | static void ini_config_append_group (struct ini_config *conf, struct ini_group *group) 124 | { 125 | if (NULL == conf) { return; } 126 | if (NULL == group) { return; } 127 | 128 | arrput(conf->groups, group); 129 | } 130 | 131 | static void print_ini(const struct ini_config *conf) 132 | { 133 | if (NULL == conf) { return; } 134 | if (NULL == conf->groups) { return; } 135 | 136 | const size_t group_count = arrlen(conf->groups); 137 | for (size_t i = 0; i < group_count; i++) { 138 | printf ("[%s]\n", conf->groups[i]->name->data); 139 | 140 | if (NULL == conf->groups[i]->values) { return; } 141 | const size_t value_count = arrlen(conf->groups[i]->values); 142 | for (size_t j = 0; j < value_count; j++) { 143 | printf(" %s = %s\n", conf->groups[i]->values[j]->key->data, conf->groups[i]->values[j]->value->data); 144 | } 145 | } 146 | } 147 | 148 | static int write_ini_file(const struct ini_config *config, FILE *file) 149 | { 150 | if (NULL == file) { return ENOENT; } 151 | 152 | int status = EINVAL; 153 | if (NULL == config) { return status; } 154 | 155 | status = 0; 156 | 157 | if (NULL != config->groups) { 158 | const size_t group_count = arrlen(config->groups); 159 | for (size_t i = 0; i < group_count; i++) { 160 | fprintf (file, "[%s]\n", config->groups[i]->name->data); 161 | 162 | if (NULL != config->groups[i]->values) { 163 | const size_t value_count = arrlen(config->groups[i]->values); 164 | for (size_t j = 0; j < value_count; j++) { 165 | fprintf(file, "%s = %s\n", config->groups[i]->values[j]->key->data, config->groups[i]->values[j]->value->data); 166 | } 167 | } 168 | fprintf(file, "\n"); 169 | } 170 | } 171 | 172 | return status; 173 | } 174 | 175 | #endif // MPRIS_SCROBBLER_INI_BASE_H 176 | -------------------------------------------------------------------------------- /tests/ini_reader_test.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define MAX_PROPERTY_LENGTH 512 4 | #define string_free free 5 | #define get_zero_string(len) calloc(1, (sizeof(char) + 1) * len) 6 | 7 | #define STB_DS_IMPLEMENTATION 8 | #include "sstrings.h" 9 | #include "stb_ds.h" 10 | #include "ini.h" 11 | 12 | #define MAX_FILE_SIZE 1048576 13 | #define array_len(A) (sizeof(A)/sizeof(A[0])) 14 | #define min(A,B) ((A) > (B) ? B : A) 15 | 16 | static int load_file(FILE *file, char buff[MAX_FILE_SIZE]) 17 | { 18 | long file_size = 0l; 19 | if (NULL == file) { goto _exit; } 20 | 21 | fseek(file, 0L, SEEK_END); 22 | file_size = ftell(file); 23 | file_size = min(file_size, MAX_FILE_SIZE); 24 | 25 | if (file_size <= 0) { goto _exit; } 26 | rewind(file); 27 | 28 | fread(buff, file_size, 1, file); 29 | 30 | _exit: 31 | return file_size; 32 | } 33 | 34 | struct element_test { 35 | const char *key; 36 | const char *value; 37 | }; 38 | 39 | struct group_test { 40 | const char *name; 41 | const int element_count; 42 | const struct element_test elements[10]; 43 | }; 44 | 45 | struct test_pair { 46 | const char *path; 47 | const int group_count; 48 | const struct group_test groups[10]; 49 | }; 50 | 51 | const struct test_pair tests[2] = { 52 | { 53 | .path = "../mocks/simple.ini", 54 | .group_count = 1, 55 | .groups = { 56 | { 57 | .element_count = 1, 58 | .name = "test", 59 | .elements = { 60 | { 61 | .key = "key", 62 | .value = "value", 63 | }, 64 | }, 65 | }, 66 | }, 67 | }, 68 | { 69 | .path = "../mocks/credentials.ini", 70 | .group_count = 3, 71 | .groups = { 72 | { 73 | .element_count = 4, 74 | .name = "one", 75 | .elements = { 76 | { 77 | .key = "enabled", 78 | .value = "true", 79 | }, 80 | { 81 | .key = "username", 82 | .value = "tester", 83 | }, 84 | { 85 | .key = "token", 86 | .value = "f00b4r", 87 | }, 88 | { 89 | .key = "session", 90 | .value = "kerfuffle", 91 | }, 92 | }, 93 | }, 94 | { 95 | .element_count = 4, 96 | .name = "two", 97 | .elements = { 98 | { 99 | .key = "enabled", 100 | .value = "1", 101 | }, 102 | { 103 | .key = "username", 104 | .value = "s!mb4", 105 | }, 106 | { 107 | .key = "token", 108 | .value = "test!321@@321", 109 | }, 110 | { 111 | .key = "session", 112 | .value = "rwar32test", 113 | }, 114 | }, 115 | }, 116 | { 117 | .element_count = 2, 118 | .name = "and-three", 119 | .elements = { 120 | { 121 | .key = "enabled", 122 | .value = "false", 123 | }, 124 | { 125 | .key = "token", 126 | .value = "f00d-ale-c0ffee-41f-6419418c768a" 127 | }, 128 | }, 129 | }, 130 | }, 131 | }, 132 | }; 133 | 134 | describe(ini_reader) { 135 | FILE *file = NULL; 136 | 137 | after_each() { 138 | if (NULL != file) { fclose(file); } 139 | }; 140 | 141 | for (size_t __test_key = 0; __test_key < array_len(tests); __test_key++) { 142 | const struct test_pair test = tests[__test_key]; 143 | const char *path = test.path; 144 | const int group_count = test.group_count; 145 | 146 | it ("opens ini file") { 147 | file = fopen(path, "r"); 148 | if (NULL == file) { 149 | break; 150 | } 151 | }; 152 | 153 | it ("reads ini file") { 154 | char buff[MAX_FILE_SIZE]; 155 | memset(&buff, '\0', MAX_FILE_SIZE); 156 | 157 | file = fopen(path, "re"); 158 | 159 | load_file(file, buff); 160 | 161 | assertneq(strlen(buff), 0); 162 | }; 163 | 164 | it ("parse ini file") { 165 | char buff[MAX_FILE_SIZE]; 166 | memset(&buff, '\0', MAX_FILE_SIZE); 167 | file = fopen(path, "re"); 168 | long buff_size = load_file(file, buff); 169 | 170 | struct ini_config config = { .groups = NULL, }; 171 | ini_parse(buff, buff_size, &config); 172 | 173 | assertneq(config.groups, NULL); 174 | assertneq(arrlen(config.groups), 0); 175 | asserteq(arrlen(config.groups), group_count); 176 | 177 | for (unsigned int i = 0; i < arrlen(config.groups); i++) { 178 | struct ini_group *group = config.groups[i]; 179 | 180 | assertneq(group, NULL); 181 | assertneq(group->name, NULL); 182 | asserteq(strncmp(group->name->data, test.groups[i].name, 100), 0); 183 | 184 | assertneq(group->values, NULL); 185 | assertneq(arrlen(group->values), 0); 186 | 187 | asserteq(arrlen(group->values), test.groups[i].element_count); 188 | 189 | for (unsigned int j = 0; j < arrlen(group->values); j++) { 190 | struct ini_value *value = group->values[j]; 191 | 192 | assertneq(value, NULL); 193 | assertneq(value->key, NULL); 194 | assertneq(value->value, NULL); 195 | 196 | asserteq(strncmp(value->key->data, test.groups[i].elements[j].key, 100), 0); 197 | asserteq(strncmp(value->value->data, test.groups[i].elements[j].value, 100), 0); 198 | 199 | asserteq(strncmp(value->key->data, test.groups[i].elements[j].key, 100), 0); 200 | asserteq(strncmp(value->value->data, test.groups[i].elements[j].value, 100), 0); 201 | } 202 | } 203 | if (NULL != config.groups) { ini_config_clean(&config); } 204 | }; 205 | } 206 | }; 207 | 208 | snow_main(); 209 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('mpris-scrobbler', 'c', 2 | default_options : [ 3 | 'c_std=c11', 4 | 'buildtype=debug', 5 | 'warning_level=everything', 6 | 'unity=on', 7 | ], 8 | license : 'MIT') 9 | 10 | 11 | bin_name = meson.project_name() 12 | c_args = [ 13 | '-DAPPLICATION_NAME="' + bin_name + '"', 14 | '-fstack-clash-protection', 15 | '-Wformat', 16 | '-Wno-switch-enum', # when libcurldebug is enabled this needs to be set 17 | '-Wstrict-overflow', 18 | '-Wno-unused-function', # not even go goes this far 19 | '-Wno-c++-compat', # we want assign from (void*) w/o cast 20 | '-Wno-cast-qual', # we sometimes want to allow const strings to be operated on 21 | '-Wno-padded', # sometimes you just can't do without 22 | '-Wno-null-dereference', # the grrrs strlen uses *s++ which triggers this. 23 | ## we should look at these to fix 24 | '-Wno-format-nonliteral', # we use a non string literal for the format of a snprintf to build the authorization url 25 | ] 26 | 27 | if build_machine.cpu_family() in ['x86', 'x86_64'] 28 | # this is supported only on x86 and x86_64, and we add it because it's a default 29 | # CFLAG for Archlinux's Makepkg 30 | add_project_arguments('-fcf-protection=full', language : 'c') 31 | endif 32 | 33 | if meson.get_compiler('c').get_id() == 'clang' 34 | clang_extra_args = [ 35 | '-Wno-declaration-after-statement', 36 | '-Wno-unsafe-buffer-usage', 37 | '-Wno-extra-semi-stmt', 38 | '-Wno-covered-switch-default', 39 | ] 40 | add_project_arguments(clang_extra_args, language : 'c') 41 | endif 42 | if meson.get_compiler('c').get_id() == 'gcc' 43 | gcc_extra_args = [ 44 | '--param=inline-min-speedup=2', 45 | '--param=max-inline-insns-auto=80', 46 | '-Wsuggest-attribute=const', 47 | '-Wno-alloc-zero', # stb arrfree uses realloc with 0 bytes 48 | '-Wno-suggest-attribute=pure', # no attributes 49 | '-Wno-suggest-attribute=malloc', # no attributes 50 | '-Wno-suggest-attribute=format', # no attributes 51 | ] 52 | add_project_arguments(gcc_extra_args, language : 'c') 53 | endif 54 | 55 | if get_option('buildtype') == 'debug' or get_option('debug') == true 56 | add_project_arguments('-DDEBUG', language : 'c') 57 | #add_project_arguments('-DRETRY_ENABLED', language : 'c') 58 | if get_option('libeventdebug') == true 59 | add_project_arguments('-DLIBEVENT_DEBUG', language : 'c') 60 | endif 61 | if get_option('libcurldebug') == true 62 | add_project_arguments('-DLIBCURL_DEBUG', language : 'c') 63 | endif 64 | if get_option('libdbusdebug') == true 65 | add_project_arguments('-DLIBDBUS_DEBUG', language : 'c') 66 | endif 67 | endif 68 | 69 | deps = [ 70 | dependency('dbus-1', required : true, version : '>=1.9'), 71 | dependency('libcurl', required : true), 72 | dependency('libevent_pthreads', required : true), 73 | dependency('libevent', required : true), 74 | dependency('json-c', required : true), 75 | ] 76 | 77 | version_hash = get_option('version') 78 | 79 | credentials = configuration_data() 80 | credentials.set('lastfm_api_key', get_option('lastfm_api_key')) 81 | credentials.set('lastfm_api_secret', get_option('lastfm_api_secret')) 82 | credentials.set('librefm_api_key', get_option('librefm_api_key')) 83 | credentials.set('librefm_api_secret', get_option('librefm_api_secret')) 84 | credentials.set('listenbrainz_api_key', get_option('listenbrainz_api_key')) 85 | credentials.set('listenbrainz_api_secret', get_option('listenbrainz_api_secret')) 86 | configure_file(input : 'src/credentials_lastfm.h.in', 87 | output : 'credentials_lastfm.h', 88 | configuration : credentials) 89 | configure_file(input : 'src/credentials_librefm.h.in', 90 | output : 'credentials_librefm.h', 91 | configuration : credentials) 92 | configure_file(input : 'src/credentials_listenbrainz.h.in', 93 | output : 'credentials_listenbrainz.h', 94 | configuration : credentials) 95 | 96 | 97 | prefixdir = get_option('prefix') 98 | if not prefixdir.startswith('/') 99 | error('Prefix is not absolute: "@0@"'.format(prefixdir)) 100 | endif 101 | bindir = join_paths(prefixdir, get_option('bindir')) 102 | unitdir = join_paths(prefixdir, get_option('unitdir')) 103 | 104 | srcdir = include_directories('src') 105 | 106 | daemon_sources = ['src/daemon.c'] 107 | signon_sources = ['src/signon.c'] 108 | 109 | git = find_program('git', required : version_hash == '') 110 | if git.found() 111 | version_h = vcs_tag( 112 | input : 'src/version.h.in', 113 | output : 'version.h', 114 | replace_string : '@GIT_VERSION@', 115 | command : ['git', 'describe', '--tags', '--long', '--dirty=-git', '--always'], 116 | fallback : '(unknown)') 117 | deps += declare_dependency(sources : version_h) 118 | endif 119 | if version_hash != '' 120 | add_project_arguments('-DVERSION_HASH="' + version_hash + '"', language : 'c') 121 | endif 122 | 123 | 124 | executable('mpris-scrobbler', 125 | daemon_sources, 126 | c_args : c_args, 127 | include_directories : srcdir, 128 | install : true, 129 | install_dir : bindir, 130 | dependencies : deps 131 | ) 132 | executable('mpris-scrobbler-signon', 133 | signon_sources, 134 | c_args : c_args + ['-D_POSIX_C_SOURCE=200809L'], 135 | include_directories : srcdir, 136 | install : true, 137 | install_dir : bindir, 138 | dependencies : deps 139 | ) 140 | 141 | ctags = find_program('ctags', required : false) 142 | if ctags.found() 143 | run_target('ctags', command : [ctags, '-f', '../tags', '--tag-relative=never', '-R', '../src', '/usr/include/dbus-1.0/dbus/', '/usr/include/event2/', '/usr/include/curl']) 144 | endif 145 | 146 | m4_bin = find_program('m4', required : false) 147 | if m4_bin.found() 148 | unit = custom_target('systemd-service', 149 | input : 'units/systemd-user.service.in', 150 | output : bin_name + '.service', 151 | capture : true, 152 | command : [ 153 | m4_bin, 154 | '-P', 155 | '-DBINPATH=' + bindir + '/', 156 | '-DDAEMONNAME=' + bin_name, 157 | '@INPUT@' 158 | ], 159 | install : true, 160 | install_dir : unitdir 161 | ) 162 | endif 163 | 164 | scdoc = find_program('scdoc', required : false) 165 | 166 | if scdoc.found() 167 | sh = find_program('sh') 168 | mandir = get_option('mandir') 169 | man_files = [ 170 | 'doc/mpris-scrobbler.1.scd', 171 | 'doc/mpris-scrobbler-signon.1.scd', 172 | 'doc/mpris-scrobbler-credentials.5.scd', 173 | 'doc/mpris-scrobbler-config.5.scd', 174 | ] 175 | foreach filename : man_files 176 | topic = filename.split('.')[-3].split('/')[-1] 177 | section = filename.split('.')[-2] 178 | output = '@0@.@1@'.format(topic, section) 179 | 180 | custom_target( 181 | output, 182 | input : filename, 183 | output : output, 184 | command : [ 185 | sh, '-c', '@0@ < @INPUT@ > @1@'.format(scdoc.full_path(), output) 186 | ], 187 | install : true, 188 | install_dir : '@0@/man@1@'.format(mandir, section) 189 | ) 190 | endforeach 191 | endif 192 | 193 | -------------------------------------------------------------------------------- /src/sstrings.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Marius Orcsik 3 | */ 4 | #ifndef MPRIS_SCROBBLER_SSTRINGS_H 5 | #define MPRIS_SCROBBLER_SSTRINGS_H 6 | 7 | #ifdef DEBUG 8 | #include 9 | #else 10 | #ifndef assert 11 | #define assert(A) 12 | #endif 13 | #endif 14 | 15 | #include 16 | #include 17 | 18 | #ifndef grrrs_std_alloc 19 | #include 20 | #define grrrs_std_alloc malloc 21 | #endif 22 | 23 | #ifndef grrrs_std_realloc 24 | #include 25 | #define grrrs_std_realloc realloc 26 | #endif 27 | 28 | #ifndef grrrs_std_free 29 | #include 30 | #define grrrs_std_free free 31 | #endif 32 | 33 | #ifndef GRRRS_OOM 34 | #define GRRRS_OOM 35 | #endif 36 | 37 | #ifndef GRRRS_ERR 38 | #define GRRRS_ERR(...) 39 | #endif 40 | 41 | #define internal static 42 | #define _VOID(A) (NULL == (A)) 43 | #define _OKP(A) (NULL != (A)) 44 | #define _GRRRS_NULL_TOP_PTR (ptrdiff_t)(-2 * (ptrdiff_t)sizeof(uint32_t)) 45 | 46 | #define _grrr_sizeof(C) (sizeof(struct grrr_string) + ((size_t)(C+1) * sizeof(char))) 47 | 48 | #define grrrs_from_string(A) (_VOID(A) ? \ 49 | (char *)&_grrrs_new_empty(0)->data : \ 50 | (char *)&_grrrs_new_from_cstring(A)->data) 51 | 52 | #define grrrs_new(A) (char*)&(_grrrs_new_empty(A)->data) 53 | #define grrrs_free(A) _grrrs_free(A) 54 | 55 | // TODO(marius): investigate how this aligns 56 | struct grrr_string { 57 | uint32_t len; /* currently used size of data[] */ 58 | uint32_t cap; /* available size for data[] */ 59 | char data[]; 60 | }; 61 | 62 | //typedef struct grrr_string grrrs; 63 | 64 | internal struct grrr_string *_grrrs_ptr(char *s) 65 | { 66 | return (void*)(s + _GRRRS_NULL_TOP_PTR); 67 | } 68 | 69 | internal void _grrrs_free(void *s) 70 | { 71 | if (_VOID(s)) { return; } 72 | 73 | struct grrr_string *gs = _grrrs_ptr(s); 74 | 75 | if (s != (void*)&gs->data) { 76 | grrrs_std_free(s); 77 | return; 78 | } 79 | grrrs_std_free(gs); 80 | } 81 | 82 | static struct grrr_string *_grrrs_new_empty(const size_t cap) 83 | { 84 | struct grrr_string *result = grrrs_std_alloc(_grrr_sizeof(cap)); 85 | if (_VOID(result)) { 86 | GRRRS_OOM; 87 | return NULL; 88 | } 89 | 90 | result->len = 0; 91 | result->cap = (uint32_t)cap; 92 | for (size_t i = 0; i <= cap; i++) { 93 | result->data[i] = '\0'; 94 | } 95 | 96 | return result; 97 | } 98 | 99 | internal uint32_t __strlen(const char *s) 100 | { 101 | if (_VOID(s)) { return 0; } 102 | 103 | uint32_t result = 0; 104 | 105 | while (*s++ != '\0') { result++; } 106 | 107 | return result; 108 | } 109 | 110 | static void __cstrncpy(char *dest, const char *src, uint32_t len) 111 | { 112 | if (_VOID(dest)) { 113 | return; 114 | } 115 | if (_VOID(src)) { 116 | return; 117 | } 118 | for (uint32_t i = 0; i < len; i++) { 119 | dest[i] = src[i]; 120 | } 121 | dest[len] = '\0'; 122 | } 123 | 124 | internal struct grrr_string *_grrrs_new_from_cstring(const char* s) 125 | { 126 | const uint32_t len = __strlen(s); 127 | struct grrr_string *result = grrrs_std_alloc(_grrr_sizeof(len)); 128 | if (_VOID(result)) { 129 | GRRRS_OOM; 130 | return (void*)_GRRRS_NULL_TOP_PTR; 131 | } 132 | 133 | result->len = len; 134 | result->cap = len; 135 | 136 | __cstrncpy(result->data, s, result->len); 137 | 138 | return result; 139 | } 140 | 141 | static uint32_t grrrs_cap(const char* s) 142 | { 143 | #ifdef DEBUG 144 | assert(_OKP(s)); 145 | #endif 146 | struct grrr_string *gs = _grrrs_ptr((char*)s); 147 | 148 | #ifdef DEBUG 149 | assert(_OKP(gs)); 150 | assert(__strlen(s) <= gs->cap); 151 | #endif 152 | return gs->cap; 153 | } 154 | 155 | static uint32_t grrrs_len(const char* s) 156 | { 157 | #ifdef DEBUG 158 | assert(_OKP(s)); 159 | #endif 160 | struct grrr_string *gs = _grrrs_ptr((char*)s); 161 | 162 | #ifdef DEBUG 163 | assert(_OKP(gs)); 164 | #endif 165 | 166 | #ifdef DEBUG 167 | assert(gs->data == s); 168 | assert(__strlen(gs->data) == gs->len); 169 | #endif 170 | return gs->len; 171 | } 172 | 173 | static int __grrrs_cmp(const struct grrr_string *s1, const struct grrr_string *s2) 174 | { 175 | assert(_OKP(s1)); 176 | assert(_OKP(s2)); 177 | 178 | if (s1->len != s2->len) { 179 | return (int32_t)s1->len - (int32_t)s2->len; 180 | } 181 | for (uint32_t i = 0; i < s1->len; i++) { 182 | if (s1->data[i] == '\0') { 183 | GRRRS_ERR("NULL value in string data before length[%" PRIu32 ":%" PRIu32 "]", i, s1->len); 184 | } 185 | if (s2->data[i] == '\0') { 186 | GRRRS_ERR("NULL value in string data before length[%" PRIu32 ":%" PRIu32 "]", i, s2->len); 187 | } 188 | if (s1->data[i] != s2->data[i]) { 189 | return s1->data[i] - s2->data[i]; 190 | } 191 | } 192 | return 0; 193 | } 194 | 195 | internal struct grrr_string *__grrrs_resize(struct grrr_string *gs, uint32_t new_cap) 196 | { 197 | #ifdef DEBUG 198 | assert(_OKP(gs)); 199 | #endif 200 | if (new_cap < gs->len) { 201 | GRRRS_ERR("new cap should be larger than existing length %" PRIu32 " \n", gs->len); 202 | } 203 | // TODO(marius): cover the case where new_cap is smaller than gs->len 204 | // and maybe when it's smaller than gs->cap 205 | gs = grrrs_std_realloc(gs, _grrr_sizeof(new_cap)); 206 | if (_VOID(gs)) { 207 | GRRRS_OOM ; 208 | return (void*)_GRRRS_NULL_TOP_PTR; 209 | } 210 | if ((uint32_t)new_cap < gs->cap) { 211 | // ensure existing string is null terminated 212 | gs->data[new_cap] = '\0'; 213 | } 214 | 215 | for (unsigned i = gs->cap; i <= new_cap; i++) { 216 | // ensure that the new capacity is zeroed 217 | gs->data[i] = '\0'; 218 | } 219 | gs->cap = new_cap; 220 | 221 | return gs; 222 | } 223 | 224 | static void *_grrrs_resize(void *s, uint32_t new_cap) 225 | { 226 | assert(_OKP(s)); 227 | struct grrr_string *gs = _grrrs_ptr((char*)s); 228 | return __grrrs_resize(gs, new_cap)->data; 229 | } 230 | 231 | static void *_grrrs_trim_left(char *s, const char *c) 232 | { 233 | char *result = s; 234 | char *to_trim = NULL; 235 | if (_VOID(s)) { return result; } 236 | 237 | struct grrr_string *gs = _grrrs_ptr(s); 238 | if (_VOID(gs)) { return result; } 239 | if (gs->len > 100000) { 240 | gs->len = __strlen(s); 241 | } 242 | 243 | if (_VOID(c)) { 244 | to_trim = _grrrs_new_from_cstring(" \t\r\n")->data; 245 | } else { 246 | to_trim = _grrrs_new_from_cstring(c)->data; 247 | } 248 | const uint32_t len_to_trim = grrrs_len(to_trim); 249 | 250 | int32_t trim_end = -1; 251 | uint32_t new_len = gs->len; 252 | for (uint32_t i = 0; i < gs->len; i++) { 253 | for (uint32_t j = 0; j < len_to_trim; j++) { 254 | const char t = to_trim[j]; 255 | if (gs->data[i] == '\0') { 256 | break; 257 | } 258 | if (gs->data[i] == t) { 259 | new_len--; 260 | break; 261 | } 262 | if (j == len_to_trim - 1) { 263 | trim_end = (int32_t)i; 264 | } 265 | } 266 | if (trim_end >= 0) { 267 | break; 268 | } 269 | } 270 | if (new_len == gs->len) { 271 | goto _to_trim_free; 272 | } 273 | 274 | char *temp = grrrs_std_alloc((new_len+1)*sizeof(char)); 275 | if (trim_end < 0) { trim_end = 0; } 276 | 277 | for (uint32_t k = 0; k < new_len; k++) { 278 | temp[k] = gs->data[(uint32_t)trim_end + k]; 279 | } 280 | for (uint32_t k = 0; k < new_len; k++) { 281 | gs->data[k] = temp[k]; 282 | } 283 | for (uint32_t k = new_len; k < gs->len; k++) { 284 | gs->data[k] = '\0'; 285 | } 286 | gs->len = (uint32_t)new_len; 287 | grrrs_std_free(temp); 288 | 289 | _to_trim_free: 290 | _grrrs_free(to_trim); 291 | 292 | return result; 293 | } 294 | 295 | static void *_grrrs_trim_right(char *s, const char *c) 296 | { 297 | char *result = s; 298 | char *to_trim = NULL; 299 | if (_VOID(s)) { return result; } 300 | 301 | struct grrr_string *gs = _grrrs_ptr(s); 302 | if (_VOID(gs)) { return result; } 303 | if (gs->len > 100000) { 304 | gs->len = __strlen(s); 305 | } 306 | 307 | if (_VOID(c)) { 308 | to_trim = _grrrs_new_from_cstring("\r \t\n")->data; 309 | } else { 310 | to_trim = _grrrs_new_from_cstring(c)->data; 311 | } 312 | uint32_t len_to_trim = grrrs_len(to_trim); 313 | 314 | //assert(gs->len, len(gs->data); 315 | 316 | int8_t stop = 0; 317 | int32_t new_len = (int32_t)gs->len; 318 | for (int32_t i = (int32_t)gs->len - 1; i >= 0; i--) { 319 | for (uint32_t j = 0; j < len_to_trim; j++) { 320 | const char t = to_trim[j]; 321 | // if we encounter \0 on the right side, we consider the string terminated 322 | // and we save the new length 323 | if (gs->data[i] == '\0') { 324 | new_len = i + 1; 325 | break; 326 | } 327 | if (gs->data[i] == t) { 328 | gs->data[i] = '\0'; 329 | new_len--; 330 | break; 331 | } 332 | if (j == len_to_trim - 1) { 333 | stop = 1; 334 | } 335 | } 336 | if (stop) { 337 | break; 338 | } 339 | } 340 | if (new_len == (int32_t)gs->len) { 341 | goto _to_trim_free; 342 | } 343 | gs->len = (uint32_t)new_len; 344 | 345 | _to_trim_free: 346 | _grrrs_free(to_trim); 347 | 348 | return result; 349 | } 350 | 351 | #define grrrs_trim(A, B) _grrrs_trim_right(_grrrs_trim_left((A), (B)), (B)) 352 | 353 | #endif // MPRIS_SCROBBLER_SSTRINGS_H 354 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mpris-scrobbler 2 | 3 | Is a minimalistic user daemon which submits the currently playing song to libre.fm and compatible services. 4 | To retrieve song information it uses the MPRIS DBus interface, so it works with any media player that exposes this interface. 5 | 6 | [![MIT Licensed](https://img.shields.io/github/license/mariusor/mpris-scrobbler.svg)](https://raw.githubusercontent.com/mariusor/mpris-scrobbler/master/LICENSE) 7 | [![Build status](https://builds.sr.ht/~mariusor/mpris-scrobbler.svg)](https://builds.sr.ht/~mariusor/mpris-scrobbler) 8 | [![Coverity Scan status](https://img.shields.io/coverity/scan/14230.svg)](https://scan.coverity.com/projects/14230) 9 | [![Latest build](https://img.shields.io/github/release/mariusor/mpris-scrobbler.svg)](https://github.com/mariusor/mpris-scrobbler/releases/latest) 10 | [![AUR package](https://img.shields.io/aur/version/mpris-scrobbler.svg)](https://aur.archlinux.org/packages/mpris-scrobbler/) 11 | 12 | In order to compile the application you must have a valid development environment containing pkg-config, a compiler - known to work are `clang>=5.0` or `gcc>=7.0` - and the build system `meson` plus `ninja`. 13 | 14 | The compile time dependencies are: `libevent`, `dbus-1.0>=1.9`, `libcurl`, `json-c` and their development equivalent packages. 15 | 16 | ## Getting the source 17 | 18 | You can clone the git repository or download the latest release from [here](https://github.com/mariusor/mpris-scrobbler/releases/latest). 19 | 20 | $ git clone git@github.com:mariusor/mpris-scrobbler.git 21 | $ cd mpris-scrobbler 22 | 23 | ## Installing 24 | 25 | For **packagers** please see the note at the [bottom](#packaging). 26 | 27 | ### CentOS, RHEL 28 | 29 | `mpris-scrobbler` is available for CentOS and RHEL 7 or later via the [EPEL repository](https://fedoraproject.org/wiki/EPEL "EPEL: Extra Packages for Enterprise Linux"). 30 | Run the following commands to install it: 31 | 32 | ```sh 33 | $ sudo yum install epel-release 34 | $ sudo yum install mpris-scrobbler 35 | ``` 36 | 37 | ### Fedora 38 | 39 | `mpris-scrobbler` is available since Fedora 28. 40 | Run the following command to install it: 41 | 42 | ```sh 43 | $ sudo dnf install mpris-scrobbler 44 | ``` 45 | 46 | ### Mageia, openSUSE 47 | 48 | `mpris-scrobbler` is available for Mageia, openSUSE, and other RPM distributions via the [COPR repository](https://copr.fedorainfracloud.org/coprs/jflory7/mpris-scrobbler/). 49 | First, install the correct repository for your operating system from COPR. 50 | Then, install the `mpris-scrobbler` package with your package manager of choice. 51 | 52 | ### Ubuntu 53 | 54 | Install the dependencies: 55 | 56 | #### Ubuntu 18.04 57 | 58 | ```sh 59 | sudo apt install libevent-2.1-6 libevent-dev libdbus-1-dev dbus dbus-user-session \ 60 | libcurl4 libcurl4-openssl-dev libjson-c-dev libjson-c3 meson 61 | ``` 62 | 63 | #### Ubuntu 20.04 64 | 65 | ```sh 66 | sudo apt install libevent-2.1-7 libevent-dev libdbus-1-dev dbus dbus-user-session \ 67 | libcurl4 libcurl4-openssl-dev libjson-c-dev libjson-c-dev meson m4 scdoc 68 | ``` 69 | 70 | D-bus will need to be restarted: 71 | 72 | $ systemctl --user restart dbus.service 73 | 74 | ### Compile from source 75 | 76 | To compile the scrobbler manually, you need to already have installed the dependencies mentioned above. 77 | By default the prefix for the installation is `/usr`. 78 | 79 | $ meson setup build/ 80 | 81 | $ ninja -C build/ 82 | 83 | $ sudo ninja -C build/ install 84 | 85 | ## Usage 86 | 87 | The scrobbler is comprised of two binaries: the daemon and the signon helper. 88 | 89 | The daemon is meant run as a user systemd service which listens for any signals coming from your MPRIS enabled media player. To have it start at login, please execute the following command: 90 | 91 | $ systemctl --user enable --now mpris-scrobbler.service 92 | 93 | If the command above didn't start the service, you can do it manually: 94 | 95 | $ systemctl --user start mpris-scrobbler.service 96 | 97 | It can submit the tracks being played to the [last.fm](https://last.fm) and [libre.fm](https://libre.fm) services, and to [listenbrainz.org](https://listenbrainz.org/). 98 | 99 | At first nothing will get submitted as you need to enable one or more of the available services and also generate a valid API session for your account. 100 | 101 | The valid services that mpris-scrobbler knows are: `librefm`, `lastfm` and `listenbrainz`. 102 | 103 | ### Enabling a service 104 | 105 | Enabling a service is done automatically once you obtain a valid token/session for it. See the [authentication](#authenticate-to-the-service) section. 106 | 107 | You can however disable submitting tracks to a service by invoking: 108 | 109 | $ mpris-scrobbler-signon disable 110 | 111 | If you want to re-enable a service which you previously disabled you can call, without the need to re-authenticate: 112 | 113 | $ mpris-scrobbler-signon enable 114 | 115 | ### Authenticate to the service 116 | 117 | ##### ListenBrainz 118 | 119 | Because ListenBrainz doesn't have yet support for OAuth authentication, the credentials must be added manually using the signon binary. 120 | First you need to get the **user token** from your [ListenBrainz profile page](https://listenbrainz.org/profile). 121 | Then you call the following command and type or paste the token, then press Enter: 122 | 123 | $ mpris-scrobbler-signon token listenbrainz 124 | Token for listenbrainz.org: 125 | 126 | 127 | ##### Audioscrobbler compatible 128 | 129 | Libre.fm and Last.fm are using the same API for authentication and currently this is the mechanism: 130 | 131 | Use the signon binary in the following sequence: 132 | 133 | $ mpris-scrobbler-signon token 134 | 135 | $ mpris-scrobbler-signon session 136 | 137 | The valid service labels are: `librefm` and `lastfm`. 138 | 139 | The first step opens a browser window -- for this to work your system requires `xdg-open` binary -- which asks you to login to last.fm or libre.fm and then approve access for the `mpris-scrobbler` application. 140 | 141 | After granting permission to the application from the browser, you execute the second command to create a valid API session and complete the process. 142 | 143 | The daemon loads the new generated credentials automatically and you don't need to do it manually. 144 | 145 | The authentication for the libre.fm and listenbrainz.org services supports custom URLs that can be passed to the signon binary using the `--url` argument. 146 | 147 | $ mpris-scrobbler-signon --url http://127.0.0.1:8080 token [listenbrainz|librefm] 148 | 149 | For the moment we don't support multiple entries for the same API. Ex, have a local instance for the ListenBrainz API and use the official one at the same time. 150 | 151 | ## Troubleshooting 152 | 153 | If `mpris-scrobbler` does not seem to be working after following all usage instructions, confirm that `~/.local/share/mpris-scrobbler/credentials` contains: 154 | 155 | [yourservice] ;; where yourservice is lastfm, librefm or listenbrainz 156 | enabled = true ;; set via $ mpris-scrobbler-signon enable 157 | username = {USERNAME} ;; set via $ mpris-scrobbler-signon session - only available for lastfm/librefm 158 | token = {TOKEN} ;; set via $ mpris-scrobbler-signon token 159 | session = {SESSION} ;; set via $ mpris-scrobbler-signon session - only available for lastfm/librefm 160 | 161 | ### Enhanced output verbosity 162 | 163 | If the credentials are correct for the service you're having trouble with it could be helpfull to increase the verbosity of the logs. This can be achieved with the verbosity flag: 164 | 165 | $ mpris-scrobbler -v # enable INFO messages 166 | $ mpris-scrobbler -vv # enable DEBUG messages 167 | $ mpris-scrobbler -vvv # enable TRACING messages 168 | 169 | Further verbosity can be achieved by compiling the scrobbler using the `debug` build type, and then running it using the `-vvvv` maximum verbosity output flag, which makes the `TRACING` logs be even more verbose, including the actual requests sent to the scrobbling services and potentially sensitive information like credentials or authorization tokens. 170 | 171 | $ meson setup -Dbuildtype=debug build/ 172 | 173 | $ ninja -C build 174 | 175 | $ ./build/mpris-scrobbler -vvvv # enable TRACING2 messages 176 | 177 | For the exceptional cases when you might require verbosity from the libraries that the scrobbler links against, the following options can be additionally passed at build time: 178 | 179 | * `-Dlibcurldebug=true` to enable **libcurl** debug messages 180 | * `-Dlibeventdebug=true` to enable **libevent2** debug messages 181 | * `-Dlibdbusdebug=true` does not enable libdbus verbose logging, only a couple of extra logs related to loading the MPRIS metadata from the DBus messages. 182 | 183 | An example for compiling the scrobbler with maximum verbosity would look like this: 184 | 185 | $ meson setup --reconfigure -Dbuildtype=debug -Dlibcurldebug=true -Dlibeventdebug=true -Dlibdbusdebug=true ./build 186 | $ ninja -C ./build 187 | 188 | ## Packaging 189 | 190 | If you are a packager for mpris-scrobbler, please create separate credentials for the last.fm API at the [following URL](https://www.last.fm/api/account/create) instead of using the default ones packaged with the upstream source. 191 | 192 | To use them in your build they need to be passed to meson setup: 193 | 194 | $ meson setup --reconfigure -Dlastfm_api_key=2XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -Dlastfm_api_secret=YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY build/ 195 | 196 | ## Resources 197 | 198 | For discussions related to the project without requiring a Github account please see our mailing list: [https://lists.sr.ht/~mariusor/mpris-tools](https://lists.sr.ht/~mariusor/mpris-tools). 199 | 200 | The documentation in the README file will be soon moved to a dedicated [wiki](https://man.sr.ht/~mariusor/mpris-tools/mpris-scrobbler/) 201 | 202 | Check out the following articles and resources about mpris-scrobbler: 203 | 204 | * [2 new apps for music tweakers on Fedora Workstation - Fedora Magazine](https://fedoramagazine.org/2-new-apps-for-music-tweakers-on-fedora-workstation/ "2 new apps for music tweakers on Fedora Workstation") 205 | -------------------------------------------------------------------------------- /src/sevents.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Marius Orcsik 3 | */ 4 | #ifndef MPRIS_SCROBBLER_SEVENTS_H 5 | #define MPRIS_SCROBBLER_SEVENTS_H 6 | 7 | #include 8 | #include 9 | 10 | static void send_now_playing(evutil_socket_t, short, void *); 11 | void events_free(const struct events *ev) 12 | { 13 | if (NULL == ev) { return; } 14 | _trace2("mem::free::event(%p):SIGINT", ev->sigint); 15 | event_free(ev->sigint); 16 | _trace2("mem::free::event(%p):SIGTERM", ev->sigterm); 17 | event_free(ev->sigterm); 18 | _trace2("mem::free::event(%p):SIGHUP", ev->sighup); 19 | event_free(ev->sighup); 20 | _trace2("mem::free::event_base(%p)", ev->base); 21 | event_base_free(ev->base); 22 | } 23 | 24 | struct events *events_new(void) 25 | { 26 | struct events *result = calloc(1, sizeof(struct events)); 27 | return result; 28 | } 29 | 30 | static void log_event(const int severity, const char *msg) 31 | { 32 | enum log_levels level = log_tracing2; 33 | switch (severity) { 34 | case EVENT_LOG_DEBUG: 35 | level = log_tracing2; 36 | break; 37 | case EVENT_LOG_WARN: 38 | level = log_warning; 39 | break; 40 | case EVENT_LOG_ERR: 41 | level = log_error; 42 | break; 43 | case EVENT_LOG_MSG: 44 | default: 45 | level = log_tracing; 46 | } 47 | _log(level, "libevent: %s", msg); 48 | } 49 | 50 | void events_init(struct events *ev, struct state *s) 51 | { 52 | if (NULL == ev) { return; } 53 | 54 | #if defined(LIBEVENT_DEBUG) && LIBEVENT_DEBUG 55 | event_enable_debug_mode(); 56 | event_enable_debug_logging(EVENT_DBG_ALL); 57 | event_set_log_callback(log_event); 58 | #endif 59 | #if 1 60 | // as curl uses different threads, it's better to initialize support 61 | // for it in libevent2 62 | const int maybe_threads = evthread_use_pthreads(); 63 | if (maybe_threads < 0) { 64 | _error("events::unable_to_setup_multithreading"); 65 | } 66 | #endif 67 | 68 | 69 | ev->base = event_base_new(); 70 | if (NULL == ev->base) { 71 | _error("mem::init_libevent: failure"); 72 | return; 73 | } 74 | 75 | _trace2("mem::inited_libevent(%p)", ev->base); 76 | ev->sigint = evsignal_new(ev->base, SIGINT, sighandler, s); 77 | if (NULL == ev->sigint || event_add(ev->sigint, NULL) < 0) { 78 | _error("mem::add_event(SIGINT): failed"); 79 | return; 80 | } 81 | ev->sigterm = evsignal_new(ev->base, SIGTERM, sighandler, s); 82 | if (NULL == ev->sigterm || event_add(ev->sigterm, NULL) < 0) { 83 | _error("mem::add_event(SIGTERM): failed"); 84 | return; 85 | } 86 | ev->sighup = evsignal_new(ev->base, SIGHUP, sighandler, s); 87 | if (NULL == ev->sighup || event_add(ev->sighup, NULL) < 0) { 88 | _error("mem::add_event(SIGHUP): failed"); 89 | return; 90 | } 91 | } 92 | 93 | static void send_now_playing(evutil_socket_t fd, short event, void *data) 94 | { 95 | assert(data); 96 | struct event_payload *state = data; 97 | 98 | struct scrobble *track = &state->scrobble; 99 | assert(track); 100 | if (scrobble_is_empty(track)) { 101 | _debug("events::now_playing: invalid scrobble %p", track); 102 | return; 103 | } 104 | 105 | if (track->position > (double)track->length) { 106 | _trace2("events::now_playing: track position out of bounds %d > %ld", track->position, (double)track->length); 107 | event_del(&state->event); 108 | return; 109 | } 110 | 111 | struct mpris_player *player = state->parent; 112 | assert(player); 113 | if (!mpris_player_is_valid(player)) { 114 | _debug("events::now_playing: invalid player %s", player->mpris_name); 115 | event_del(&state->event); 116 | return; 117 | } 118 | 119 | struct scrobbler *scrobbler = player->scrobbler; 120 | assert(scrobbler); 121 | 122 | _trace("events::triggered(%p:%p):now_playing", state, track); 123 | print_scrobble(track, log_debug); 124 | 125 | const struct scrobble *tracks[1] = {track}; 126 | _info("scrobbler::now_playing[%s]: %s//%s//%s", player->name, track->title, track->artist[0], track->album); 127 | // TODO(marius): this requires the number of tracks to be passed down, to avoid dependency on arrlen 128 | api_request_do(scrobbler, tracks, 1, now_playing_is_valid, api_build_request_now_playing); 129 | 130 | if (track->position + NOW_PLAYING_DELAY < (double)track->length) { 131 | add_event_now_playing(player, track, NOW_PLAYING_DELAY); 132 | } 133 | } 134 | 135 | static bool add_event_now_playing(struct mpris_player *player, const struct scrobble *track, const time_t delay) 136 | { 137 | assert(NULL != player); 138 | assert(mpris_player_is_valid(player)); 139 | if (NULL == track || scrobble_is_empty(track)) { 140 | _trace2("events::add_event:now_playing: skipping, track is empty"); 141 | return false; 142 | } 143 | 144 | if (player->ignored) { 145 | _trace2("events::add_event:now_playing: skipping, player %s is ignored", player->name); 146 | return false; 147 | } 148 | 149 | const struct timeval now_playing_tv = { .tv_sec = delay }; 150 | 151 | struct event_payload *payload = &player->now_playing; 152 | scrobble_copy(&payload->scrobble, track); 153 | 154 | if (event_initialized(&payload->event)) { 155 | event_del(&payload->event); 156 | } 157 | event_assign(&payload->event, player->evbase, -1, EV_TIMEOUT, send_now_playing, payload); 158 | if (!event_initialized(&payload->event)) { 159 | _warn("events::add_event_failed(%p):now_playing", &payload->event); 160 | return false; 161 | } 162 | 163 | _debug("events::add_event:now_playing[%s] in %2.2lfs, elapsed %2.2lfs", player->name, timeval_to_seconds(now_playing_tv), (double)track->position); 164 | event_add(&payload->event, &now_playing_tv); 165 | payload->scrobble.position += (double)delay; 166 | payload->scrobble.play_time += (double)delay; 167 | 168 | return true; 169 | } 170 | 171 | static void queue(evutil_socket_t fd, short event, void *data) 172 | { 173 | assert (data); 174 | struct event_payload *state = data; 175 | 176 | struct mpris_player *player = state->parent; 177 | if (NULL == player) { 178 | _debug("events::queue: invalid player %p", player); 179 | return; 180 | } 181 | 182 | struct scrobbler *scrobbler = player->scrobbler; 183 | if (NULL == scrobbler) { 184 | _debug("events::queue: invalid scrobbler %p", scrobbler); 185 | return; 186 | } 187 | 188 | const struct scrobble *scrobble = &state->scrobble; 189 | assert(NULL != scrobble && !scrobble_is_empty(scrobble)); 190 | //print_scrobble(scrobble, log_tracing); 191 | 192 | _trace("events::triggered(%p:%p):queue", state, scrobbler->queue); 193 | scrobbles_append(scrobbler, scrobble); 194 | 195 | int queue_count = scrobbler->queue.length; 196 | if (queue_count > 0) { 197 | queue_count -= (int)scrobbler_consume_queue(scrobbler); 198 | _debug("events::new_queue_length: %zu", queue_count); 199 | } 200 | } 201 | 202 | static bool add_event_queue(struct mpris_player *player, const struct scrobble *track) 203 | { 204 | assert (NULL != player && mpris_player_is_valid(player)); 205 | assert (NULL != track && !scrobble_is_empty(track)); 206 | 207 | if (player->ignored) { 208 | _debug("events::add_event:queue: skipping, player %s is ignored", player->name); 209 | return false; 210 | } 211 | 212 | struct event_payload *payload = &player->queue; 213 | scrobble_copy(&payload->scrobble, track); 214 | 215 | assert(!scrobble_is_empty(track)); 216 | 217 | if (event_initialized(&payload->event)) { 218 | event_del(&payload->event); 219 | } 220 | event_assign(&payload->event, player->evbase, -1, EV_TIMEOUT, queue, payload); 221 | if (!event_initialized(&payload->event)) { 222 | _warn("events::add_event_failed(%p):queue", &payload->event); 223 | } 224 | 225 | // This is the event that adds a scrobble to the queue after the correct amount of time 226 | // round to the second 227 | const struct timeval timer = { 228 | .tv_sec = (time_t)(min_scrobble_delay_seconds(track)/1), 229 | }; 230 | 231 | _debug("events::add_event:queue[%s] in %2.2lfs", player->name, timeval_to_seconds(timer)); 232 | event_add(&payload->event, &timer); 233 | 234 | return true; 235 | } 236 | 237 | #if 0 238 | static bool add_event_scrobble(struct mpris_player *scrobbler) 239 | { 240 | if (NULL == scrobbler) { return false; } 241 | 242 | struct timeval scrobble_tv = {.tv_sec = 0 }; 243 | struct player_events *ev = &player->events; 244 | struct scrobble_payload *payload = scrobble_payload_new(player->scrobbler); 245 | 246 | // Initalize timed event for scrobbling 247 | // TODO(marius): Split scrobbling into two events: 248 | // 1. Actually add the current track to the top of the queue in length / 2 or 4 minutes, whichever comes first 249 | // 2. Process the queue and call APIs with the current queue 250 | 251 | if (event_assign(payload->event, ev->base, -1, EV_PERSIST, send_scrobble, payload) == 0) { 252 | // round to the second 253 | scrobble_tv.tv_sec = min_scrobble_delay_seconds(track); 254 | _debug("events::add_event(%p):scrobble in %2.2lfs", payload->event, timeval_to_seconds(scrobble_tv)); 255 | event_add(payload->event, &scrobble_tv); 256 | } else { 257 | _warn("events::add_event_failed(%p):scrobble", payload->event); 258 | } 259 | 260 | return true; 261 | } 262 | #endif 263 | 264 | static void mpris_event_clear(struct mpris_event *ev) 265 | { 266 | //memset(ev, 0, sizeof(*ev)); 267 | ev->player_state = killed; 268 | ev->loaded_state = 0; 269 | ev->playback_status_changed = false; 270 | ev->position_changed = false; 271 | ev->track_changed = false; 272 | ev->volume_changed = false; 273 | memset(ev->sender_bus_id, 0, sizeof(ev->sender_bus_id)); 274 | ev->timestamp = 0; 275 | _trace2("mem::zeroed::mpris_event"); 276 | } 277 | 278 | static bool state_dbus_is_valid(struct dbus *bus) 279 | { 280 | return (NULL != bus->conn); 281 | 282 | } 283 | 284 | static bool state_player_is_valid(struct mpris_player *player) 285 | { 286 | return strlen(player->mpris_name) > 0; 287 | } 288 | 289 | static bool state_is_valid(struct state *state) { 290 | return ( 291 | (NULL != state) && 292 | (state->player_count > 0)/* && state_player_is_valid(state->player)*/ && 293 | (NULL != state->dbus) && state_dbus_is_valid(state->dbus) 294 | ); 295 | } 296 | 297 | void resend_now_playing (struct state *state) 298 | { 299 | if (!state_is_valid(state)) { 300 | _error("events::invalid_state"); 301 | return; 302 | } 303 | // NOTE(marius): cancel any pending connections 304 | scrobbler_connections_clean(&state->scrobbler.connections, true); 305 | for (int i = 0; i < state->player_count; i++) { 306 | struct mpris_player *player = &state->players[i]; 307 | check_player(player); 308 | } 309 | } 310 | 311 | #endif // MPRIS_SCROBBLER_SEVENTS_H 312 | -------------------------------------------------------------------------------- /src/scrobbler.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Marius Orcsik 3 | */ 4 | #ifndef MPRIS_SCROBBLER_SCROBBLER_H 5 | #define MPRIS_SCROBBLER_SCROBBLER_H 6 | 7 | #include 8 | #include 9 | #include "curl.h" 10 | 11 | static bool connection_was_fulfilled(const struct scrobbler_connection *conn) 12 | { 13 | if (NULL == conn) { return false; } 14 | 15 | const time_t now = time(NULL); 16 | const double elapsed_seconds = difftime(now, conn->request.time); 17 | // NOTE(marius): either a CURL error has happened, or the response was returned, or the wait seconds have been exceeded. 18 | const bool fulfilled = (conn->response.code > 0 && conn->response.code < 600) || elapsed_seconds > MAX_WAIT_SECONDS || 19 | strlen(conn->error) > 0 || strlen(conn->response.body) > 0; 20 | return fulfilled; 21 | } 22 | 23 | static void scrobbler_connection_free (struct scrobbler_connection *conn, const bool force) 24 | { 25 | if (NULL == conn) { return; } 26 | if (!(force || connection_was_fulfilled(conn))) { return; } 27 | 28 | const char *api_label = get_api_type_label(conn->credentials.end_point); 29 | _trace("scrobbler::connection_free[%s]", api_label); 30 | 31 | if (NULL != conn->headers) { 32 | const size_t headers_count = arrlen(conn->headers); 33 | for (int i = (int)headers_count - 1; i >= 0; i--) { 34 | _trace2("scrobbler::connection_free::curl_headers(%zd::%zd:%p)", i, headers_count, conn->headers[i]); 35 | curl_slist_free_all(conn->headers[i]); 36 | arrdel(conn->headers, (size_t)i); 37 | conn->headers[i] = NULL; 38 | } 39 | assert(arrlen(conn->headers) == 0); 40 | arrfree(conn->headers); 41 | conn->headers = NULL; 42 | } 43 | 44 | if (event_initialized(&conn->ev)) { 45 | _trace2("scrobbler::connection_free::event[%p]", &conn->ev); 46 | event_del(&conn->ev); 47 | } 48 | #ifdef RETRY_ENABLED 49 | if (event_initialized(&conn->retry_event)) { 50 | _trace2("scrobbler::connection_free::retry_event[%p]", &conn->retry_event); 51 | event_del(&conn->retry_event); 52 | } 53 | #endif 54 | 55 | _trace2("scrobbler::connection_clean::request[%p]", conn->request); 56 | http_request_clean(&conn->request); 57 | _trace2("scrobbler::connection_clean:response[%p]", conn->response); 58 | http_response_clean(&conn->response); 59 | if (NULL != conn->handle) { 60 | _trace2("scrobbler::connection_free:curl_easy_handle[%p]", conn->handle); 61 | if (NULL != conn->parent && NULL != conn->parent->handle) { 62 | curl_multi_remove_handle(conn->parent->handle, conn->handle); 63 | } 64 | curl_easy_cleanup(conn->handle); 65 | conn->handle = NULL; 66 | } 67 | _trace2("scrobbler::connection_free:conn[%p]", conn); 68 | free(conn); 69 | conn = NULL; 70 | } 71 | 72 | static struct scrobbler_connection *scrobbler_connection_new(void) 73 | { 74 | struct scrobbler_connection *s = calloc(1, sizeof(struct scrobbler_connection)); 75 | s->idx = -1; 76 | return (s); 77 | } 78 | 79 | static void scrobbler_connection_init(struct scrobbler_connection *connection, struct scrobbler *s, const struct api_credentials credentials, const int idx) 80 | { 81 | connection->handle = curl_easy_init(); 82 | connection->idx = idx; 83 | connection->parent = s; 84 | 85 | memcpy(&connection->credentials, &credentials, sizeof(credentials)); 86 | memset(&connection->error, '\0', CURL_ERROR_SIZE); 87 | 88 | http_request_init(&connection->request); 89 | http_response_init(&connection->response); 90 | 91 | _trace("scrobbler::connection_init[%s][%p]:curl_easy_handle(%p)", get_api_type_label(credentials.end_point), connection, connection->handle); 92 | } 93 | 94 | static void scrobbler_connections_clean(struct scrobble_connections *connections, const bool force) 95 | { 96 | if (force) { 97 | _trace("scrobbler::connections_clean[%p]: %d", connections, connections->length); 98 | } 99 | size_t cleaned = 0; 100 | size_t skipped = 0; 101 | for (int i = MAX_QUEUE_LENGTH; i >= 0; i--) { 102 | struct scrobbler_connection *conn = connections->entries[i]; 103 | if (NULL == conn) { 104 | continue; 105 | } 106 | if (!(force || connection_was_fulfilled(conn))) { 107 | skipped++; 108 | continue; 109 | } 110 | scrobbler_connection_free(conn, force); 111 | connections->entries[i] = NULL; 112 | cleaned++; 113 | } 114 | if (cleaned > 0) { 115 | _trace("scrobbler::connections_freed: %zu, skipped %zu", cleaned, skipped); 116 | } 117 | 118 | int length = 0; 119 | for (int i = 0; i < MAX_QUEUE_LENGTH; i++) { 120 | if (NULL != connections->entries[i]) { length++; } 121 | } 122 | connections->length = length; 123 | } 124 | 125 | static bool scrobbler_queue_is_empty(const struct scrobble_queue *queue) 126 | { 127 | return (NULL == queue || queue->length == 0); 128 | } 129 | 130 | bool configuration_folder_create(const char *); 131 | bool configuration_folder_exists(const char *); 132 | static bool queue_persist_to_file(const struct scrobble_queue *to_persist, const char* path) 133 | { 134 | bool status = false; 135 | 136 | if (NULL == to_persist || NULL == path) { return status; } 137 | if (to_persist->length > 0) { return status; } 138 | 139 | char *file_path = (char *)path; 140 | char *folder_path = dirname(file_path); 141 | if (!configuration_folder_exists(folder_path) && !configuration_folder_create(folder_path)) { 142 | _error("main::cache: unable to create cache folder %s", folder_path); 143 | goto _exit; 144 | } 145 | 146 | _debug("saving::queue[%u]: %s", to_persist->length, path); 147 | FILE *file = fopen(path, "w+"); 148 | if (NULL == file) { 149 | _warn("saving::queue:failed: %s", path); 150 | goto _exit; 151 | } 152 | const size_t wrote = fwrite(to_persist, sizeof(to_persist), 1, file); 153 | status = wrote == sizeof(to_persist); 154 | if (!status) { 155 | _warn("saving::queue:unable to save full file %zu vs. %zu", wrote, sizeof(to_persist)); 156 | } 157 | 158 | fclose(file); 159 | 160 | _exit: 161 | return status; 162 | } 163 | 164 | static bool queue_append(struct scrobble_queue *, const struct scrobble *); 165 | static bool scrobbler_persist_queue(const struct scrobbler *scrobbler) 166 | { 167 | if (NULL == scrobbler || scrobbler_queue_is_empty(&scrobbler->queue)) { 168 | return false; 169 | } 170 | 171 | return queue_persist_to_file(&scrobbler->queue, scrobbler->conf->cache_path); 172 | } 173 | 174 | static void scrobbler_clean(struct scrobbler *s) 175 | { 176 | if (NULL == s) { return; } 177 | 178 | scrobbler_persist_queue(s); 179 | 180 | _trace("scrobbler::clean[%p]", s); 181 | 182 | scrobbler_connections_clean(&s->connections, true); 183 | 184 | if(evtimer_initialized(&s->timer_event) && evtimer_pending(&s->timer_event, NULL)) { 185 | _trace2("curl::multi_timer_remove(%p)", &s->timer_event); 186 | evtimer_del(&s->timer_event); 187 | } 188 | 189 | curl_multi_cleanup(s->handle); 190 | curl_global_cleanup(); 191 | } 192 | 193 | static struct scrobbler_connection *scrobbler_connection_get(const struct scrobble_connections *connections, const CURL *e) 194 | { 195 | struct scrobbler_connection *conn = NULL; 196 | for (int i = 0; i < MAX_QUEUE_LENGTH; i++) { 197 | conn = connections->entries[i]; 198 | if (NULL == conn) { 199 | continue; 200 | } 201 | if (conn->handle == e) { 202 | _trace2("curl::found_easy_handle:idx[%d]: %p e[%p]", i, conn, conn->handle); 203 | break; 204 | } 205 | } 206 | return conn; 207 | } 208 | 209 | static void scrobbler_init(struct scrobbler *s, struct configuration *config, struct event_base *evbase) 210 | { 211 | curl_global_init(CURL_GLOBAL_DEFAULT); 212 | s->conf = config; 213 | s->handle = curl_multi_init(); 214 | 215 | s->evbase = evbase; 216 | 217 | evtimer_assign(&s->timer_event, s->evbase, timer_cb, s); 218 | _trace2("curl::multi_timer_add(%p:%p)", s->handle, &s->timer_event); 219 | 220 | curl_multi_setopt(s->handle, CURLMOPT_SOCKETFUNCTION, curl_request_has_data); 221 | curl_multi_setopt(s->handle, CURLMOPT_SOCKETDATA, s); 222 | curl_multi_setopt(s->handle, CURLMOPT_TIMERFUNCTION, curl_request_wait_timeout); 223 | curl_multi_setopt(s->handle, CURLMOPT_TIMERDATA, s); 224 | curl_multi_setopt(s->handle, CURLMOPT_MAX_TOTAL_CONNECTIONS, MAX_CREDENTIALS*2L); 225 | curl_multi_setopt(s->handle, CURLMOPT_MAX_HOST_CONNECTIONS, 2L); 226 | 227 | s->connections.length = 0; 228 | } 229 | 230 | typedef void(*request_builder_t)(struct http_request*, const struct scrobble*[MAX_QUEUE_LENGTH], const unsigned, const struct api_credentials*, CURL*); 231 | typedef bool(*request_validation_t)(const struct scrobble*, const struct api_credentials*); 232 | 233 | static bool scrobble_is_valid(const struct scrobble *m, const struct api_credentials *cur) 234 | { 235 | if (NULL == m) { 236 | return false; 237 | } 238 | 239 | switch (cur->end_point) { 240 | case api_listenbrainz: 241 | return listenbrainz_scrobble_is_valid(m); 242 | case api_librefm: 243 | case api_lastfm: 244 | case api_unknown: 245 | default: 246 | return audioscrobbler_scrobble_is_valid(m); 247 | } 248 | } 249 | 250 | static void api_request_do(struct scrobbler *s, const struct scrobble *tracks[], const unsigned track_count, const request_validation_t validate_request, const request_builder_t build_request) 251 | { 252 | if (NULL == s) { return; } 253 | if (NULL == s->conf || 0 == s->conf->credentials_count) { return; } 254 | 255 | const size_t credentials_count = s->conf->credentials_count; 256 | 257 | for (size_t i = 0; i < credentials_count; i++) { 258 | const struct api_credentials *cur = &s->conf->credentials[i]; 259 | if (!credentials_valid(cur)) { 260 | if (cur->enabled) { 261 | _warn("scrobbler::invalid_service[%s]", get_api_type_label(cur->end_point)); 262 | } 263 | continue; 264 | } 265 | const struct scrobble *current_api_tracks[MAX_QUEUE_LENGTH] = {0}; 266 | unsigned current_api_track_count = 0; 267 | for (size_t ti = 0; ti < track_count; ti++) { 268 | const struct scrobble *track = tracks[ti]; 269 | if (validate_request(track, cur)) { 270 | current_api_tracks[current_api_track_count] = track; 271 | current_api_track_count++; 272 | } 273 | } 274 | if (current_api_track_count == 0) { 275 | _warn("scrobbler::invalid_now_playing[%s]: no valid tracks", get_api_type_label(cur->end_point)); 276 | continue; 277 | } 278 | assert(s->connections.length < MAX_QUEUE_LENGTH); 279 | if (s->connections.length == MAX_QUEUE_LENGTH) { 280 | s->connections.length = 0; 281 | } 282 | 283 | struct scrobbler_connection *conn = scrobbler_connection_new(); 284 | scrobbler_connection_init(conn, s, *cur, s->connections.length); 285 | build_request(&conn->request, current_api_tracks, current_api_track_count, cur, conn->handle); 286 | s->connections.entries[conn->idx] = conn; 287 | s->connections.length++; 288 | _trace("scrobbler::new_connection[%s]: connections: %zu ", get_api_type_label(cur->end_point), s->connections.length); 289 | 290 | build_curl_request(conn); 291 | 292 | curl_multi_add_handle(s->handle, conn->handle); 293 | } 294 | } 295 | 296 | #endif // MPRIS_SCROBBLER_SCROBBLER_H 297 | -------------------------------------------------------------------------------- /src/utils.h: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @author Marius Orcsik 4 | */ 5 | #ifndef MPRIS_SCROBBLER_UTILS_H 6 | #define MPRIS_SCROBBLER_UTILS_H 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | enum log_levels _log_level; 19 | 20 | #define _neg(a, b) (a) &= ~(b); 21 | 22 | #define _eq(a, b) (memcmp((void*)&(a), (void*)&(b), sizeof(a)) == 0) 23 | #define _cpy(a, b) memcpy(&(a), &(b), sizeof(a)) 24 | #define _is_zero(a) _eq(a, (char[sizeof(a)]){0}) 25 | 26 | #define _to_bool(a) (a ? "yes" : "no") 27 | 28 | #define array_count(a) (sizeof(a)/sizeof 0[a]) 29 | #define max(a, b) (((a) >= (b)) ? a : b) 30 | #define min(a, b) (((a) <= (b)) ? a : b) 31 | #define zero_string(incoming, length) memset(&incoming, 0, (length + 1) * sizeof(char)) 32 | #define get_zero_string(len) grrrs_new(len + 1) 33 | #define string_free(s) grrrs_free(s) 34 | 35 | #define timeval_to_seconds(T) (double)((T).tv_sec) + (double)((T).tv_usec)/(double)1000000.0f 36 | 37 | #define LOG_ERROR_LABEL "ERROR" 38 | #define LOG_WARNING_LABEL "WARNING" 39 | #define LOG_DEBUG_LABEL "DEBUG" 40 | #define LOG_INFO_LABEL "INFO" 41 | #define LOG_TRACING_LABEL "TRACING" 42 | #define LOG_TRACING2_LABEL "TRACING" 43 | 44 | static bool level_is(const unsigned incoming, enum log_levels level) 45 | { 46 | return ((incoming & level) == level); 47 | } 48 | 49 | static const char *get_log_level (enum log_levels l) 50 | { 51 | if (level_is(l, log_tracing2)) { return LOG_TRACING_LABEL; } 52 | if (level_is(l, log_tracing)) { return LOG_TRACING_LABEL; } 53 | if (level_is(l, log_debug)) { return LOG_DEBUG_LABEL; } 54 | if (level_is(l, log_info)) { return LOG_INFO_LABEL; } 55 | if (level_is(l, log_warning)) { return LOG_WARNING_LABEL; } 56 | if (level_is(l, log_error)) { return LOG_ERROR_LABEL; } 57 | return LOG_TRACING_LABEL; 58 | } 59 | 60 | #define _log(level, format, ...) _logd(level, "", "", 0, format, __VA_ARGS__) 61 | #define _error(...) _logd(log_error, __FILE__, __func__, __LINE__, __VA_ARGS__) 62 | #define _warn(...) _logd(log_warning, __FILE__, __func__, __LINE__, __VA_ARGS__) 63 | #define _info(...) _logd(log_info, __FILE__, __func__, __LINE__, __VA_ARGS__) 64 | #define _debug(...) _logd(log_debug, __FILE__, __func__, __LINE__, __VA_ARGS__) 65 | #define _trace(...) _logd(log_tracing, __FILE__, __func__, __LINE__, __VA_ARGS__) 66 | #define _trace2(...) _logd(log_tracing2, __FILE__, __func__, __LINE__, __VA_ARGS__) 67 | 68 | static void trim_path(char *destination, const char *path, const int length) 69 | { 70 | char dirpath[FILE_PATH_MAX] = {0}; 71 | const size_t path_len = strlen(path); 72 | 73 | assert(path_len < FILE_PATH_MAX-1); 74 | 75 | memcpy(dirpath, path, path_len); 76 | char *dir = dirname(dirpath); 77 | char *basedir = basename(dir); 78 | 79 | char basepath[FILE_PATH_MAX] = {0}; 80 | memcpy(basepath, path, path_len); 81 | char *base = basename(basepath); 82 | 83 | snprintf(destination, (size_t)length, "%s/%s", basedir, base); 84 | } 85 | 86 | #define GRAY_COLOUR "\033[38;5;240m" 87 | #define RESET_COLOUR "\033[0m" 88 | 89 | static int _logd(enum log_levels level, const char *file, const char *function, const int line, const char *format, ...) 90 | { 91 | #ifndef DEBUG 92 | if (level >= log_tracing) { return 0; } 93 | (void)file; 94 | (void)function; 95 | (void)line; 96 | #endif 97 | if (!level_is(_log_level, level)) { return 0; } 98 | 99 | FILE *out = stdout; 100 | #ifndef DEBUG 101 | if (level < log_warning) { 102 | out = stderr; 103 | } 104 | #else 105 | bool output_is_tty = isatty(STDOUT_FILENO); 106 | if (level < log_warning) { 107 | out = stderr; 108 | output_is_tty = isatty(STDERR_FILENO); 109 | } 110 | #endif 111 | 112 | va_list args; 113 | va_start(args, format); 114 | 115 | const char *label = get_log_level(level); 116 | 117 | const size_t f_len = strlen(format); 118 | char log_format[10240] = {0}; 119 | snprintf(log_format, 10240-1, "%-7s ", label); 120 | 121 | strncat(log_format, format, f_len + 1); 122 | char suffix[1024] = {"\n"}; 123 | #ifdef DEBUG 124 | if (level > log_debug && strlen(function) > 0 && strlen(file) > 0 && line > 0) { 125 | char path[256] = {0}; 126 | trim_path(path, file, 255); 127 | if (!output_is_tty) { 128 | snprintf(suffix, 1024, " in %s() %s:%d\n", function, path, line); 129 | } else { 130 | snprintf(suffix, 1024, GRAY_COLOUR " in %s() %s:%d" RESET_COLOUR "\n", function, path, line); 131 | } 132 | } 133 | #endif 134 | strncat(log_format, suffix, 1024); 135 | 136 | const int result = vfprintf(out, log_format, args); 137 | va_end(args); 138 | fflush(out); 139 | 140 | return result; 141 | } 142 | 143 | static void array_log_with_label(char *output, char arr[MAX_PROPERTY_COUNT][MAX_PROPERTY_LENGTH+1], const int count) 144 | { 145 | if (count <= 0) { return; } 146 | 147 | memset(output, 0, MAX_PROPERTY_LENGTH*MAX_PROPERTY_COUNT+9); 148 | 149 | char temp[MAX_PROPERTY_COUNT*MAX_PROPERTY_LENGTH+1] = {0}; 150 | unsigned short cnt = 0; 151 | for (int i = 0; i < count; i++) { 152 | if (strlen(arr[i]) == 0) { 153 | break; 154 | } 155 | if (i > 0) { 156 | memcpy(temp + strlen(temp), ", ", 2); 157 | } 158 | memcpy(temp + strlen(temp), arr[i], strlen(arr[i])); 159 | cnt++; 160 | } 161 | if (cnt > 1) { 162 | snprintf(output, MAX_PROPERTY_LENGTH*MAX_PROPERTY_COUNT+10, "[%u]: %s", cnt, temp); 163 | } else { 164 | snprintf(output, MAX_PROPERTY_LENGTH*MAX_PROPERTY_COUNT+1, "%s", temp); 165 | } 166 | } 167 | 168 | static const char *get_api_type_label(const enum api_type end_point) 169 | { 170 | switch (end_point) { 171 | case(api_lastfm): 172 | return "last.fm"; 173 | case(api_librefm): 174 | return "libre.fm"; 175 | case(api_listenbrainz): 176 | return "listenbrainz.org"; 177 | case(api_unknown): 178 | default: 179 | return "unknown"; 180 | } 181 | } 182 | 183 | void resend_now_playing (struct state *); 184 | bool load_configuration(struct configuration*, const char*); 185 | static void sighandler(const evutil_socket_t signum, short events, void *user_data) 186 | { 187 | if (events) { events = 0; } 188 | struct state *s = user_data; 189 | struct event_base *eb = s->events.base; 190 | 191 | const char *signal_name = "UNKNOWN"; 192 | switch (signum) { 193 | case SIGHUP: 194 | signal_name = "SIGHUP"; 195 | break; 196 | case SIGINT: 197 | signal_name = "SIGINT"; 198 | break; 199 | case SIGTERM: 200 | signal_name = "SIGTERM"; 201 | break; 202 | default: 203 | return; 204 | } 205 | _info("main::signal_received: %s", signal_name); 206 | 207 | if (signum == SIGHUP) { 208 | load_configuration(s->config, APPLICATION_NAME); 209 | resend_now_playing(s); 210 | } 211 | if (signum == SIGINT || signum == SIGTERM) { 212 | event_base_loopexit(eb, NULL); 213 | } 214 | } 215 | 216 | static void free_arguments(struct parsed_arguments *args) 217 | { 218 | free(args); 219 | } 220 | 221 | #define VERBOSE_TRACE2 "vvv" 222 | #define VERBOSE_TRACE "vv" 223 | #define VERBOSE_DEBUG "v" 224 | 225 | static void parse_command_line(struct parsed_arguments *args, enum binary_type which_bin, int argc, char *argv[]) 226 | { 227 | args->get_token = false; 228 | args->get_session = false; 229 | args->has_help = false; 230 | args->has_url = false; 231 | args->disable = false; 232 | args->enable = false; 233 | args->reload = false; 234 | args->service = api_unknown; 235 | args->log_level = log_warning | log_error; 236 | 237 | const char *name = basename(argv[0]); 238 | memcpy(args->name, name, min(NAME_ARG_MAX, strlen(name))); 239 | 240 | int option_index = 0; 241 | 242 | static struct option long_options[] = { 243 | {"help", no_argument, NULL, 'h'}, 244 | {"quiet", no_argument, NULL, 'q'}, 245 | {"verbose", optional_argument, NULL, 'v'}, 246 | {"url", required_argument, NULL, 'u'}, 247 | }; 248 | opterr = 0; 249 | while (true) { 250 | const int char_arg = getopt_long(argc, argv, "-rhqu:v::", long_options, &option_index); 251 | if (char_arg == -1) { break; } 252 | switch (char_arg) { 253 | case 1: 254 | if (which_bin == daemon_bin) { break; } 255 | if (strncmp(optarg, ARG_COMMAND_RELOAD, strlen(ARG_COMMAND_RELOAD)) == 0) { 256 | args->reload = true; 257 | } 258 | if (strncmp(optarg, ARG_LASTFM, strlen(ARG_LASTFM)) == 0) { 259 | args->service = api_lastfm; 260 | } 261 | if (strncmp(optarg, ARG_LIBREFM, strlen(ARG_LIBREFM)) == 0) { 262 | args->service = api_librefm; 263 | } 264 | if (strncmp(optarg, ARG_LISTENBRAINZ, strlen(ARG_LISTENBRAINZ)) == 0) { 265 | args->service = api_listenbrainz; 266 | } 267 | if (strncmp(optarg, ARG_COMMAND_TOKEN, strlen(ARG_COMMAND_TOKEN)) == 0) { 268 | args->get_token = true; 269 | } 270 | if (strncmp(optarg, ARG_COMMAND_SESSION, strlen(ARG_COMMAND_SESSION)) == 0) { 271 | args->get_session = true; 272 | } 273 | if (strncmp(optarg, ARG_COMMAND_DISABLE, strlen(ARG_COMMAND_DISABLE)) == 0) { 274 | args->disable = true; 275 | } 276 | if (strncmp(optarg, ARG_COMMAND_ENABLE, strlen(ARG_COMMAND_ENABLE)) == 0) { 277 | args->enable = true; 278 | } 279 | break; 280 | case 'q': 281 | args->log_level = log_error; 282 | break; 283 | case 'v': 284 | if (args->log_level == log_error) { break; } 285 | if (NULL == optarg) { 286 | args->log_level = log_info | log_warning | log_error; 287 | break; 288 | } 289 | if (strncmp(optarg, VERBOSE_TRACE, strlen(VERBOSE_TRACE)) == 0 || strtol(optarg, NULL, 10) >= 3) { 290 | args->log_level = log_debug | log_info | log_warning | log_error; 291 | #ifdef DEBUG 292 | args->log_level |= log_tracing; 293 | if (strncmp(optarg, VERBOSE_TRACE2, strlen(VERBOSE_TRACE2)) == 0 || strtol(optarg, NULL, 10) >= 4) { 294 | args->log_level |= log_tracing2; 295 | } 296 | #else 297 | _warn("main::debug: extra verbose output is disabled"); 298 | #endif 299 | break; 300 | } 301 | if (strncmp(optarg, VERBOSE_DEBUG, strlen(VERBOSE_DEBUG)) == 0 || strtol(optarg, NULL, 10) == 2) { 302 | args->log_level = log_debug | log_info | log_warning | log_error; 303 | break; 304 | } 305 | break; 306 | case 'u': 307 | args->has_url = true; 308 | memcpy(args->url, optarg, min(sizeof(args->url)-1, strlen(optarg))); 309 | break; 310 | case 'h': 311 | args->has_help = true; 312 | case '?': 313 | default: 314 | break; 315 | } 316 | } 317 | _log_level = args->log_level; 318 | } 319 | 320 | static const char *get_version(void) 321 | { 322 | #ifndef VERSION_HASH 323 | #include "version.h" 324 | #endif 325 | return VERSION_HASH; 326 | } 327 | 328 | static const char *get_application_name(void) 329 | { 330 | return APPLICATION_NAME; 331 | } 332 | 333 | #endif // MPRIS_SCROBBLER_UTILS_H 334 | -------------------------------------------------------------------------------- /src/signon.c: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Marius Orcsik 3 | */ 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #define STB_DS_IMPLEMENTATION 15 | #include "stb_ds.h" 16 | #include "structs.h" 17 | #include "sstrings.h" 18 | #include "utils.h" 19 | #include "api.h" 20 | #include "smpris.h" 21 | #include "scrobbler.h" 22 | #include "scrobble.h" 23 | #include "sdbus.h" 24 | #include "sevents.h" 25 | #include "configuration.h" 26 | 27 | #define HELP_MESSAGE "MPRIS scrobbler user signon, version %s\n\n" \ 28 | "Usage:\n %s COMMAND SERVICE - Execute COMMAND for SERVICE\n\n" \ 29 | "Commands:\n" \ 30 | "\t" ARG_COMMAND_RELOAD "\t\tReload daemon service if found running.\n" \ 31 | "\t" ARG_COMMAND_TOKEN "\t\tGet the authentication token for SERVICE.\n" \ 32 | "\t" ARG_COMMAND_SESSION "\t\tActivate a new session for SERVICE. SERVICE must have a valid token.\n" \ 33 | "\t" ARG_COMMAND_ENABLE "\t\tActivate SERVICE for submitting tracks.\n" \ 34 | "\t" ARG_COMMAND_DISABLE "\t\tDeactivate submitting tracks to SERVICE.\n\n" \ 35 | "Services:\n" \ 36 | "\t" ARG_LASTFM "\t\tlast.fm\n" \ 37 | "\t" ARG_LIBREFM "\t\tlibre.fm\n" \ 38 | "\t" ARG_LISTENBRAINZ "\tlistenbrainz.org\n\n" \ 39 | HELP_OPTIONS \ 40 | "\t" ARG_URL_LONG "\tConnect to a custom URL.\n\t\t\t\tValid only for libre.fm and listenbrainz.org \n" \ 41 | "\t" ARG_URL "\n" \ 42 | "" 43 | 44 | #define XDG_OPEN "/usr/bin/xdg-open \"%s\"" 45 | #define MAX_OPEN_CMD_LENGTH MAX_URL_LENGTH + 22 // The max URL length and the XDG_OPEN command length 46 | 47 | static void print_help(const char *name) 48 | { 49 | fprintf(stdout, HELP_MESSAGE, get_version(), name); 50 | } 51 | 52 | char *get_pid_file(struct configuration *); 53 | static void reload_daemon(struct configuration *config) 54 | { 55 | load_pid_path(config); 56 | FILE *pid_file = fopen(config->pid_path, "r"); 57 | if (NULL == pid_file) { 58 | _debug("signon::daemon_reload: unable to find PID file"); 59 | return; 60 | } 61 | 62 | pid_t pid = 0; 63 | if (fscanf(pid_file, "%du", &pid) == 1) { 64 | if (kill(pid, SIGHUP) == 0) { 65 | _info("signon::daemon_reload[%zu]: ok", pid); 66 | } else { 67 | _warn("signon::daemon_reload[%zu]: failed", pid); 68 | } 69 | } 70 | fclose(pid_file); 71 | } 72 | 73 | static enum api_return_status request_call(struct scrobbler_connection *conn) 74 | { 75 | enum api_return_status ok = status_failed; 76 | CURLcode cres = curl_easy_perform(conn->handle); 77 | /* Check for errors */ 78 | if(cres != CURLE_OK) { 79 | _error("curl::error: %s", curl_easy_strerror(cres)); 80 | } 81 | curl_easy_getinfo(conn->handle, CURLINFO_RESPONSE_CODE, &conn->response.code); 82 | _trace("curl::response(%lu): %s", conn->response.body_length, &conn->response.body); 83 | 84 | if (conn->response.code == 200) { ok = status_ok; } 85 | return ok; 86 | } 87 | 88 | static void get_session(struct api_credentials *creds) 89 | { 90 | if (NULL == creds) { return; } 91 | if (strlen(creds->api_key) == 0) { 92 | _error("invalid_api_key %s %s %s: %s", LASTFM_API_KEY, LIBREFM_API_KEY, LISTENBRAINZ_API_KEY, creds->api_key); 93 | } 94 | if (strlen(creds->secret) == 0) { 95 | _error("invalid_api_secret %s %s %s: %s", LASTFM_API_SECRET, LIBREFM_API_SECRET, LISTENBRAINZ_API_SECRET, creds->secret); 96 | return; 97 | } 98 | if (strlen(creds->token) == 0) { return; } 99 | 100 | struct scrobbler_connection *conn = scrobbler_connection_new(); 101 | scrobbler_connection_init(conn, NULL, *creds, 0); 102 | api_build_request_get_session(&conn->request, creds, conn->handle); 103 | 104 | build_curl_request(conn); 105 | 106 | enum api_return_status ok = request_call(conn); 107 | if (ok == status_ok && !json_document_is_error(conn->response.body, conn->response.body_length, creds->end_point)) { 108 | api_response_get_session_key_json(conn->response.body, conn->response.body_length, creds); 109 | if (strlen(creds->session_key) > 0) { 110 | _info("api::get_session[%s] %s", get_api_type_label(creds->end_point), "ok"); 111 | creds->enabled = true; 112 | } else { 113 | _error("api::get_session[%s] %s - disabling", get_api_type_label(creds->end_point), "nok"); 114 | api_credentials_disable(creds); 115 | } 116 | } else { 117 | api_credentials_disable(creds); 118 | } 119 | scrobbler_connection_free(conn, true); 120 | } 121 | 122 | static bool get_token(struct api_credentials *creds) 123 | { 124 | if (NULL == creds) { return false; } 125 | if (strlen(creds->api_key) == 0) { 126 | _error("api::invalid_key"); 127 | return false; 128 | } 129 | if (strlen(creds->secret) == 0) { 130 | _error("api::invalid_secret"); 131 | return false; 132 | } 133 | 134 | struct scrobbler_connection *conn = scrobbler_connection_new(); 135 | scrobbler_connection_init(conn, NULL, *creds, 0); 136 | api_build_request_get_token(&conn->request, creds, conn->handle); 137 | if (NULL == conn->request.url) { 138 | _error("api::invalid_get_token_request"); 139 | } 140 | 141 | build_curl_request(conn); 142 | 143 | const enum api_return_status ok = request_call(conn); 144 | 145 | if (ok == status_ok && !json_document_is_error(conn->response.body, conn->response.body_length, creds->end_point)) { 146 | api_credentials_disable(creds); 147 | api_response_get_token_json(conn->response.body, conn->response.body_length, creds); 148 | } 149 | 150 | CURLU *auth_url = curl_url(); 151 | if (strlen(creds->token) > 0) { 152 | _info("api::get_token[%s] %s", get_api_type_label(creds->end_point), "ok"); 153 | creds->enabled = true; 154 | api_get_auth_url(auth_url, creds); 155 | } else { 156 | _error("api::get_token[%s] %s - disabling", get_api_type_label(creds->end_point), "nok"); 157 | api_credentials_disable(creds); 158 | } 159 | scrobbler_connection_free(conn, true); 160 | 161 | char *url; 162 | curl_url_get(auth_url, CURLUPART_URL, &url, MPRIS_CURLU_FLAGS); 163 | if (strlen(url) == 0) { 164 | _error("signon::get_token_error: unable to open authentication url"); 165 | return false; 166 | } 167 | 168 | char open_cmd[MAX_OPEN_CMD_LENGTH] = {0}; 169 | snprintf(open_cmd, MAX_OPEN_CMD_LENGTH, XDG_OPEN, url); 170 | const int status = system(open_cmd); 171 | 172 | if (status == EXIT_SUCCESS) { 173 | _debug("xdg::opened[ok]: %s", url); 174 | } else { 175 | _debug("xdg::opened[nok]: %s", url); 176 | } 177 | curl_url_cleanup(auth_url); 178 | curl_free(url); 179 | 180 | return status == EXIT_SUCCESS; 181 | } 182 | 183 | static int getch(void) { 184 | struct termios oldt, newt; 185 | tcgetattr(STDIN_FILENO, &oldt); 186 | newt = oldt; 187 | newt.c_lflag &= ~((tcflag_t)ICANON | (tcflag_t)ECHO); 188 | tcsetattr(STDIN_FILENO, TCSANOW, &newt); 189 | 190 | const int ch = getchar(); 191 | tcsetattr(STDIN_FILENO, TCSANOW, &oldt); 192 | return ch; 193 | } 194 | 195 | static bool set_token(struct api_credentials *creds) 196 | { 197 | if (NULL == creds) { return false; } 198 | 199 | int chr = 0; 200 | size_t pos = 0; 201 | 202 | fprintf(stdout, "Token for %s: ", get_api_type_label(creds->end_point)); 203 | while (chr != '\n') { 204 | chr = getch(); 205 | ((char*)creds->token)[pos] = (char)chr; 206 | pos++; 207 | } 208 | ((char*)creds->token)[pos-1] = 0x0; 209 | fprintf(stdout, "\n"); 210 | 211 | if (strlen(creds->token) > 0) { 212 | _info("api::get_token[%s] %s", get_api_type_label(creds->end_point), "ok"); 213 | creds->enabled = true; 214 | } else { 215 | _error("api::get_token[%s] %s - disabling", get_api_type_label(creds->end_point), "nok"); 216 | api_credentials_disable(creds); 217 | } 218 | return true; 219 | } 220 | 221 | int main (const int argc, char *argv[]) 222 | { 223 | int status = EXIT_FAILURE; 224 | struct parsed_arguments arguments = {0}; 225 | parse_command_line(&arguments, signon_bin, argc, argv); 226 | 227 | if (arguments.has_help) { 228 | print_help(arguments.name); 229 | status = EXIT_SUCCESS; 230 | goto _exit; 231 | } 232 | if(arguments.service == api_unknown && !arguments.reload) { 233 | _error("signon::debug: no service selected"); 234 | status = EXIT_FAILURE; 235 | goto _exit; 236 | } 237 | 238 | bool success = true; 239 | bool found = false; 240 | 241 | struct configuration config = {0}; 242 | load_configuration(&config, APPLICATION_NAME); 243 | 244 | if (arguments.reload) { 245 | reload_daemon(&config); 246 | goto _exit; 247 | } 248 | 249 | const size_t count = config.credentials_count; 250 | if (count == 0) { 251 | _warn("main::load_credentials: no credentials were loaded"); 252 | } 253 | 254 | struct api_credentials *creds = NULL; 255 | for (size_t i = 0; i < count; i++) { 256 | if (config.credentials[i].end_point != arguments.service) { continue; } 257 | 258 | creds = &config.credentials[i]; 259 | found = true; 260 | break; 261 | } 262 | if (NULL == creds) { 263 | creds = api_credentials_new(); 264 | creds->end_point = arguments.service; 265 | const char *key = api_get_application_key(creds->end_point); 266 | memcpy((char*)creds->api_key, key, min(MAX_SECRET_LENGTH, strlen(key))); 267 | const char *secret = api_get_application_secret(creds->end_point); 268 | memcpy((char*)creds->secret, secret, min(MAX_SECRET_LENGTH, strlen(secret))); 269 | } 270 | if (arguments.has_url) { 271 | if (strlen(arguments.url) > 0) { 272 | strncpy((char*)creds->url, arguments.url, MAX_URL_LENGTH); 273 | } else { 274 | _warn("signon::argument_error: missing --url argument"); 275 | } 276 | } 277 | if (arguments.disable) { 278 | _info("signon::disabling: %s", get_api_type_label(arguments.service)); 279 | creds->enabled = false; 280 | } 281 | if (arguments.enable) { 282 | _info("signon::enable: %s", get_api_type_label(arguments.service)); 283 | creds->enabled = true; 284 | } 285 | if (arguments.get_token) { 286 | _info("signon::getting_token: %s", get_api_type_label(arguments.service)); 287 | 288 | if (creds->end_point == api_listenbrainz) { 289 | success = set_token(creds); 290 | } else { 291 | success = get_token(creds); 292 | } 293 | } 294 | if (arguments.get_session) { 295 | if (creds->end_point == api_listenbrainz) { 296 | _warn("signon::getting_session_key: skipping for %s", get_api_type_label(arguments.service)); 297 | } else { 298 | _info("signon::getting_session_key: %s", get_api_type_label(arguments.service)); 299 | get_session(creds); 300 | } 301 | } 302 | if (!found) { 303 | memcpy(&config.credentials[config.credentials_count], creds, sizeof(struct api_credentials)); 304 | config.credentials_count++; 305 | } 306 | #if 0 307 | print_application_config(config); 308 | #endif 309 | 310 | if (success) { 311 | if (write_credentials_file(&config) == 0) { 312 | if ( 313 | (arguments.get_token && arguments.service == api_listenbrainz) || 314 | arguments.get_session || 315 | arguments.disable || arguments.enable 316 | ) { 317 | reload_daemon(&config); 318 | } 319 | status = EXIT_SUCCESS; 320 | } else { 321 | _warn("signon::config_error: unable to write to configuration file"); 322 | status = EXIT_FAILURE; 323 | } 324 | } else { 325 | status = EXIT_FAILURE; 326 | } 327 | 328 | configuration_clean(&config); 329 | 330 | _exit: 331 | return status; 332 | } 333 | -------------------------------------------------------------------------------- /src/structs.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Marius Orcsik 3 | */ 4 | #ifndef MPRIS_SCROBBLER_STRUCTS_H 5 | #define MPRIS_SCROBBLER_STRUCTS_H 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #define ARG_HELP "-h" 13 | #define ARG_HELP_LONG "--help" 14 | #define ARG_QUIET "-q" 15 | #define ARG_QUIET_LONG "--quiet" 16 | #define ARG_VERBOSE1 "-v" 17 | #define ARG_VERBOSE2 "-vv" 18 | #define ARG_VERBOSE3 "-vvv" 19 | #define ARG_VERBOSE_LONG "--verbose[=1-3]" 20 | #define ARG_URL "-u " 21 | #define ARG_URL_LONG "--url=" 22 | 23 | #define ARG_LASTFM "lastfm" 24 | #define ARG_LIBREFM "librefm" 25 | #define ARG_LISTENBRAINZ "listenbrainz" 26 | 27 | #define ARG_COMMAND_RELOAD "reload" 28 | #define ARG_COMMAND_TOKEN "token" 29 | #define ARG_COMMAND_ENABLE "enable" 30 | #define ARG_COMMAND_DISABLE "disable" 31 | #define ARG_COMMAND_SESSION "session" 32 | 33 | #define HELP_OPTIONS "Options:\n"\ 34 | "\t" ARG_HELP_LONG "\t\t\tDisplay this help.\n" \ 35 | "\t" ARG_HELP "\n" \ 36 | "\t" ARG_QUIET_LONG "\t\t\tDo not output debugging messages.\n" \ 37 | "\t" ARG_QUIET "\n" \ 38 | "\t" ARG_VERBOSE_LONG "\t\tIncrease output verbosity to level:\n" \ 39 | "\t" ARG_VERBOSE1 "\t\t\t1 - Info messages.\n" \ 40 | "\t" ARG_VERBOSE2 "\t\t\t2 - Debug messages.\n" \ 41 | "\t" ARG_VERBOSE3 "\t\t\t3 - Tracing messages.\n" \ 42 | "" 43 | 44 | #define MAX_URL_LENGTH 2048 45 | 46 | #define MAX_PROPERTY_LENGTH 384 // bytes 47 | 48 | #define MAX_API_COUNT 3 49 | 50 | #define MAX_NOW_PLAYING_EVENTS 20 51 | 52 | enum end_point_type { 53 | unknown_endpoint = 0, 54 | authorization_endpoint, 55 | scrobble_endpoint, 56 | }; 57 | 58 | enum api_type { 59 | api_unknown = 0, 60 | api_lastfm, 61 | api_librefm, 62 | api_listenbrainz, 63 | }; 64 | 65 | #define MAX_SECRET_LENGTH 128 66 | #define USER_NAME_MAX 32 67 | 68 | struct api_credentials { 69 | char api_key[MAX_SECRET_LENGTH + 1]; 70 | char secret[MAX_SECRET_LENGTH + 1]; 71 | char user_name[USER_NAME_MAX + 1]; 72 | char password[MAX_SECRET_LENGTH + 1]; 73 | char token[MAX_SECRET_LENGTH + 1]; 74 | char session_key[MAX_SECRET_LENGTH + 1]; 75 | char url[MAX_URL_LENGTH + 1]; 76 | enum api_type end_point; 77 | bool enabled; 78 | bool authenticated; 79 | }; 80 | 81 | #define FILE_PATH_MAX 4095 82 | #define HOME_PATH_MAX 512 83 | 84 | #define XDG_PATH_ELEM_MAX FILE_PATH_MAX / 3 85 | 86 | struct env_variables { 87 | const char xdg_config_home[XDG_PATH_ELEM_MAX + 1]; 88 | const char xdg_data_home[XDG_PATH_ELEM_MAX + 1]; 89 | const char xdg_cache_home[XDG_PATH_ELEM_MAX + 1]; 90 | const char xdg_runtime_dir[XDG_PATH_ELEM_MAX + 1]; 91 | const char home[HOME_PATH_MAX + 1]; 92 | const char user_name[USER_NAME_MAX + 1]; 93 | }; 94 | 95 | #define MAX_PLAYERS 10 96 | #define MAX_CREDENTIALS 10 97 | 98 | struct configuration { 99 | const char name[USER_NAME_MAX+1]; 100 | const char pid_path[FILE_PATH_MAX+1]; 101 | const char config_path[FILE_PATH_MAX+1]; 102 | const char credentials_path[FILE_PATH_MAX+1]; 103 | const char cache_path[FILE_PATH_MAX+1]; 104 | const char ignore_players[MAX_PLAYERS][MAX_PROPERTY_LENGTH+1]; 105 | struct api_credentials credentials[MAX_CREDENTIALS]; 106 | struct env_variables env; 107 | size_t credentials_count; 108 | bool wrote_pid; 109 | bool env_loaded; 110 | short ignore_players_count; 111 | }; 112 | 113 | #define MAX_PROPERTY_COUNT 8 114 | struct mpris_metadata { 115 | uint64_t length; // mpris specific 116 | unsigned track_number; 117 | unsigned bitrate; 118 | unsigned disc_number; 119 | char track_id[MAX_PROPERTY_LENGTH+1]; 120 | char album[MAX_PROPERTY_LENGTH+1]; 121 | char content_created[MAX_PROPERTY_LENGTH+1]; 122 | char title[MAX_PROPERTY_LENGTH+1]; 123 | char url[MAX_PROPERTY_LENGTH+1]; 124 | char art_url[MAX_PROPERTY_LENGTH+1]; //mpris specific 125 | char composer[MAX_PROPERTY_LENGTH+1]; 126 | char genre[MAX_PROPERTY_COUNT][MAX_PROPERTY_LENGTH+1]; 127 | char comment[MAX_PROPERTY_COUNT][MAX_PROPERTY_LENGTH+1]; 128 | char artist[MAX_PROPERTY_COUNT][MAX_PROPERTY_LENGTH+1]; 129 | char album_artist[MAX_PROPERTY_COUNT][MAX_PROPERTY_LENGTH+1]; 130 | char mb_track_id[MAX_PROPERTY_COUNT][MAX_PROPERTY_LENGTH+1]; //music brainz specific 131 | char mb_album_id[MAX_PROPERTY_COUNT][MAX_PROPERTY_LENGTH+1]; 132 | char mb_artist_id[MAX_PROPERTY_COUNT][MAX_PROPERTY_LENGTH+1]; 133 | char mb_album_artist_id[MAX_PROPERTY_COUNT][MAX_PROPERTY_LENGTH+1]; 134 | }; 135 | 136 | struct mpris_properties { 137 | struct mpris_metadata metadata; 138 | double volume; 139 | int64_t position; 140 | bool can_control; 141 | bool can_go_next; 142 | bool can_go_previous; 143 | bool can_play; 144 | bool can_pause; 145 | bool can_seek; 146 | bool shuffle; 147 | char player_name[MAX_PROPERTY_LENGTH+1]; 148 | char loop_status[MAX_PROPERTY_LENGTH+1]; 149 | char playback_status[MAX_PROPERTY_LENGTH+1]; 150 | }; 151 | 152 | struct events { 153 | struct event_base *base; 154 | struct event *sigint; 155 | struct event *sigterm; 156 | struct event *sighup; 157 | struct event dispatch; 158 | }; 159 | 160 | struct scrobble { 161 | double play_time; 162 | double position; 163 | double length; 164 | time_t start_time; 165 | 166 | bool scrobbled; 167 | unsigned short track_number; 168 | 169 | char url[MAX_PROPERTY_LENGTH+1]; 170 | char title[MAX_PROPERTY_LENGTH+1]; 171 | char album[MAX_PROPERTY_LENGTH+1]; 172 | char artist[MAX_PROPERTY_COUNT][MAX_PROPERTY_LENGTH+1]; 173 | 174 | char mb_track_id[MAX_PROPERTY_COUNT][MAX_PROPERTY_LENGTH+1]; //music brainz specific 175 | char mb_album_id[MAX_PROPERTY_COUNT][MAX_PROPERTY_LENGTH+1]; 176 | char mb_artist_id[MAX_PROPERTY_COUNT][MAX_PROPERTY_LENGTH+1]; 177 | char mb_album_artist_id[MAX_PROPERTY_COUNT][MAX_PROPERTY_LENGTH+1]; 178 | char player_name[MAX_PROPERTY_LENGTH+1]; 179 | char mb_spotify_id[MAX_PROPERTY_LENGTH+1]; // spotify id for listenbrainz 180 | }; 181 | 182 | enum playback_state { 183 | killed = 0U, 184 | stopped = 1U << 0U, 185 | paused = 1U << 1U, 186 | playing = 1U << 2U 187 | }; 188 | 189 | enum mpris_load_types { 190 | mpris_load_nothing = 0U, 191 | mpris_load_property_can_control = 1U << 0U, 192 | mpris_load_property_can_go_next = 1U << 1U, 193 | mpris_load_property_can_go_previous = 1U << 2U, 194 | mpris_load_property_can_pause = 1U << 3U, 195 | mpris_load_property_can_play = 1U << 4U, 196 | mpris_load_property_can_seek = 1U << 5U, 197 | mpris_load_property_loop_status = 1U << 6U, 198 | mpris_load_property_volume = 1U << 7U, 199 | mpris_load_property_shuffle = 1U << 8U, 200 | // from here the loaded information is relevant for a scrobble change 201 | mpris_load_property_position = 1U << 9U, 202 | mpris_load_property_playback_status = 1U << 10U, 203 | mpris_load_metadata_bitrate = 1U << 11U, 204 | mpris_load_metadata_art_url = 1U << 12U, 205 | mpris_load_metadata_length = 1U << 13U, 206 | mpris_load_metadata_track_id = 1U << 14U, 207 | mpris_load_metadata_album = 1U << 15U, 208 | mpris_load_metadata_album_artist = 1U << 16U, 209 | mpris_load_metadata_artist = 1U << 17U, 210 | mpris_load_metadata_comment = 1U << 18U, 211 | mpris_load_metadata_title = 1U << 19U, 212 | mpris_load_metadata_track_number = 1U << 20U, 213 | mpris_load_metadata_url = 1U << 21U, 214 | mpris_load_metadata_genre = 1U << 22U, 215 | mpris_load_metadata_mb_track_id = 1U << 23U, 216 | mpris_load_metadata_mb_album_id = 1U << 24U, 217 | mpris_load_metadata_mb_artist_id = 1U << 25U, 218 | mpris_load_metadata_mb_album_artist_id = 1U << 26U, 219 | mpris_load_all = (1U << 31U) - 1, // all bits are set for our max enum val 220 | }; 221 | 222 | struct mpris_event { 223 | time_t timestamp; 224 | enum playback_state player_state; 225 | bool playback_status_changed; 226 | bool track_changed; 227 | bool volume_changed; 228 | bool position_changed; 229 | long loaded_state; 230 | char sender_bus_id[MAX_PROPERTY_LENGTH+1]; 231 | }; 232 | 233 | struct dbus { 234 | DBusConnection *conn; 235 | DBusWatch *watch; 236 | DBusTimeout *timeout; 237 | }; 238 | 239 | struct event_payload { 240 | struct mpris_player *parent; 241 | struct scrobble scrobble; 242 | struct event event; 243 | }; 244 | 245 | #define MAX_HEADER_LENGTH 256 246 | #define MAX_HEADER_NAME_LENGTH 128 247 | #define MAX_HEADER_VALUE_LENGTH 512 248 | #define MAX_BODY_SIZE 16384 249 | 250 | struct http_header { 251 | char name[MAX_HEADER_NAME_LENGTH]; 252 | char value[MAX_HEADER_VALUE_LENGTH]; 253 | }; 254 | 255 | struct http_response { 256 | char body[MAX_BODY_SIZE+1]; 257 | struct http_header **headers; 258 | size_t body_length; 259 | long code; 260 | }; 261 | 262 | typedef enum http_request_types { 263 | http_get, 264 | http_post, 265 | http_put, 266 | http_head, 267 | http_patch, 268 | } http_request_type; 269 | 270 | struct http_request { 271 | char body[MAX_BODY_SIZE+1]; 272 | struct http_header **headers; 273 | size_t body_length; 274 | time_t time; 275 | struct api_endpoint *end_point; 276 | CURLU *url; 277 | http_request_type request_type; 278 | }; 279 | 280 | struct scrobbler_connection { 281 | char error[CURL_ERROR_SIZE+1]; 282 | struct event ev; 283 | struct api_credentials credentials; 284 | #ifdef RETRY_ENABLED 285 | struct event retry_event; 286 | #endif 287 | struct scrobbler *parent; 288 | struct curl_slist **headers; 289 | struct http_request request; 290 | struct http_response response; 291 | CURL *handle; 292 | curl_socket_t sockfd; 293 | bool should_free; 294 | int action; 295 | int idx; 296 | #ifdef RETRY_ENABLED 297 | int retries; 298 | #endif 299 | }; 300 | 301 | #define MAX_QUEUE_LENGTH 32 302 | #define MAX_WAIT_SECONDS 10 303 | 304 | struct scrobble_connections { 305 | int length; 306 | struct scrobbler_connection *entries[MAX_QUEUE_LENGTH]; 307 | }; 308 | 309 | struct scrobble_queue { 310 | int length; 311 | struct scrobble entries[MAX_QUEUE_LENGTH]; 312 | }; 313 | 314 | struct scrobbler { 315 | int still_running; 316 | CURLM *handle; 317 | struct event_base *evbase; 318 | struct configuration *conf; 319 | struct event timer_event; 320 | struct scrobble_connections connections; 321 | struct scrobble_queue queue; 322 | }; 323 | 324 | struct mpris_player { 325 | bool ignored; 326 | bool deleted; 327 | char mpris_name[MAX_PROPERTY_LENGTH + 1]; 328 | char bus_id[MAX_PROPERTY_LENGTH + 1]; 329 | char name[MAX_PROPERTY_LENGTH + 1]; 330 | struct mpris_event changed; 331 | struct mpris_properties properties; 332 | struct event_payload now_playing; 333 | struct event_payload queue; 334 | struct scrobbler *scrobbler; 335 | struct event_base *evbase; 336 | struct mpris_properties **history; 337 | }; 338 | 339 | struct state { 340 | struct dbus *dbus; 341 | struct configuration *config; 342 | struct events events; 343 | struct scrobbler scrobbler; 344 | short player_count; 345 | struct mpris_player players[MAX_PLAYERS]; 346 | }; 347 | 348 | enum log_levels 349 | { 350 | log_none = 0U, 351 | log_error = (1U << 0U), 352 | log_warning = (1U << 1U), 353 | log_info = (1U << 2U), 354 | log_debug = (1U << 3U), 355 | log_tracing = (1U << 4U), 356 | log_tracing2 = (1U << 5U), 357 | }; 358 | 359 | enum binary_type { 360 | daemon_bin, 361 | signon_bin, 362 | }; 363 | 364 | #define URL_ARG_MAX 2048 365 | #define NAME_ARG_MAX 128 366 | 367 | struct parsed_arguments { 368 | char name[NAME_ARG_MAX + 1]; 369 | char url[URL_ARG_MAX + 1]; 370 | bool has_url; 371 | bool has_help; 372 | bool get_token; 373 | bool get_session; 374 | bool disable; 375 | bool enable; 376 | bool reload; 377 | enum binary_type binary; 378 | enum log_levels log_level; 379 | enum api_type service; 380 | char pid_path[MAX_PROPERTY_LENGTH + 1]; 381 | }; 382 | 383 | #endif // MPRIS_SCROBBLER_STRUCTS_H 384 | -------------------------------------------------------------------------------- /tests/strings_basic.c: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Marius Orcsik 3 | */ 4 | 5 | #include 6 | 7 | #ifndef assert 8 | #include 9 | #endif 10 | #define GRRRS_OOM assert(!"Failed to allocate memory") 11 | 12 | #include "sstrings.h" 13 | 14 | #include 15 | 16 | void assert_grrrs(char *t) { 17 | struct grrr_string *gs = _grrrs_ptr(t); 18 | asserteq_ptr((void*)&gs->data, (void*)t); 19 | asserteq_ptr((void*)&gs->cap, (void*)(t - sizeof(gs->cap))); 20 | asserteq_ptr((void*)&gs->len, (void*)(t - sizeof(gs->cap) - sizeof(gs->len))); 21 | asserteq_ptr((void*)gs, (void*)(t - sizeof(gs->cap) - sizeof(gs->len))); 22 | asserteq_int(gs->len, strlen(t)); 23 | asserteq_int(gs->cap, strlen(t)); 24 | } 25 | 26 | describe(basic) { 27 | subdesc(plumbing) { 28 | it ("Check __strlen") { 29 | const char *t = "ana are mere"; 30 | 31 | asserteq_int(__strlen(t), strlen(t)); 32 | } 33 | it ("Check __strlen for zero length") { 34 | const char *t = ""; 35 | 36 | asserteq_int(__strlen(t), strlen(t)); 37 | } 38 | it ("Check __strlen for NULL string") { 39 | const char *t = NULL; 40 | 41 | asserteq_int(__strlen(t), 0); 42 | } 43 | } 44 | subdesc(pointer plumbing) { 45 | it("Check pointer addresses") { 46 | char *t = grrrs_from_string(0); 47 | 48 | defer(_grrrs_free(t)); 49 | 50 | asserteq_int(grrrs_len(t), 0); 51 | asserteq_int(grrrs_cap(t), 0); 52 | asserteq_buf(t, "", 1); 53 | 54 | assert_grrrs(t); 55 | } 56 | } 57 | 58 | subdesc(initialization) { 59 | it("Zero string") { 60 | char *t = grrrs_from_string(0); 61 | 62 | defer(_grrrs_free(t)); 63 | 64 | asserteq_int(grrrs_len(t), 0); 65 | asserteq_int(grrrs_cap(t), 0); 66 | asserteq_buf(t, "", 1); 67 | } 68 | 69 | it("From static string") { 70 | char *t = grrrs_from_string("ana are mere\n"); 71 | 72 | defer(_grrrs_free(t)); 73 | 74 | asserteq_int(grrrs_len(t), 13); 75 | asserteq_int(grrrs_cap(t), 13); 76 | asserteq_buf(t, "ana are mere\n", 14); 77 | 78 | assert_grrrs(t); 79 | } 80 | 81 | it("From heap string") { 82 | char *h = calloc(1, sizeof(char)*10 + 1); 83 | for (unsigned i = 0; i < 10; i++) { 84 | h[i] = 't'; 85 | } 86 | char *t = grrrs_from_string(h); 87 | 88 | asserteq_int(grrrs_len(t), 10); 89 | asserteq_int(grrrs_cap(t), 10); 90 | asserteq_buf(t, "tttttttttt", 10); 91 | 92 | assert_grrrs(t); 93 | 94 | _grrrs_free(t); 95 | free(h); 96 | } 97 | } 98 | 99 | subdesc(operations) { 100 | it("Grow string") { 101 | char *t = grrrs_from_string(0); 102 | asserteq_int(grrrs_len(t), 0); 103 | asserteq_int(grrrs_cap(t), 0); 104 | 105 | t = _grrrs_resize(t, 128); 106 | asserteq_int(grrrs_cap(t), 128); 107 | asserteq_int(grrrs_len(t), 0); 108 | 109 | char null_block[128] = {0}; 110 | 111 | asserteq_int(memcmp(t, &null_block, 128), 0); 112 | _grrrs_free(t); 113 | } 114 | 115 | it("Shrink string") { 116 | char *t = grrrs_from_string(0); 117 | 118 | asserteq_int(grrrs_len(t), 0); 119 | asserteq_int(grrrs_cap(t), 0); 120 | 121 | t = _grrrs_resize(t, 128); 122 | asserteq_int(grrrs_cap(t), 128); 123 | asserteq_int(grrrs_len(t), 0); 124 | 125 | t = _grrrs_resize(t, 1); 126 | asserteq_int(grrrs_cap(t), 1); 127 | asserteq_int(grrrs_len(t), 0); 128 | 129 | char null_block[1] = {0}; 130 | asserteq_int(memcmp(t, &null_block, 1), 0); 131 | 132 | _grrrs_free(t); 133 | } 134 | } 135 | 136 | subdesc(trim_left) { 137 | it("no matches to trim") { 138 | char *t = grrrs_from_string("ana"); 139 | defer(_grrrs_free(t)); 140 | 141 | assert_grrrs(t); 142 | asserteq_int(grrrs_len(t), 3); 143 | asserteq_int(grrrs_cap(t), 3); 144 | 145 | _grrrs_trim_left(t, 0); 146 | asserteq_buf(t, "ana", 3); 147 | asserteq_int(grrrs_len(t), 3); 148 | asserteq_int(grrrs_cap(t), 3); 149 | } 150 | it("custom character 'a'") { 151 | char *t = grrrs_from_string("aana"); 152 | defer(_grrrs_free(t)); 153 | 154 | assert_grrrs(t); 155 | asserteq_int(grrrs_len(t), 4); 156 | asserteq_int(grrrs_cap(t), 4); 157 | 158 | _grrrs_trim_left(t, "a"); 159 | asserteq_buf(t, "na", 2); 160 | asserteq_int(grrrs_len(t), 2); 161 | asserteq_int(grrrs_cap(t), 4); 162 | } 163 | it("trim to empty 'a'") { 164 | char *t = grrrs_from_string("aaaaaaaa"); 165 | defer(_grrrs_free(t)); 166 | 167 | assert_grrrs(t); 168 | asserteq_int(grrrs_len(t), 8); 169 | asserteq_int(grrrs_cap(t), 8); 170 | 171 | _grrrs_trim_left(t, "a"); 172 | asserteq_buf(t, "", 1); 173 | asserteq_int(grrrs_len(t), 0); 174 | asserteq_int(grrrs_cap(t), 8); 175 | } 176 | it("trim to empty 'ab'") { 177 | char *t = grrrs_from_string("abaabbba"); 178 | defer(_grrrs_free(t)); 179 | 180 | assert_grrrs(t); 181 | asserteq_int(grrrs_len(t), 8); 182 | asserteq_int(grrrs_cap(t), 8); 183 | 184 | _grrrs_trim_left(t, "ab"); 185 | asserteq_buf(t, "", 1); 186 | asserteq_int(grrrs_len(t), 0); 187 | asserteq_int(grrrs_cap(t), 8); 188 | } 189 | it("trim to empty default whitespace characters") { 190 | char *t = grrrs_from_string(" \t \r \n"); 191 | defer(_grrrs_free(t)); 192 | 193 | assert_grrrs(t); 194 | asserteq_int(grrrs_len(t), 8); 195 | asserteq_int(grrrs_cap(t), 8); 196 | 197 | _grrrs_trim_left(t, 0); 198 | asserteq_buf(t, "", 1); 199 | asserteq_int(grrrs_len(t), 0); 200 | asserteq_int(grrrs_cap(t), 8); 201 | } 202 | it("trim default whitespace characters") { 203 | char *t = grrrs_from_string(" ana are mere\n"); 204 | defer(_grrrs_free(t)); 205 | 206 | assert_grrrs(t); 207 | asserteq_int(grrrs_len(t), 20); 208 | asserteq_int(grrrs_cap(t), 20); 209 | 210 | _grrrs_trim_left(t, 0); 211 | asserteq_buf(t, "ana are mere\n", 13); 212 | asserteq_int(grrrs_len(t), 13); 213 | asserteq_int(grrrs_cap(t), 20); 214 | } 215 | } 216 | subdesc(trim_right) { 217 | it("no matches to trim") { 218 | char *t = grrrs_from_string("ana"); 219 | defer(_grrrs_free(t)); 220 | 221 | assert_grrrs(t); 222 | asserteq_int(grrrs_len(t), 3); 223 | asserteq_int(grrrs_cap(t), 3); 224 | 225 | _grrrs_trim_right(t, 0); 226 | asserteq_buf(t, "ana", 3); 227 | asserteq_int(grrrs_len(t), 3); 228 | asserteq_int(grrrs_cap(t), 3); 229 | } 230 | it("custom character 'a'") { 231 | char *t = grrrs_from_string("aana"); 232 | defer(_grrrs_free(t)); 233 | 234 | assert_grrrs(t); 235 | asserteq_int(grrrs_len(t), 4); 236 | asserteq_int(grrrs_cap(t), 4); 237 | 238 | _grrrs_trim_right(t, "a"); 239 | asserteq_buf(t, "aan", 3); 240 | asserteq_int(grrrs_len(t), 3); 241 | asserteq_int(grrrs_cap(t), 4); 242 | } 243 | it("trim to empty 'a'") { 244 | char *t = grrrs_from_string("aaaaaaaa"); 245 | defer(_grrrs_free(t)); 246 | 247 | assert_grrrs(t); 248 | asserteq_int(grrrs_len(t), 8); 249 | asserteq_int(grrrs_cap(t), 8); 250 | 251 | _grrrs_trim_right(t, "a"); 252 | asserteq_buf(t, "", 1); 253 | asserteq_int(grrrs_len(t), 0); 254 | asserteq_int(grrrs_cap(t), 8); 255 | } 256 | it("trim to empty 'ab'") { 257 | char *t = grrrs_from_string("abaabbba"); 258 | defer(_grrrs_free(t)); 259 | 260 | assert_grrrs(t); 261 | asserteq_int(grrrs_len(t), 8); 262 | asserteq_int(grrrs_cap(t), 8); 263 | 264 | _grrrs_trim_right(t, "ab"); 265 | asserteq_buf(t, "", 1); 266 | asserteq_int(grrrs_len(t), 0); 267 | asserteq_int(grrrs_cap(t), 8); 268 | } 269 | it("trim to empty default whitespace characters") { 270 | char *t = grrrs_from_string(" \t \r \n"); 271 | defer(_grrrs_free(t)); 272 | 273 | assert_grrrs(t); 274 | asserteq_int(grrrs_len(t), 8); 275 | asserteq_int(grrrs_cap(t), 8); 276 | 277 | _grrrs_trim_right(t, 0); 278 | asserteq_buf(t, "", 1); 279 | asserteq_int(grrrs_len(t), 0); 280 | asserteq_int(grrrs_cap(t), 8); 281 | } 282 | it("trim default whitespace characters") { 283 | char *t = grrrs_from_string(" ana are mere\n"); 284 | defer(_grrrs_free(t)); 285 | 286 | assert_grrrs(t); 287 | asserteq_int(grrrs_len(t), 20); 288 | asserteq_int(grrrs_cap(t), 20); 289 | 290 | _grrrs_trim_right(t, 0); 291 | asserteq_buf(t, " ana are mere", 19); 292 | asserteq_int(grrrs_len(t), 19); 293 | asserteq_int(grrrs_cap(t), 20); 294 | } 295 | } 296 | subdesc(trim_both) { 297 | it("no matches to trim") { 298 | char *t = grrrs_from_string("ana"); 299 | defer(_grrrs_free(t)); 300 | 301 | assert_grrrs(t); 302 | asserteq_int(grrrs_len(t), 3); 303 | asserteq_int(grrrs_cap(t), 3); 304 | 305 | grrrs_trim(t, 0); 306 | asserteq_buf(t, "ana", 3); 307 | asserteq_int(grrrs_len(t), 3); 308 | asserteq_int(grrrs_cap(t), 3); 309 | } 310 | it("custom character 'a'") { 311 | char *t = grrrs_from_string("aana"); 312 | defer(_grrrs_free(t)); 313 | 314 | assert_grrrs(t); 315 | asserteq_int(grrrs_len(t), 4); 316 | asserteq_int(grrrs_cap(t), 4); 317 | 318 | grrrs_trim(t, "a"); 319 | asserteq_buf(t, "n", 1); 320 | asserteq_int(grrrs_len(t), 1); 321 | asserteq_int(grrrs_cap(t), 4); 322 | } 323 | it("trim to empty 'a'") { 324 | char *t = grrrs_from_string("aaaaaaaa"); 325 | defer(_grrrs_free(t)); 326 | 327 | assert_grrrs(t); 328 | asserteq_int(grrrs_len(t), 8); 329 | asserteq_int(grrrs_cap(t), 8); 330 | 331 | grrrs_trim(t, "a"); 332 | asserteq_buf(t, "", 1); 333 | asserteq_int(grrrs_len(t), 0); 334 | asserteq_int(grrrs_cap(t), 8); 335 | } 336 | it("trim to empty 'ab'") { 337 | char *t = grrrs_from_string("abaabbba"); 338 | defer(_grrrs_free(t)); 339 | 340 | assert_grrrs(t); 341 | asserteq_int(grrrs_len(t), 8); 342 | asserteq_int(grrrs_cap(t), 8); 343 | 344 | grrrs_trim(t, "ab"); 345 | asserteq_buf(t, "", 1); 346 | asserteq_int(grrrs_len(t), 0); 347 | asserteq_int(grrrs_cap(t), 8); 348 | } 349 | it("trim to empty default whitespace characters") { 350 | char *t = grrrs_from_string(" \t \r \n"); 351 | defer(_grrrs_free(t)); 352 | 353 | assert_grrrs(t); 354 | asserteq_int(grrrs_len(t), 8); 355 | asserteq_int(grrrs_cap(t), 8); 356 | 357 | grrrs_trim(t, 0); 358 | asserteq_buf(t, "", 1); 359 | asserteq_int(grrrs_len(t), 0); 360 | asserteq_int(grrrs_cap(t), 8); 361 | } 362 | it("trim default whitespace characters") { 363 | char *t = grrrs_from_string(" ana are mere\n"); 364 | defer(_grrrs_free(t)); 365 | 366 | assert_grrrs(t); 367 | asserteq_int(grrrs_len(t), 20); 368 | asserteq_int(grrrs_cap(t), 20); 369 | 370 | grrrs_trim(t, 0); 371 | asserteq_buf("ana are mere", t, 12); 372 | asserteq_int(grrrs_len(t), 12); 373 | asserteq_int(grrrs_cap(t), 20); 374 | } 375 | it("trim sequence that segfaults: \\020") { 376 | char *t = grrrs_from_string("\0 "); 377 | defer(_grrrs_free(t)); 378 | 379 | assert_grrrs(t); 380 | 381 | grrrs_trim(t, NULL); 382 | asserteq_buf("", t, 1); 383 | } 384 | } 385 | } 386 | 387 | snow_main(); 388 | -------------------------------------------------------------------------------- /src/listenbrainz_api.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Marius Orcsik 3 | */ 4 | #ifndef MPRIS_SCROBBLER_LISTENBRAINZ_API_H 5 | #define MPRIS_SCROBBLER_LISTENBRAINZ_API_H 6 | 7 | #ifndef LISTENBRAINZ_API_KEY 8 | #include "credentials_listenbrainz.h" 9 | #endif 10 | 11 | #define API_LISTEN_TYPE_NOW_PLAYING "playing_now" 12 | #define API_LISTEN_TYPE_SINGLE "single" 13 | #define API_LISTEN_TYPE_IMPORT "import" 14 | 15 | #define API_LISTEN_TYPE_NODE_NAME "listen_type" 16 | #define API_PAYLOAD_NODE_NAME "payload" 17 | #define API_LISTENED_AT_NODE_NAME "listened_at" 18 | #define API_METADATA_NODE_NAME "track_metadata" 19 | #define API_ARTIST_NAME_NODE_NAME "artist_name" 20 | #define API_TRACK_NAME_NODE_NAME "track_name" 21 | #define API_ALBUM_NAME_NODE_NAME "release_name" 22 | 23 | #define API_ADDITIONAL_INFO_NODE_NAME "additional_info" 24 | #define API_MUSICBRAINZ_TRACK_ID_NODE_NAME "track_mbid" 25 | #define API_MUSICBRAINZ_RECORDING_ID_NODE_NAME "recording_mbid" 26 | #define API_MUSICBRAINZ_ARTISTS_ID_NODE_NAME "artist_mbids" 27 | #define API_MUSICBRAINZ_ALBUM_ID_NODE_NAME "release_mbid" 28 | #define API_MUSICBRAINZ_SPOTIFY_ID_NODE_NAME "spotify_id" 29 | #define API_DURATION_NODE_NAME "duration" 30 | #define API_SUBMITTER_NODE_NAME "submission_client" 31 | #define API_SUBMITTER_VERSION_NODE_NAME "submission_client_version" 32 | #define API_URI_NODE_NAME "origin_url" 33 | #define API_PLAYER_NODE_NAME "media_player" 34 | 35 | // we e.g. don't want to submit file:// URIs to LB 36 | #define API_PROTO_WHITELIST_HTTP "http://" 37 | #define API_PROTO_WHITELIST_HTTPS "https://" 38 | 39 | #define API_CODE_NODE_NAME "code" 40 | #define API_ERROR_NODE_NAME "error" 41 | 42 | #define LISTENBRAINZ_AUTH_URL "https://listenbrainz.org/api/auth/" 43 | #define LISTENBRAINZ_API_BASE_URL "api.listenbrainz.org" 44 | #define LISTENBRAINZ_API_VERSION "1" 45 | 46 | #define API_ENDPOINT_SUBMIT_LISTEN "submit-listens" 47 | 48 | static bool listenbrainz_valid_credentials(const struct api_credentials *auth) 49 | { 50 | if (NULL == auth) { return false; } 51 | if (auth->end_point != api_listenbrainz) { return false; } 52 | 53 | return true; 54 | } 55 | 56 | static bool listenbrainz_scrobble_is_valid(const struct scrobble *s) 57 | { 58 | if (NULL == s) { return false; } 59 | if (_is_zero(s->artist)) { return false; } 60 | 61 | const double scrobble_interval = min_scrobble_delay_seconds(s); 62 | double d; 63 | if (s->play_time > 0) { 64 | d = s->play_time +1lu; 65 | } else { 66 | const time_t now = time(0); 67 | d = difftime(now, s->start_time) + 1lu; 68 | } 69 | 70 | const bool result = ( 71 | s->length >= (double)MIN_TRACK_LENGTH && 72 | d >= scrobble_interval && 73 | strlen(s->title) > 0 && 74 | strlen(s->artist[0]) > 0 75 | ); 76 | return result; 77 | } 78 | 79 | static bool listenbrainz_now_playing_is_valid(const struct scrobble *m/*, const time_t current_time, const time_t last_playing_time*/) { 80 | if (NULL == m) { 81 | return false; 82 | } 83 | 84 | const bool result = ( 85 | strlen(m->title) > 0LU && 86 | strlen(m->artist[0]) > 0LU && 87 | m->length > 0.0L && 88 | m->position <= (double)m->length 89 | ); 90 | 91 | return result; 92 | } 93 | 94 | #if 0 95 | static http_request *build_generic_request() 96 | { 97 | } 98 | #endif 99 | 100 | static void listenbrainz_api_append_additional_info(json_object* root,const struct scrobble *track){ 101 | const char *mb_track_id = (char*)track->mb_track_id[0]; 102 | const char *mb_artist_id = (char*)track->mb_artist_id[0]; 103 | const char *mb_album_id = (char*)track->mb_album_id[0]; 104 | 105 | json_object *additional_info = json_object_new_object(); 106 | json_object_object_add(additional_info, API_DURATION_NODE_NAME, json_object_new_int((int)track->length)); 107 | json_object_object_add(additional_info, API_SUBMITTER_NODE_NAME, json_object_new_string(get_application_name())); 108 | json_object_object_add(additional_info, API_SUBMITTER_VERSION_NODE_NAME, json_object_new_string(get_version())); 109 | json_object_object_add(additional_info, API_PLAYER_NODE_NAME, json_object_new_string(track->player_name)); 110 | if (strlen(mb_track_id) > 0) { 111 | json_object_object_add(additional_info, API_MUSICBRAINZ_RECORDING_ID_NODE_NAME, json_object_new_string(mb_track_id)); 112 | } 113 | if (strlen(mb_artist_id) > 0) { 114 | json_object *artists_ids = json_object_new_array(); 115 | json_object_array_add(artists_ids, json_object_new_string(mb_artist_id)); 116 | json_object_object_add(additional_info, API_MUSICBRAINZ_ARTISTS_ID_NODE_NAME, artists_ids); 117 | } 118 | if (strlen(mb_album_id) > 0) { 119 | json_object_object_add(additional_info, API_MUSICBRAINZ_ALBUM_ID_NODE_NAME, json_object_new_string(mb_album_id)); 120 | } 121 | if (strlen(track->mb_spotify_id) > 0) { 122 | json_object_object_add(additional_info, API_MUSICBRAINZ_SPOTIFY_ID_NODE_NAME, json_object_new_string(track->mb_spotify_id)); 123 | } 124 | if ( 125 | strlen(track->url) > 0 126 | && (strncmp(track->url, API_PROTO_WHITELIST_HTTP, strlen(API_PROTO_WHITELIST_HTTP)) == 0 127 | || strncmp(track->url, API_PROTO_WHITELIST_HTTPS, strlen(API_PROTO_WHITELIST_HTTPS)) == 0) 128 | ) { 129 | json_object_object_add(additional_info, API_URI_NODE_NAME, json_object_new_string(track->url)); 130 | } 131 | 132 | json_object_object_add(root, API_ADDITIONAL_INFO_NODE_NAME, additional_info); 133 | } 134 | 135 | struct http_header *http_authorization_header_new (const char*); 136 | struct http_header *http_content_type_header_new (void); 137 | static void listenbrainz_api_build_request_now_playing(struct http_request *request, const struct scrobble *tracks[], const unsigned track_count, const struct api_credentials *auth) 138 | { 139 | if (!listenbrainz_valid_credentials(auth)) { return; } 140 | 141 | assert(track_count == 1); 142 | 143 | const struct scrobble *track = tracks[0]; 144 | 145 | const char *token = auth->token; 146 | 147 | char body[MAX_BODY_SIZE+1] = {0}; 148 | 149 | json_object *root = json_object_new_object(); 150 | json_object_object_add(root, API_LISTEN_TYPE_NODE_NAME, json_object_new_string(API_LISTEN_TYPE_NOW_PLAYING)); 151 | 152 | json_object *payload = json_object_new_array(); 153 | 154 | json_object *payload_elem = json_object_new_object(); 155 | json_object *metadata = json_object_new_object(); 156 | if (strlen(track->album) > 0) { 157 | json_object_object_add(metadata, API_ALBUM_NAME_NODE_NAME, json_object_new_string(track->album)); 158 | } 159 | 160 | char full_artist[MAX_PROPERTY_COUNT * (MAX_PROPERTY_LENGTH + 1)] = {0}; 161 | size_t full_artist_len = 0; 162 | for (size_t i = 0; i < array_count(track->artist); i++) { 163 | const char *artist = track->artist[i]; 164 | const size_t artist_len = strlen(artist); 165 | if (NULL == artist || artist_len == 0) { continue; } 166 | 167 | if (full_artist_len > 0) { 168 | const size_t l_val_sep = strlen(VALUE_SEPARATOR); 169 | strncat(full_artist, VALUE_SEPARATOR, l_val_sep + 1); 170 | full_artist_len += l_val_sep; 171 | } 172 | strncat(full_artist, artist, min(MAX_PROPERTY_LENGTH, artist_len)); 173 | full_artist_len += artist_len; 174 | } 175 | if (full_artist_len > 0) { 176 | json_object_object_add(metadata, API_ARTIST_NAME_NODE_NAME, json_object_new_string(full_artist)); 177 | } 178 | json_object_object_add(metadata, API_TRACK_NAME_NODE_NAME, json_object_new_string(track->title)); 179 | 180 | listenbrainz_api_append_additional_info(metadata, track); 181 | 182 | json_object_object_add(payload_elem, API_METADATA_NODE_NAME, metadata); 183 | 184 | json_object_array_add(payload, payload_elem); 185 | json_object_object_add(root, API_PAYLOAD_NODE_NAME, payload); 186 | 187 | const char *json_str = json_object_to_json_string(root); 188 | strncpy(body, json_str, MAX_BODY_SIZE); 189 | 190 | arrput(request->headers, http_authorization_header_new(token)); 191 | arrput(request->headers, http_content_type_header_new()); 192 | 193 | request->request_type = http_post; 194 | memcpy(request->body, body, MAX_BODY_SIZE); 195 | request->body_length = strlen(body); 196 | request->end_point = api_endpoint_new(auth); 197 | api_get_url(request->url, request->end_point); 198 | 199 | json_object_put(root); 200 | } 201 | 202 | /* 203 | */ 204 | static void listenbrainz_api_build_request_scrobble(struct http_request *request, const struct scrobble *tracks[], const unsigned track_count, const struct api_credentials *auth) 205 | { 206 | if (!listenbrainz_valid_credentials(auth)) { return; } 207 | 208 | const char *token = auth->token; 209 | 210 | char body[MAX_BODY_SIZE+1] = {0}; 211 | 212 | json_object *root = json_object_new_object(); 213 | if (track_count > 1) { 214 | json_object_object_add(root, API_LISTEN_TYPE_NODE_NAME, json_object_new_string(API_LISTEN_TYPE_IMPORT)); 215 | } else { 216 | json_object_object_add(root, API_LISTEN_TYPE_NODE_NAME, json_object_new_string(API_LISTEN_TYPE_SINGLE)); 217 | } 218 | 219 | json_object *payload = json_object_new_array(); 220 | for (size_t ti = 0; ti < track_count; ti++) { 221 | const struct scrobble *track = tracks[ti]; 222 | 223 | if (scrobble_is_empty(track)) { 224 | continue; 225 | } 226 | 227 | json_object *payload_elem = json_object_new_object(); 228 | json_object *metadata = json_object_new_object(); 229 | if (strlen(track->album) > 0) { 230 | json_object_object_add(metadata, API_ALBUM_NAME_NODE_NAME, json_object_new_string(track->album)); 231 | } 232 | char full_artist[MAX_PROPERTY_COUNT * (MAX_PROPERTY_LENGTH + 1)] = {0}; 233 | size_t full_artist_len = 0; 234 | for (size_t ai = 0; ai < array_count(track->artist); ai++) { 235 | const char *artist = track->artist[ai]; 236 | const size_t artist_len = strlen(artist); 237 | if (NULL == artist || artist_len == 0) { continue; } 238 | 239 | if (full_artist_len > 0) { 240 | const size_t l_val_sep = strlen(VALUE_SEPARATOR); 241 | strncat(full_artist, VALUE_SEPARATOR, l_val_sep + 1); 242 | full_artist_len += l_val_sep; 243 | } 244 | strncat(full_artist, artist, min(MAX_PROPERTY_LENGTH, artist_len)); 245 | full_artist_len += artist_len; 246 | } 247 | if (full_artist_len > 0) { 248 | json_object_object_add(metadata, API_ARTIST_NAME_NODE_NAME, json_object_new_string(full_artist)); 249 | } 250 | if (strlen(track->title) > 0) { 251 | json_object_object_add(metadata, API_TRACK_NAME_NODE_NAME, json_object_new_string(track->title)); 252 | } 253 | 254 | listenbrainz_api_append_additional_info(metadata, track); 255 | 256 | json_object_object_add(payload_elem, API_LISTENED_AT_NODE_NAME, json_object_new_int64(track->start_time)); 257 | json_object_object_add(payload_elem, API_METADATA_NODE_NAME, metadata); 258 | 259 | json_object_array_add(payload, payload_elem); 260 | } 261 | 262 | json_object_object_add(root, API_PAYLOAD_NODE_NAME, payload); 263 | 264 | const char *json_str = json_object_to_json_string(root); 265 | strncpy(body, json_str, MAX_BODY_SIZE); 266 | 267 | arrput(request->headers, (http_authorization_header_new(token))); 268 | arrput(request->headers, (http_content_type_header_new())); 269 | 270 | request->request_type = http_post; 271 | memcpy(request->body, body, MAX_BODY_SIZE); 272 | request->body_length = strlen(body); 273 | request->end_point = api_endpoint_new(auth); 274 | api_get_url(request->url, request->end_point); 275 | 276 | json_object_put(root); 277 | } 278 | 279 | static bool listenbrainz_json_document_is_error(const char *buffer, const size_t length) 280 | { 281 | // { "code": 401, "error": "You need to provide an Authorization header." } 282 | bool result = false; 283 | 284 | if (NULL == buffer) { return result; } 285 | if (length == 0) { return result; } 286 | 287 | json_object *code_object = NULL; 288 | json_object *err_object = NULL; 289 | 290 | struct json_tokener *tokener = json_tokener_new(); 291 | if (NULL == tokener) { return result; } 292 | json_object *root = json_tokener_parse_ex(tokener, buffer, (int)length); 293 | 294 | if (NULL == root || json_object_object_length(root) < 1) { 295 | goto _exit; 296 | } 297 | json_object_object_get_ex(root, API_CODE_NODE_NAME, &code_object); 298 | json_object_object_get_ex(root, API_ERROR_NODE_NAME, &err_object); 299 | if (NULL == code_object || !json_object_is_type(code_object, json_type_string)) { 300 | goto _exit; 301 | } 302 | if (NULL == err_object || !json_object_is_type(err_object, json_type_int)) { 303 | goto _exit; 304 | } 305 | result = true; 306 | 307 | _exit: 308 | if (NULL != root) { json_object_put(root); } 309 | json_tokener_free(tokener); 310 | return result; 311 | } 312 | 313 | #endif // MPRIS_SCROBBLER_LISTENBRAINZ_API_H 314 | -------------------------------------------------------------------------------- /src/stb_ds.h: -------------------------------------------------------------------------------- 1 | /* stb_ds.h - v0.4 - public domain data structures - Sean Barrett 2019 2 | 3 | This is a single-header-file library that provides easy-to-use 4 | dynamic arrays and hash tables for C (also works in C++). 5 | 6 | For a gentle introduction: 7 | http://nothings.org/stb_ds 8 | 9 | To use this library, do this in *one* C or C++ file: 10 | #define STB_DS_IMPLEMENTATION 11 | #include "stb_ds.h" 12 | 13 | TABLE OF CONTENTS 14 | 15 | Table of Contents 16 | Compile-time options 17 | License 18 | Documentation 19 | Notes 20 | Credits 21 | 22 | COMPILE-TIME OPTIONS 23 | 24 | #define STBDS_NO_SHORT_NAMES 25 | 26 | This flag needs to be set globally. 27 | 28 | By default stb_ds exposes shorter function names that are not qualified 29 | with the "stbds_" prefix. If these names conflict with the names in your 30 | code, define this flag. 31 | 32 | #define STBDS_SIPHASH_2_4 33 | 34 | This flag only needs to be set in the file containing #define STB_DS_IMPLEMENTATION. 35 | 36 | By default stb_ds.h hashes using a weaker variant of SipHash and a custom hash for 37 | 4- and 8-byte keys. On 64-bit platforms, you can define the above flag to force 38 | stb_ds.h to use specification-compliant SipHash-2-4 for all keys. Doing so makes 39 | hash table insertion about 20% slower on 4- and 8-byte keys, 5% slower on 40 | 64-byte keys, and 10% slower on 256-byte keys on my test computer. 41 | 42 | LICENSE 43 | 44 | Placed in the public domain and also MIT licensed. 45 | See end of file for detailed license information. 46 | 47 | DOCUMENTATION 48 | 49 | Dynamic Arrays 50 | 51 | Non-function interface: 52 | 53 | Declare an empty dynamic array of type T 54 | T* foo = NULL; 55 | 56 | Access the i'th item of a dynamic array 'foo' of type T, T* foo: 57 | foo[i] 58 | 59 | Functions (actually macros) 60 | 61 | arrfree: 62 | void arrfree(T*); 63 | Frees the array. 64 | 65 | arrlen: 66 | ptrdiff_t arrlen(T*); 67 | Returns the number of elements in the array. 68 | 69 | arrlenu: 70 | size_t arrlenu(T*); 71 | Returns the number of elements in the array as an unsigned type. 72 | 73 | arrpop: 74 | T arrpop(T* a) 75 | Removes the final element of the array and returns it. 76 | 77 | arrput: 78 | T arrput(T* a, T b); 79 | Appends the item b to the end of array a. Returns b. 80 | 81 | arrins: 82 | T arrins(T* a, int p, T b); 83 | Inserts the item b into the middle of array a, into a[p], 84 | moving the rest of the array over. Returns b. 85 | 86 | arrinsn: 87 | void arrins(T* a, int p, int n); 88 | Inserts n uninitialized items into array a starting at a[p], 89 | moving the rest of the array over. 90 | 91 | arrdel: 92 | void arrdel(T* a, int p); 93 | Deletes the element at a[p], moving the rest of the array over. 94 | 95 | arrdeln: 96 | void arrdel(T* a, int p, int n); 97 | Deletes n elements starting at a[p], moving the rest of the array over. 98 | 99 | arrdelswap: 100 | void arrdelswap(T* a, int p); 101 | Deletes the element at a[p], replacing it with the element from 102 | the end of the array. O(1) performance. 103 | 104 | arrsetlen: 105 | void arrsetlen(T* a, int n); 106 | Changes the length of the array to n. Allocates uninitialized 107 | slots at the end if necessary. 108 | 109 | arrsetcap: 110 | size_t arrsetcap(T* a, int n); 111 | Sets the length of allocated storage to at least n. It will not 112 | change the length of the array. 113 | 114 | arrcap: 115 | size_t arrcap(T* a); 116 | Returns the number of total elements the array can contain without 117 | needing to be reallocated. 118 | 119 | NOTES 120 | 121 | * These data structures are realloc'd when they grow, and the macro "functions" 122 | write to the provided pointer. This means: (a) the pointer must be an lvalue, 123 | and (b) the pointer to the data structure is not stable, and you must maintain 124 | it the same as you would a realloc'd pointer. For example, if you pass a pointer 125 | to a dynamic array to a function which updates it, the function must return 126 | back the new pointer to the caller. This is the price of trying to do this in C. 127 | 128 | * You iterate over the contents of a dynamic array and a hashmap in exactly 129 | the same way, using arrlen/hmlen/shlen: 130 | 131 | for (i=0; i < arrlen(foo); ++i) 132 | ... foo[i] ... 133 | 134 | * All operations except arrins/arrdel are O(1) amortized, but individual 135 | operations can be slow, so these data structures may not be suitable 136 | for real time use. Dynamic arrays double in capacity as needed, so 137 | elements are copied an average of once. Hash tables double/halve 138 | their size as needed, with appropriate hysteresis to maintain O(1) 139 | performance. 140 | 141 | NOTES - DYNAMIC ARRAY 142 | 143 | * If you know how long a dynamic array is going to be in advance, you can avoid 144 | extra memory allocations by using arrsetlen to allocate it to that length in 145 | advance and use foo[n] while filling it out, or arrsetcap to allocate the memory 146 | for that length and use arrput/arrpush as normal. 147 | 148 | * Unlike some other versions of the dynamic array, this version should 149 | be safe to use with strict-aliasing optimizations. 150 | 151 | CREDITS 152 | 153 | Sean Barrett -- library, idea for dynamic array API/implementation 154 | Per Vognsen -- idea for hash table API/implementation 155 | Rafael Sachetto -- arrpop() 156 | */ 157 | #pragma GCC diagnostic ignored "-Wuninitialized" 158 | #pragma GCC diagnostic ignored "-Wunused-parameter" 159 | #pragma GCC diagnostic ignored "-Wunused-result" 160 | #pragma GCC diagnostic ignored "-Wunused-function" 161 | #pragma GCC diagnostic ignored "-Wimplicit-fallthrough" 162 | 163 | #ifndef INCLUDE_STB_DS_H 164 | #define INCLUDE_STB_DS_H 165 | 166 | #include 167 | #include 168 | #include 169 | 170 | #ifndef STBDS_NO_SHORT_NAMES 171 | #define arrlen stbds_arrlen 172 | #define arrlenu stbds_arrlenu 173 | #define arrput stbds_arrput 174 | #define arrpush stbds_arrput 175 | #define arrpop stbds_arrpop 176 | #define arrfree stbds_arrfree 177 | #define arraddn stbds_arraddn 178 | #define arrsetlen stbds_arrsetlen 179 | #define arrlast stbds_arrlast 180 | #define arrins stbds_arrins 181 | #define arrinsn stbds_arrinsn 182 | #define arrdel stbds_arrdel 183 | #define arrdeln stbds_arrdeln 184 | #define arrdelswap stbds_arrdelswap 185 | #define arrcap stbds_arrcap 186 | #define arrsetcap stbds_arrsetcap 187 | #endif 188 | 189 | /////////////// 190 | // 191 | // Everything below here is implementation details 192 | // 193 | 194 | extern void * stbds_arrgrowf(void *a, size_t elemsize, size_t addlen, size_t min_cap); 195 | 196 | 197 | #if defined(__GNUC__) || defined(__clang__) 198 | #define STBDS_HAS_TYPEOF 199 | #endif 200 | 201 | #if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L 202 | #define STBDS_HAS_LITERAL_ARRAY 203 | #endif 204 | 205 | #define stbds_header(t) ((stbds_array_header *) (t) - 1) 206 | 207 | #define stbds_arrsetcap(a,n) (stbds_arrgrow(a,0,n)) 208 | #define stbds_arrsetlen(a,n) ((stbds_arrcap(a) < n ? stbds_arrsetcap(a,n),0 : 0), (a) ? stbds_header(a)->length = (n) : 0) 209 | #define stbds_arrcap(a) ((a) ? stbds_header(a)->capacity : 0) 210 | #define stbds_arrlen(a) (size_t)((a) ? (ptrdiff_t) stbds_header(a)->length : 0) 211 | #define stbds_arrlenu(a) ((a) ? stbds_header(a)->length : 0) 212 | #define stbds_arrput(a,v) (stbds_arrmaybegrow(a,1), (a)[stbds_header(a)->length++] = (v)) 213 | #define stbds_arrpush stbds_arrput // synonym 214 | #define stbds_arrpop(a) (stbds_header(a)->length--, (a)[stbds_header(a)->length]) 215 | #define stbds_arraddn(a,n) (stbds_arrmaybegrow(a,n), stbds_header(a)->length += (n)) 216 | #define stbds_arrlast(a) ((a)[stbds_header(a)->length-1]) 217 | #define stbds_arrfree(a) ((void) ((a) ? realloc(stbds_header(a),0) : 0), (a)=NULL) 218 | #define stbds_arrdel(a,i) stbds_arrdeln(a,i,1) 219 | #define stbds_arrdeln(a,i,n) (memmove(&(a)[i], &(a)[(i)+(n)], sizeof *(a) * (stbds_header(a)->length-(n)-(i))), stbds_header(a)->length -= (n)) 220 | #define stbds_arrdelswap(a,i) ((a)[i] = stbds_arrlast(a), stbds_header(a)->length -= 1) 221 | #define stbds_arrinsn(a,i,n) (stbds_arraddn((a),(n)), memmove(&(a)[(i)+(n)], &(a)[i], sizeof *(a) * (stbds_header(a)->length-(n)-(i)))) 222 | #define stbds_arrins(a,i,v) (stbds_arrinsn((a),(i),1), (a)[i]=(v)) 223 | 224 | #define stbds_arrmaybegrow(a,n) ((!(a) || stbds_header(a)->length + (n) > stbds_header(a)->capacity) \ 225 | ? (stbds_arrgrow(a,n,0),0) : 0) 226 | 227 | #define stbds_arrgrow(a,b,c) ((a) = stbds_arrgrowf_wrapper((a), sizeof *(a), (b), (c))) 228 | 229 | 230 | typedef struct 231 | { 232 | size_t length; 233 | size_t capacity; 234 | void * hash_table; 235 | ptrdiff_t temp; 236 | } stbds_array_header; 237 | 238 | #define stbds_arrgrowf_wrapper stbds_arrgrowf 239 | 240 | #endif // INCLUDE_STB_DS_H 241 | 242 | 243 | ////////////////////////////////////////////////////////////////////////////// 244 | // 245 | // IMPLEMENTATION 246 | // 247 | 248 | #ifdef STB_DS_IMPLEMENTATION 249 | #include 250 | #include 251 | 252 | #ifndef STBDS_ASSERT 253 | #define STBDS_ASSERT_WAS_UNDEFINED 254 | #define STBDS_ASSERT(x) ((void) 0) 255 | #endif 256 | 257 | #ifdef STBDS_STATISTICS 258 | #define STBDS_STATS(x) x 259 | size_t stbds_array_grow; 260 | #else 261 | #define STBDS_STATS(x) 262 | #endif 263 | 264 | // 265 | // stbds_arr implementation 266 | // 267 | 268 | void *stbds_arrgrowf(void *a, size_t elemsize, size_t addlen, size_t min_cap) 269 | { 270 | void *b; 271 | size_t min_len = stbds_arrlen(a) + addlen; 272 | 273 | // compute the minimum capacity needed 274 | if (min_len > min_cap) 275 | min_cap = min_len; 276 | 277 | if (min_cap <= stbds_arrcap(a)) 278 | return a; 279 | 280 | // increase needed capacity to guarantee O(1) amortized 281 | if (min_cap < 2 * stbds_arrcap(a)) 282 | min_cap = 2 * stbds_arrcap(a); 283 | else if (min_cap < 4) 284 | min_cap = 4; 285 | 286 | b = realloc((a) ? stbds_header(a) : 0, elemsize * min_cap + sizeof(stbds_array_header)); 287 | b = (char *) b + sizeof(stbds_array_header); 288 | if (a == NULL) { 289 | stbds_header(b)->length = 0; 290 | stbds_header(b)->hash_table = 0; 291 | } else { 292 | STBDS_STATS(++stbds_array_grow); 293 | } 294 | stbds_header(b)->capacity = min_cap; 295 | return b; 296 | } 297 | 298 | #endif 299 | /* 300 | ------------------------------------------------------------------------------ 301 | This software is available under 2 licenses -- choose whichever you prefer. 302 | ------------------------------------------------------------------------------ 303 | ALTERNATIVE A - MIT License 304 | Copyright (c) 2019 Sean Barrett 305 | Permission is hereby granted, free of charge, to any person obtaining a copy of 306 | this software and associated documentation files (the "Software"), to deal in 307 | the Software without restriction, including without limitation the rights to 308 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 309 | of the Software, and to permit persons to whom the Software is furnished to do 310 | so, subject to the following conditions: 311 | The above copyright notice and this permission notice shall be included in all 312 | copies or substantial portions of the Software. 313 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 314 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 315 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 316 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 317 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 318 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 319 | SOFTWARE. 320 | ------------------------------------------------------------------------------ 321 | ALTERNATIVE B - Public Domain (www.unlicense.org) 322 | This is free and unencumbered software released into the public domain. 323 | Anyone is free to copy, modify, publish, use, compile, sell, or distribute this 324 | software, either in source code form or as a compiled binary, for any purpose, 325 | commercial or non-commercial, and by any means. 326 | In jurisdictions that recognize copyright laws, the author or authors of this 327 | software dedicate any and all copyright interest in the software to the public 328 | domain. We make this dedication for the benefit of the public at large and to 329 | the detriment of our heirs and successors. We intend this dedication to be an 330 | overt act of relinquishment in perpetuity of all present and future rights to 331 | this software under copyright law. 332 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 333 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 334 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 335 | AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 336 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 337 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 338 | ------------------------------------------------------------------------------ 339 | */ 340 | -------------------------------------------------------------------------------- /src/curl.h: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @author Marius Orcsik 4 | */ 5 | #ifndef MPRIS_SCROBBLER_CURL_H 6 | #define MPRIS_SCROBBLER_CURL_H 7 | 8 | #include 9 | 10 | #ifdef RETRY_ENABLED 11 | 12 | #define MAX_RETRIES 5 13 | 14 | static void retry_cb(int fd, short kind, void *data) 15 | { 16 | assert(data); 17 | 18 | const struct scrobbler_connection *conn = data; 19 | assert(conn->parent); 20 | assert(conn->parent->handle); 21 | assert(conn->handle); 22 | curl_multi_add_handle(conn->parent->handle, conn->handle); 23 | } 24 | 25 | static void connection_retry(struct scrobbler_connection *conn) 26 | { 27 | const struct timeval retry_timeout = { .tv_sec = 3 + conn->retries, .tv_usec = 0, }; 28 | http_response_clean(&conn->response); 29 | 30 | const struct scrobbler *s = conn->parent; 31 | curl_multi_remove_handle(conn->parent->handle, conn->handle); 32 | 33 | if (conn->retries == 0) { 34 | evtimer_assign(&conn->retry_event, s->evbase, retry_cb, conn); 35 | } else { 36 | evtimer_del(&conn->retry_event); 37 | } 38 | evtimer_add(&conn->retry_event, &retry_timeout); 39 | conn->retries++; 40 | _debug("curl::retrying[%zd]: in %2.2lfs", conn->retries, timeval_to_seconds(retry_timeout)); 41 | } 42 | 43 | static bool connection_should_retry(const struct scrobbler_connection *conn) 44 | { 45 | const int code = (int)conn->response.code; 46 | if (code >= 400 && code < 500) { 47 | // NOTE(marius): 4XX errors mean that there's something wrong with our request 48 | // so we shouldn't retry. 49 | return false; 50 | } 51 | return conn->retries < MAX_RETRIES; 52 | } 53 | #endif 54 | 55 | static bool connection_was_fulfilled(const struct scrobbler_connection *); 56 | /* 57 | * Based on https://curl.se/libcurl/c/hiperfifo.html 58 | * Check for completed transfers, and remove their easy handles 59 | */ 60 | static void check_multi_info(struct scrobbler *s) 61 | { 62 | char *eff_url; 63 | int msgs_left; 64 | struct scrobbler_connection *conn; 65 | CURL *easy; 66 | CURLMsg *msg; 67 | CURLcode res; 68 | long code = -1; 69 | _trace2("curl::check_multi_info[%p]: remaining %d", s, s->still_running); 70 | 71 | while((msg = curl_multi_info_read(s->handle, &msgs_left))) { 72 | if(msg->msg != CURLMSG_DONE) { 73 | continue; 74 | } 75 | 76 | easy = msg->easy_handle; 77 | res = msg->data.result; 78 | curl_easy_getinfo(easy, CURLINFO_PRIVATE, &conn); 79 | curl_easy_getinfo(easy, CURLINFO_EFFECTIVE_URL, &eff_url); 80 | curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &code); 81 | 82 | assert(conn); 83 | 84 | if (strlen(conn->error) != 0) { 85 | _warn("curl::transfer::done[%zd]: %s => (%d) %s", conn->idx, eff_url, res, conn->error); 86 | } else { 87 | _trace("curl::transfer::done[%zd]: %s", conn->idx, eff_url); 88 | conn->response.code = code; 89 | } 90 | 91 | http_response_print(&conn->response, log_tracing2); 92 | 93 | const bool success = conn->response.code == 200; 94 | _info(" api::submitted_to[%s]: %s", get_api_type_label(conn->credentials.end_point), (success ? "ok" : "nok")); 95 | if(evtimer_pending(&s->timer_event, NULL)) { 96 | _trace2("curl::multi_timer_remove(%p)", &s->timer_event); 97 | evtimer_del(&s->timer_event); 98 | } 99 | #ifdef RETRY_ENABLED 100 | if (!connection_was_fulfilled(conn) && connection_should_retry(conn)) { 101 | connection_retry(conn); 102 | } else { 103 | #else 104 | if (!connection_was_fulfilled(conn)) { 105 | #endif 106 | conn->should_free = true; 107 | } 108 | } 109 | } 110 | 111 | static void scrobbler_connections_clean(struct scrobble_connections*, const bool); 112 | 113 | /* 114 | * Based on https://curl.se/libcurl/c/hiperfifo.html 115 | * Called by libevent when our timeout expires 116 | */ 117 | static void timer_cb(int fd, short kind, void *data) 118 | { 119 | assert(data); 120 | 121 | struct scrobbler *s = data; 122 | 123 | const CURLMcode rc = curl_multi_socket_action(s->handle, CURL_SOCKET_TIMEOUT, 0, &s->still_running); 124 | const char *res; 125 | switch(rc) { 126 | case CURLM_OK: 127 | res = "OK"; 128 | break; 129 | case CURLM_BAD_HANDLE: 130 | res = "bad multi handle"; 131 | break; 132 | case CURLM_BAD_EASY_HANDLE: 133 | res = "bad easy handle"; 134 | break; 135 | case CURLM_OUT_OF_MEMORY: 136 | res = "OOM"; 137 | break; 138 | case CURLM_INTERNAL_ERROR: 139 | res = "internal error"; 140 | break; 141 | case CURLM_UNKNOWN_OPTION: 142 | res = "unknown option"; 143 | break; 144 | case CURLM_LAST: 145 | res = "last"; 146 | break; 147 | case CURLM_BAD_SOCKET: 148 | _warn("curl::multi_socket_activation:error: %s", curl_multi_strerror(rc)); 149 | return; 150 | default: 151 | res = "unknown"; 152 | } 153 | _trace2("curl::multi_socket_activation:%s[%p:%d]: still_running: %d", res, s, fd, s->still_running); 154 | 155 | check_multi_info(s); 156 | 157 | scrobbler_connections_clean(&s->connections, false); 158 | } 159 | 160 | /* Called by libevent when we get action on a multi socket */ 161 | static void event_cb(int fd, short kind, void *data) 162 | { 163 | assert(data); 164 | 165 | struct scrobbler *s = (struct scrobbler*)data; 166 | assert(&s->connections); 167 | 168 | const int action = ((kind & EV_READ) ? CURL_CSELECT_IN : 0) | ((kind & EV_WRITE) ? CURL_CSELECT_OUT : 0); 169 | 170 | const CURLMcode rc = curl_multi_socket_action(s->handle, fd, action, &s->still_running); 171 | if (rc != CURLM_OK) { 172 | _warn("curl::transfer::error: %s", curl_multi_strerror(rc)); 173 | return; 174 | } 175 | 176 | _trace2("curl::event_cb(%p:%p:%zd:%zd): still running: %d", s, s->handle, fd, action, s->still_running); 177 | 178 | check_multi_info(s); 179 | 180 | scrobbler_connections_clean(&s->connections, false); 181 | } 182 | 183 | /* 184 | * Based on https://curl.se/libcurl/c/hiperfifo.html 185 | * Assign information to a scrobbler_connection structure 186 | */ 187 | static void setsock(struct scrobbler_connection *conn, curl_socket_t sock, CURL *e, int act, struct scrobbler *s) 188 | { 189 | const short kind = ((act & CURL_POLL_IN) ? EV_READ : 0) | ((act & CURL_POLL_OUT) ? EV_WRITE : 0) | EV_PERSIST; 190 | 191 | if (conn->handle != e) { 192 | _trace("curl::mismatched_handle %p %p kind %zd", conn->handle, e, kind); 193 | return; 194 | } 195 | 196 | conn->sockfd = sock; 197 | conn->action = act; 198 | 199 | if (event_initialized(&conn->ev)) { 200 | event_del(&conn->ev); 201 | } 202 | 203 | evutil_make_socket_nonblocking(conn->sockfd); 204 | event_assign(&conn->ev, s->evbase, conn->sockfd, kind, event_cb, s); 205 | event_add(&conn->ev, NULL); 206 | } 207 | 208 | const char *whatstr[]={ "none", "IN", "OUT", "INOUT", "REMOVE" }; 209 | static struct scrobbler_connection *scrobbler_connection_get(const struct scrobble_connections*, const CURL*); 210 | /* CURLMOPT_SOCKETFUNCTION */ 211 | static int curl_request_has_data(CURL *e, const curl_socket_t sock, const int what, void *data, void *conn_data) 212 | { 213 | if (NULL == data) { return CURLM_OK; } 214 | 215 | struct scrobbler *s = data; 216 | int events = 0; 217 | 218 | struct scrobbler_connection *conn = conn_data; 219 | _trace2("curl::data_callback[%p:%zd]: s: %p, conn: %p", e, sock, data, conn_data); 220 | switch(what) { 221 | case CURL_POLL_REMOVE: 222 | if (conn) { 223 | _trace2("curl::data_remove[%p]: action=%s", e, whatstr[what]); 224 | if(event_initialized(&conn->ev)) { 225 | event_del(&conn->ev); 226 | } 227 | conn->should_free = true; 228 | } 229 | break; 230 | case CURL_POLL_IN: 231 | case CURL_POLL_OUT: 232 | case CURL_POLL_INOUT: 233 | bool missing_connection = (NULL == conn); 234 | if (missing_connection) { 235 | conn = scrobbler_connection_get(&s->connections, e); 236 | if (NULL == conn) { 237 | const CURLcode status = curl_easy_getinfo(e, CURLINFO_PRIVATE, &conn); 238 | if (NULL == conn) { 239 | return status; 240 | } 241 | } 242 | } 243 | if (!missing_connection) { 244 | curl_multi_assign(s->handle, conn->sockfd, conn); 245 | } else { 246 | // set libevent context for a new connection 247 | if(what != CURL_POLL_IN) events |= EV_WRITE; 248 | if(what != CURL_POLL_OUT) events |= EV_READ; 249 | 250 | events |= EV_PERSIST; 251 | setsock(conn, sock, e, what, s); 252 | if (conn->action != what) { 253 | _trace2("curl::data_callback[%zd:%p]: s=%d, changing action=%s->%s", conn->idx, e, sock, whatstr[conn->action], whatstr[what]); 254 | } else { 255 | _trace2("curl::data_callback[%zd:%p]: s=%d, action=%s", conn->idx, e, sock, whatstr[what]); 256 | } 257 | } 258 | 259 | break; 260 | default: 261 | _trace2("curl::unknown_socket_action[%p]: action=%s", e, whatstr[what]); 262 | assert(false); 263 | } 264 | 265 | return CURLM_OK; 266 | } 267 | 268 | /* Update the event timer after curl_multi library calls */ 269 | static int curl_request_wait_timeout(CURLM *multi, const long timeout_ms, struct scrobbler *s) 270 | { 271 | assert(multi); 272 | assert(s); 273 | 274 | const struct timeval timeout = { 275 | .tv_sec = timeout_ms/1000, 276 | .tv_usec = (timeout_ms % 1000) * 1000, 277 | }; 278 | 279 | /** 280 | * if timeout_ms is -1, just delete the timer 281 | * 282 | * For all other values of timeout_ms, this should set or *update* 283 | * the timer to the new value 284 | */ 285 | if (timeout_ms == -1) { 286 | _trace2("curl::multi_timer_remove(%p:%p)", multi, s->timer_event); 287 | evtimer_del(&s->timer_event); 288 | } else { 289 | _trace2("curl::multi_timer_update(%p:%p):still_running: %d, timeout: %d", s, &s->timer_event, s->still_running, timeout_ms); 290 | evtimer_add(&s->timer_event, &timeout); 291 | } 292 | 293 | return 0; 294 | } 295 | 296 | static size_t http_response_write_body(void *buffer, size_t size, size_t nmemb, void* data) 297 | { 298 | if (NULL == buffer) { return 0; } 299 | if (NULL == data) { return 0; } 300 | if (0 == size) { return 0; } 301 | if (0 == nmemb) { return 0; } 302 | 303 | 304 | struct scrobbler_connection *conn = (struct scrobbler_connection*)data; 305 | struct http_response *res = &conn->response; 306 | assert(res); 307 | 308 | const size_t new_size = size * nmemb; 309 | 310 | strncat(res->body, buffer, new_size); 311 | res->body_length += new_size; 312 | 313 | assert (res->body_length < MAX_BODY_SIZE); 314 | memset(res->body + res->body_length, 0x0, MAX_BODY_SIZE - res->body_length); 315 | 316 | return new_size; 317 | } 318 | 319 | static size_t http_response_write_headers(char *buffer, size_t size, size_t nitems, void* data) 320 | { 321 | if (NULL == buffer) { return 0; } 322 | if (NULL == data) { return 0; } 323 | if (0 == size) { return 0; } 324 | if (0 == nitems) { return 0; } 325 | 326 | struct scrobbler_connection *conn = (struct scrobbler_connection *)data; 327 | struct http_response *res = &conn->response; 328 | 329 | const size_t new_size = size * nitems; 330 | 331 | struct http_header *h = http_header_new(); 332 | 333 | http_header_load(buffer, nitems, h); 334 | if (_is_zero(h->name)) { goto _err_exit; } 335 | 336 | arrput(res->headers, h); 337 | return new_size; 338 | 339 | _err_exit: 340 | http_header_free(h); 341 | return new_size; 342 | } 343 | 344 | #if defined(LIBCURL_DEBUG) && LIBCURL_DEBUG 345 | static int curl_debug(CURL *handle, curl_infotype type, char *data, size_t size, void *userp) 346 | { 347 | /* prevent compiler warning */ 348 | (void)handle; 349 | (void)size; 350 | 351 | data = grrrs_from_string(data); 352 | grrrs_trim(data, NULL); 353 | switch(type) { 354 | case CURLINFO_TEXT: 355 | _trace2("curl::debug: %s", data); 356 | break; 357 | case CURLINFO_HEADER_OUT: 358 | _trace2("curl::debug: Send header: %s", data); 359 | break; 360 | case CURLINFO_DATA_OUT: 361 | _trace2("curl::debug: Send data: %s", data); 362 | break; 363 | case CURLINFO_HEADER_IN: 364 | _trace2("curl::debug: Recv header: %s", data); 365 | break; 366 | case CURLINFO_DATA_IN: 367 | _trace2("curl::debug: Recv data: %s", data); 368 | break; 369 | case CURLINFO_SSL_DATA_OUT: 370 | _trace2("curl::debug: Send SSL data"); 371 | break; 372 | case CURLINFO_SSL_DATA_IN: 373 | _trace2("curl::debug: Recv SSL data"); 374 | break; 375 | default: /* in case a new one is introduced to shock us */ 376 | break; 377 | } 378 | grrrs_free(data); 379 | 380 | return 0; 381 | } 382 | #endif 383 | 384 | static void build_curl_request(struct scrobbler_connection *conn) 385 | { 386 | assert (NULL != conn); 387 | 388 | CURL *handle = conn->handle; 389 | 390 | #if defined(LIBCURL_DEBUG) && LIBCURL_DEBUG 391 | if (_log_level >= log_tracing) { 392 | curl_easy_setopt(handle, CURLOPT_VERBOSE, 1L); 393 | curl_easy_setopt(handle, CURLOPT_DEBUGFUNCTION, curl_debug); 394 | } 395 | #endif 396 | 397 | const struct http_request *req = &conn->request; 398 | struct curl_slist ***req_headers = &conn->headers; 399 | 400 | if (NULL == handle) { return; } 401 | const enum http_request_types t = req->request_type; 402 | 403 | if (t == http_post) { 404 | curl_easy_setopt(handle, CURLOPT_POST, 1L); 405 | curl_easy_setopt(handle, CURLOPT_POSTFIELDS, req->body); 406 | curl_easy_setopt(handle, CURLOPT_POSTFIELDSIZE, (long)req->body_length); 407 | } 408 | 409 | http_request_print(req, log_tracing2); 410 | 411 | curl_easy_setopt(handle, CURLOPT_PRIVATE, conn); 412 | curl_easy_setopt(handle, CURLOPT_ERRORBUFFER, conn->error); 413 | curl_easy_setopt(handle, CURLOPT_TIMEOUT_MS, MAX_WAIT_SECONDS * 1000L); 414 | 415 | curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, 1L); 416 | curl_easy_setopt(handle, CURLOPT_CURLU, req->url); 417 | curl_easy_setopt(handle, CURLOPT_HEADER, 0L); 418 | const size_t headers_count = arrlen(req->headers); 419 | if (headers_count > 0) { 420 | struct curl_slist *headers = NULL; 421 | 422 | for (size_t i = 0; i < headers_count; i++) { 423 | struct http_header *header = req->headers[i]; 424 | char full_header[MAX_URL_LENGTH] = {0}; 425 | snprintf(full_header, MAX_URL_LENGTH, "%s: %s", header->name, header->value); 426 | 427 | headers = curl_slist_append(headers, full_header); 428 | } 429 | curl_easy_setopt(handle, CURLOPT_HTTPHEADER, headers); 430 | arrput(*req_headers, headers); 431 | } 432 | 433 | curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, http_response_write_body); 434 | curl_easy_setopt(handle, CURLOPT_WRITEDATA, conn); 435 | curl_easy_setopt(handle, CURLOPT_HEADERFUNCTION, http_response_write_headers); 436 | curl_easy_setopt(handle, CURLOPT_HEADERDATA, conn); 437 | } 438 | 439 | #endif // MPRIS_SCROBBLER_CURL_H 440 | -------------------------------------------------------------------------------- /src/configuration.h: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @author Marius Orcsik 4 | */ 5 | #ifndef MPRIS_SCROBBLER_CONFIGURATION_H 6 | #define MPRIS_SCROBBLER_CONFIGURATION_H 7 | 8 | #include 9 | #include 10 | #include "ini.h" 11 | 12 | #ifndef APPLICATION_NAME 13 | #define APPLICATION_NAME "mpris-scrobbler" 14 | #endif 15 | 16 | #define PID_SUFFIX ".pid" 17 | #define CREDENTIALS_FILE_NAME "credentials" 18 | #define CACHE_FILE_NAME "queue" 19 | #define CONFIG_FILE_NAME "config" 20 | #define CONFIG_DIR_NAME ".config" 21 | #define CACHE_DIR_NAME ".cache" 22 | #define DATA_DIR_NAME ".local/share" 23 | #define HOME_DIR "/home" 24 | 25 | #define TOKENIZED_CONFIG_DIR "%s/%s" 26 | #define TOKENIZED_DATA_DIR "%s/%s" 27 | #define TOKENIZED_CACHE_DIR "%s/%s" 28 | #define TOKENIZED_CONFIG_PATH "%s/%s/%s" 29 | #define TOKENIZED_PID_PATH "%s/%s%s" 30 | #define TOKENIZED_CREDENTIALS_PATH "%s/%s/%s" 31 | #define TOKENIZED_CACHE_PATH "%s/%s/%s" 32 | 33 | #define HOME_VAR_NAME "HOME" 34 | #define USERNAME_VAR_NAME "USER" 35 | #define XDG_CONFIG_HOME_VAR_NAME "XDG_CONFIG_HOME" 36 | #define XDG_DATA_HOME_VAR_NAME "XDG_DATA_HOME" 37 | #define XDG_CACHE_HOME_VAR_NAME "XDG_CACHE_HOME" 38 | #define XDG_RUNTIME_DIR_VAR_NAME "XDG_RUNTIME_DIR" 39 | 40 | #define CONFIG_VALUE_TRUE "true" 41 | #define CONFIG_VALUE_ONE "1" 42 | #define CONFIG_VALUE_FALSE "false" 43 | #define CONFIG_VALUE_ZERO "0" 44 | #define CONFIG_KEY_ENABLED "enabled" 45 | #define CONFIG_KEY_USER_NAME "username" 46 | #define CONFIG_KEY_PASSWORD "password" 47 | #define CONFIG_KEY_TOKEN "token" 48 | #define CONFIG_KEY_SESSION "session" 49 | #define CONFIG_KEY_URL "url" 50 | #define SERVICE_LABEL_LASTFM "lastfm" 51 | #define SERVICE_LABEL_LIBREFM "librefm" 52 | #define SERVICE_LABEL_LISTENBRAINZ "listenbrainz" 53 | #define CONFIG_KEY_IGNORE "ignore" 54 | 55 | static const char *get_api_type_group(enum api_type end_point) 56 | { 57 | const char *api_label = NULL; 58 | switch (end_point) { 59 | case(api_lastfm): 60 | api_label = "lastfm"; 61 | break; 62 | case(api_librefm): 63 | api_label = "librefm"; 64 | break; 65 | case(api_listenbrainz): 66 | api_label = "listenbrainz"; 67 | break; 68 | case(api_unknown): 69 | default: 70 | api_label = NULL; 71 | break; 72 | } 73 | 74 | return api_label; 75 | } 76 | 77 | static struct ini_config *get_ini_from_credentials(struct api_credentials credentials[MAX_CREDENTIALS], const size_t length) 78 | { 79 | if (NULL == credentials) { return NULL; } 80 | if (length == 0) { return NULL; } 81 | 82 | struct ini_config *creds_config = ini_config_new(); 83 | if (NULL == creds_config) { return NULL; } 84 | 85 | for (size_t i = 0; i < length; i++) { 86 | struct api_credentials *current = &credentials[i]; 87 | if (NULL == current) { continue; } 88 | if (current->end_point == api_unknown) { continue; } 89 | 90 | const char *label = get_api_type_group(current->end_point); 91 | struct ini_group *group = ini_group_new(label); 92 | 93 | struct ini_value *enabled = ini_value_new(CONFIG_KEY_ENABLED, current->enabled ? "true" : "false"); 94 | ini_group_append_value(group, enabled); 95 | 96 | if (strlen(current->user_name) > 0) { 97 | struct ini_value *user_name = ini_value_new(CONFIG_KEY_USER_NAME, (char*)current->user_name); 98 | ini_group_append_value(group, user_name); 99 | } 100 | if (strlen(current->token) > 0) { 101 | struct ini_value *token = ini_value_new(CONFIG_KEY_TOKEN, (char*)current->token); 102 | ini_group_append_value(group, token); 103 | } 104 | if (strlen(current->session_key) > 0) { 105 | struct ini_value *session_key = ini_value_new(CONFIG_KEY_SESSION, (char*)current->session_key); 106 | ini_group_append_value(group, session_key); 107 | } 108 | if (strlen(current->url) > 0) { 109 | struct ini_value *url = ini_value_new(CONFIG_KEY_URL, (char*)current->url); 110 | ini_group_append_value(group, url); 111 | } 112 | 113 | ini_config_append_group(creds_config, group); 114 | } 115 | 116 | return creds_config; 117 | } 118 | 119 | static struct api_credentials *api_credentials_new(void) 120 | { 121 | struct api_credentials *credentials = calloc(1, sizeof(struct api_credentials)); 122 | if (NULL == credentials) { return NULL; } 123 | 124 | credentials->end_point = api_unknown; 125 | credentials->enabled = false; 126 | credentials->authenticated = false; 127 | return credentials; 128 | } 129 | 130 | static void api_credentials_disable(struct api_credentials *credentials) 131 | { 132 | // NOTE(marius): in here we don't need to zero the entire credentials struct, 133 | // but only the authorization session ones. 134 | // We need to preserve the end_point, the api_key and the secret. 135 | memset(credentials->user_name, 0x0, MAX_SECRET_LENGTH); 136 | memset(credentials->password, 0x0, MAX_SECRET_LENGTH); 137 | memset((char*)credentials->token, 0x0, MAX_SECRET_LENGTH); 138 | memset((char*)credentials->session_key, 0x0, MAX_SECRET_LENGTH); 139 | if (strlen(credentials->url) > 0) { 140 | memset((char*)credentials->url, 0x0, MAX_PROPERTY_LENGTH); 141 | } 142 | 143 | credentials->enabled = false; 144 | } 145 | 146 | static void api_credentials_free(struct api_credentials *credentials) 147 | { 148 | if (NULL == credentials) { return; } 149 | if (credentials->enabled) { 150 | _trace2("mem::free::credentials(%p): %s", credentials, get_api_type_label(credentials->end_point)); 151 | } 152 | 153 | free(credentials); 154 | } 155 | 156 | extern char **environ; 157 | static void load_environment(struct env_variables *env) 158 | { 159 | if (NULL == env) { return; } 160 | 161 | const size_t home_var_len = strlen(HOME_VAR_NAME); 162 | const size_t username_var_len = strlen(USERNAME_VAR_NAME); 163 | const size_t config_home_var_len = strlen(XDG_CONFIG_HOME_VAR_NAME); 164 | const size_t data_home_var_len = strlen(XDG_DATA_HOME_VAR_NAME); 165 | const size_t cache_home_var_len = strlen(XDG_CACHE_HOME_VAR_NAME); 166 | const size_t runtime_dir_var_len = strlen(XDG_RUNTIME_DIR_VAR_NAME); 167 | 168 | size_t i = 0; 169 | while(environ[i]) { 170 | const char *current = environ[i]; 171 | if (strncmp(current, HOME_VAR_NAME, home_var_len) == 0) { 172 | strncpy((char*)env->home, current + home_var_len + 1, HOME_PATH_MAX); 173 | } 174 | if (strncmp(current, USERNAME_VAR_NAME, username_var_len) == 0) { 175 | strncpy((char*)env->user_name, current + username_var_len + 1, USER_NAME_MAX); 176 | } 177 | if (strncmp(current, XDG_CONFIG_HOME_VAR_NAME, config_home_var_len) == 0) { 178 | strncpy((char*)env->xdg_config_home, current + config_home_var_len + 1, XDG_PATH_ELEM_MAX); 179 | } 180 | if (strncmp(current, XDG_DATA_HOME_VAR_NAME, data_home_var_len) == 0) { 181 | strncpy((char*)env->xdg_data_home, current + data_home_var_len + 1, XDG_PATH_ELEM_MAX); 182 | } 183 | if (strncmp(current, XDG_CACHE_HOME_VAR_NAME, cache_home_var_len) == 0) { 184 | strncpy((char*)env->xdg_cache_home, current + cache_home_var_len + 1, XDG_PATH_ELEM_MAX); 185 | } 186 | if (strncmp(current, XDG_RUNTIME_DIR_VAR_NAME, runtime_dir_var_len) == 0) { 187 | strncpy((char*)env->xdg_runtime_dir, current + runtime_dir_var_len + 1, XDG_PATH_ELEM_MAX); 188 | } 189 | i++; 190 | } 191 | if (strlen(env->user_name) > 0 && strlen(env->home) == 0) { 192 | snprintf((char*)env->home, HOME_PATH_MAX, TOKENIZED_DATA_DIR, HOME_DIR, env->user_name); 193 | } 194 | if (strlen(env->home) > 0) { 195 | if (strlen(env->xdg_data_home) == 0) { 196 | snprintf((char*)&env->xdg_data_home, XDG_PATH_ELEM_MAX, TOKENIZED_DATA_DIR, env->home, DATA_DIR_NAME); 197 | } 198 | if (strlen(env->xdg_config_home) == 0) { 199 | snprintf((char*)&env->xdg_config_home, XDG_PATH_ELEM_MAX, TOKENIZED_CONFIG_DIR, env->home, CONFIG_DIR_NAME); 200 | } 201 | if (strlen(env->xdg_cache_home) == 0) { 202 | snprintf((char*)&env->xdg_cache_home, XDG_PATH_ELEM_MAX, TOKENIZED_CACHE_DIR, env->home, CACHE_DIR_NAME); 203 | } 204 | } 205 | } 206 | 207 | static void set_cache_file(const struct configuration *config, const char *file_name) 208 | { 209 | if (NULL == config) { return; } 210 | 211 | if (NULL == file_name) { 212 | file_name = ""; 213 | } 214 | 215 | const int wrote = snprintf((char*)config->cache_path, FILE_PATH_MAX-3, TOKENIZED_CACHE_PATH, config->env.xdg_cache_home, config->name, file_name); 216 | if (wrote == 0) { 217 | _trace2("path::error: unable build cache path"); 218 | } 219 | } 220 | 221 | static void set_cache_path(const struct configuration *config) 222 | { 223 | set_cache_file(config, CACHE_FILE_NAME); 224 | } 225 | 226 | static void set_credentials_file(const struct configuration *config, const char *file_name) 227 | { 228 | if (NULL == config) { return; } 229 | 230 | if (NULL == file_name) { 231 | file_name = ""; 232 | } 233 | 234 | const int wrote = snprintf((char*)config->credentials_path, FILE_PATH_MAX, TOKENIZED_CREDENTIALS_PATH, config->env.xdg_data_home, config->name, file_name); 235 | if (wrote == 0) { 236 | _trace2("path::error: unable build credentials path"); 237 | } 238 | } 239 | 240 | static void set_credentials_path(const struct configuration *config) 241 | { 242 | set_credentials_file(config, CREDENTIALS_FILE_NAME); 243 | } 244 | 245 | static void set_config_file(const struct configuration *config, const char *file_name) 246 | { 247 | if (NULL == config) { return; } 248 | 249 | if (NULL == file_name) { 250 | file_name = "config"; 251 | } 252 | 253 | const int wrote = snprintf((char*)config->config_path, FILE_PATH_MAX+1, TOKENIZED_CONFIG_PATH, config->env.xdg_config_home, config->name, file_name); 254 | if (wrote == 0) { 255 | _trace2("path::error: unable build config path"); 256 | } 257 | } 258 | 259 | static void set_config_path(const struct configuration *config) 260 | { 261 | set_config_file(config, CONFIG_FILE_NAME); 262 | } 263 | 264 | static bool cleanup_pid(const char *path) 265 | { 266 | if(NULL == path) { return false; } 267 | return (unlink(path) == 0); 268 | } 269 | 270 | static int load_pid_path(const struct configuration *config) 271 | { 272 | if (NULL == config) { return 0; } 273 | 274 | return snprintf((char*)config->pid_path, FILE_PATH_MAX-5, TOKENIZED_PID_PATH, config->env.xdg_runtime_dir, config->name, PID_SUFFIX); 275 | } 276 | 277 | static bool load_credentials_from_ini_group (struct ini_group *group, struct api_credentials *credentials) 278 | { 279 | if (NULL == credentials) { return false; } 280 | if (NULL == group) { return false; } 281 | #if 0 282 | _trace("api::loaded:%s", group->name); 283 | #endif 284 | 285 | if (strncmp(group->name->data, SERVICE_LABEL_LASTFM, strlen(SERVICE_LABEL_LASTFM)) == 0) { 286 | (credentials)->end_point = api_lastfm; 287 | } else if (strncmp(group->name->data, SERVICE_LABEL_LIBREFM, strlen(SERVICE_LABEL_LIBREFM)) == 0) { 288 | (credentials)->end_point = api_librefm; 289 | } else if (strncmp(group->name->data, SERVICE_LABEL_LISTENBRAINZ, strlen(SERVICE_LABEL_LISTENBRAINZ)) == 0) { 290 | (credentials)->end_point = api_listenbrainz; 291 | } 292 | assert(group->values); 293 | 294 | const size_t count = arrlen(group->values); 295 | for (size_t i = 0; i < count; i++) { 296 | const struct ini_value *setting = group->values[i]; 297 | if (NULL == setting) { continue; } 298 | 299 | const string key = setting->key; 300 | if (NULL == key) { continue; } 301 | 302 | const string value = setting->value; 303 | if (NULL == value) { continue; } 304 | 305 | if (value->len == 0) { continue; } 306 | 307 | if (strncmp(key->data, CONFIG_KEY_ENABLED, strlen(CONFIG_KEY_ENABLED)) == 0) { 308 | if (strncmp(value->data, CONFIG_VALUE_TRUE, strlen(CONFIG_VALUE_TRUE)) == 0) { 309 | (credentials)->enabled = true; 310 | } 311 | if (strncmp(value->data, CONFIG_VALUE_ONE, strlen(CONFIG_VALUE_ONE)) == 0) { 312 | (credentials)->enabled = true; 313 | } 314 | // NOTE(marius): redundant, as false should be the default if nothing is present 315 | if (strncmp(value->data, CONFIG_VALUE_FALSE, strlen(CONFIG_VALUE_FALSE)) == 0) { 316 | (credentials)->enabled = false; 317 | } 318 | if (strncmp(value->data, CONFIG_VALUE_ZERO, strlen(CONFIG_VALUE_ZERO)) == 0) { 319 | (credentials)->enabled = false; 320 | } 321 | } 322 | if (strncmp(key->data, CONFIG_KEY_USER_NAME, strlen(CONFIG_KEY_USER_NAME)) == 0) { 323 | strncpy((credentials)->user_name, value->data, value->len + 1); 324 | } 325 | if (strncmp(key->data, CONFIG_KEY_PASSWORD, strlen(CONFIG_KEY_PASSWORD)) == 0) { 326 | strncpy((credentials)->password, value->data, value->len + 1); 327 | } 328 | if (strncmp(key->data, CONFIG_KEY_TOKEN, strlen(CONFIG_KEY_TOKEN)) == 0) { 329 | strncpy((char*)(credentials)->token, value->data, value->len + 1); 330 | } 331 | if (strncmp(key->data, CONFIG_KEY_SESSION, strlen(CONFIG_KEY_SESSION)) == 0) { 332 | strncpy((char*)(credentials)->session_key, value->data, value->len + 1); 333 | } 334 | switch ((credentials)->end_point) { 335 | case api_librefm: 336 | case api_listenbrainz: 337 | if (strncmp(key->data, CONFIG_KEY_URL, strlen(CONFIG_KEY_URL)) == 0) { 338 | strncpy((char*)(credentials)->url, value->data, value->len + 1); 339 | } 340 | break; 341 | case api_lastfm: 342 | case api_unknown: 343 | default: 344 | break; 345 | } 346 | } 347 | return true; 348 | } 349 | 350 | static bool write_pid(const char *path) 351 | { 352 | if (NULL == path) { return false; } 353 | 354 | FILE *pidfile = fopen(path, "w"); 355 | if (NULL == pidfile) { 356 | _warn("main::invalid_pid_path %s", path); 357 | return false; 358 | } 359 | fprintf(pidfile, "%d", getpid()); 360 | fclose(pidfile); 361 | return true; 362 | } 363 | 364 | static void load_ini_from_file(struct ini_config *ini, const char* path) 365 | { 366 | if (NULL == ini) { return; } 367 | if (NULL == path) { return; } 368 | 369 | FILE *file = fopen(path, "r"); 370 | if (NULL == file) { return; } 371 | 372 | 373 | fseek(file, 0L, SEEK_END); 374 | const long file_size = ftell(file); 375 | 376 | if (file_size <= 0) { goto _exit; } 377 | rewind (file); 378 | 379 | char *buffer = get_zero_string((size_t)file_size); 380 | if (NULL == buffer) { goto _exit; } 381 | if (1 != fread(buffer, (size_t)file_size, 1, file)) { 382 | _warn("config::error: unable to read file %s", path); 383 | goto _failure; 384 | } 385 | 386 | if (ini_parse(buffer, (size_t)file_size, ini) < 0) { 387 | _error("config::error: failed to parse file %s", path); 388 | } 389 | 390 | _failure: 391 | string_free(buffer); 392 | _exit: 393 | fclose(file); 394 | } 395 | 396 | static bool load_config_from_file(struct configuration *config, const char* path) 397 | { 398 | if (NULL == config) { return false; } 399 | if (NULL == path) { return false; } 400 | config->ignore_players_count = 0; 401 | 402 | struct ini_config ini = {0}; 403 | load_ini_from_file(&ini, path); 404 | const size_t group_count = arrlen(ini.groups); 405 | for (size_t i = 0; i < group_count; i++) { 406 | const struct ini_group *group = ini.groups[i]; 407 | if (NULL == group->name) { continue; } 408 | if (strncmp(group->name->data, DEFAULT_GROUP_NAME, group->name->len) != 0) { 409 | break; 410 | } 411 | const size_t value_count = arrlen(group->values); 412 | for (size_t j = 0; j < value_count; j++) { 413 | const struct ini_value *val = group->values[j]; 414 | if (strncmp(val->key->data, CONFIG_KEY_IGNORE, val->key->len) != 0) { 415 | break; 416 | } 417 | const short cnt = config->ignore_players_count; 418 | _trace("config::ignore_player[%d]: %s", cnt, val->value->data); 419 | memcpy((char*)config->ignore_players[cnt], val->value->data, val->value->len); 420 | config->ignore_players_count++; 421 | } 422 | } 423 | ini_config_clean(&ini); 424 | return true; 425 | } 426 | 427 | static bool load_credentials_from_file(struct configuration *config, const char* path) 428 | { 429 | if (NULL == config) { return false; } 430 | if (NULL == path) { return false; } 431 | 432 | struct ini_config ini = {0}; 433 | load_ini_from_file(&ini, path); 434 | const size_t count = arrlen(ini.groups); 435 | assert(count < MAX_CREDENTIALS); 436 | 437 | for (size_t i = 0; i < count; i++) { 438 | struct ini_group *group = ini.groups[i]; 439 | struct api_credentials *creds = &config->credentials[i]; 440 | 441 | if (!load_credentials_from_ini_group(group, creds)) { 442 | _warn("ini::invalid_config[%s]: not loading values", group->name->data); 443 | memset(creds, 0x0, sizeof(struct api_credentials)); 444 | } else { 445 | config->credentials_count++; 446 | } 447 | } 448 | ini_config_clean(&ini); 449 | return true; 450 | } 451 | 452 | static void load_config (struct configuration *config) 453 | { 454 | set_config_path(config); 455 | if (load_config_from_file(config, config->config_path)) { 456 | _debug("main::loading_config: ok"); 457 | } else { 458 | _warn("main::loading_config: failed"); 459 | } 460 | } 461 | 462 | static void load_credentials (struct configuration *config) 463 | { 464 | set_credentials_path(config); 465 | 466 | memset(config->credentials, 0x0, sizeof(config->credentials)); 467 | config->credentials_count = 0; 468 | 469 | // Load 470 | if (load_credentials_from_file(config, config->credentials_path)) { 471 | _debug("main::loading_credentials: ok"); 472 | } else { 473 | _warn("main::loading_credentials: failed"); 474 | } 475 | 476 | // load 477 | const size_t count = config->credentials_count; 478 | for(size_t i = 0; i < count; i++) { 479 | struct api_credentials *cur = &config->credentials[i]; 480 | const char *api_key = api_get_application_key(cur->end_point); 481 | memcpy(cur->api_key, api_key, min(MAX_SECRET_LENGTH, strlen(api_key))); 482 | const char *api_secret = api_get_application_secret(cur->end_point); 483 | memcpy(cur->secret, api_secret, min(MAX_SECRET_LENGTH, strlen(api_secret))); 484 | if (strlen(cur->api_key) == 0) { 485 | _warn("scrobbler::invalid_service[%s]: missing API key", get_api_type_label(cur->end_point)); 486 | cur->enabled = false; 487 | } 488 | if (strlen(cur->secret) == 0) { 489 | _warn("scrobbler::invalid_service[%s]: missing API secret key", get_api_type_label(cur->end_point)); 490 | cur->enabled = false; 491 | } 492 | } 493 | } 494 | 495 | static void configuration_clean(struct configuration *config) 496 | { 497 | if (NULL == config) { return; } 498 | const size_t count = config->credentials_count; 499 | _trace2("mem::free::configuration(%u)", count); 500 | memset(&config->credentials, 0x0, sizeof(config->credentials)); 501 | if (config->wrote_pid) { 502 | _trace("main::cleanup_pid: %s", config->pid_path); 503 | cleanup_pid(config->pid_path); 504 | } 505 | } 506 | 507 | static void print_application_config(const struct configuration *config) 508 | { 509 | printf("app::name %s\n", config->name); 510 | printf("app::user %s\n", config->env.user_name); 511 | printf("app::home_folder %s\n", config->env.home); 512 | printf("app::config_folder %s\n", config->env.xdg_config_home); 513 | printf("app::data_folder %s\n", config->env.xdg_data_home); 514 | printf("app::cache_folder %s\n", config->env.xdg_cache_home); 515 | printf("app::runtime_dir %s\n", config->env.xdg_runtime_dir); 516 | 517 | const size_t credentials_count = config->credentials_count; 518 | printf("app::loaded_credentials_count %zu\n", (size_t)credentials_count); 519 | 520 | if (credentials_count == 0) { return; } 521 | for (size_t i = 0 ; i < credentials_count; i++) { 522 | const struct api_credentials *cur = &config->credentials[i]; 523 | printf("app::credentials[%zu]:%s\n", (size_t)i, get_api_type_label(cur->end_point)); 524 | printf("\tenabled = %s\n", cur->enabled ? "true" : "false" ); 525 | printf("\tauthenticated = %s\n", cur->authenticated ? "true" : "false"); 526 | if (strlen(cur->url) > 0) { 527 | printf("\turl = %s\n", cur->url); 528 | } 529 | if (strlen(cur->user_name) > 0) { 530 | printf("\tusername = %s\n", cur->user_name); 531 | } 532 | if (strlen(cur->password) > 0) { 533 | printf("\tpassword = %s\n", cur->password); 534 | } 535 | if (strlen(cur->token) > 0) { 536 | printf("\ttoken = %s\n", cur->token); 537 | } 538 | if (strlen(cur->session_key)) { 539 | printf("\tsession_key = %s\n", cur->session_key); 540 | } 541 | } 542 | } 543 | 544 | bool load_configuration(struct configuration *config, const char *name) 545 | { 546 | if (NULL == config) { return false; } 547 | if (NULL != name) { 548 | memcpy((char*)config->name, name, min(USER_NAME_MAX, strlen(name))); 549 | } 550 | 551 | if (!config->env_loaded) { 552 | load_environment(&config->env); 553 | config->env_loaded = true; 554 | } 555 | 556 | set_config_path(config); 557 | set_credentials_path(config); 558 | set_cache_path(config); 559 | 560 | load_config(config); 561 | 562 | load_credentials(config); 563 | 564 | return true; 565 | } 566 | 567 | bool configuration_folder_exists(const char *path) 568 | { 569 | if (NULL == path) { return false; } 570 | 571 | struct stat st; 572 | return (stat(path, &st) == 0 && S_ISDIR(st.st_mode)); 573 | } 574 | 575 | bool configuration_folder_create(const char *path) 576 | { 577 | const char *err_msg = NULL; 578 | 579 | bool status = (mkdir(path, S_IRWXU) == 0); 580 | if (!status) { 581 | switch(errno) { 582 | case EACCES: 583 | err_msg = "permission denied when writing folder"; 584 | break; 585 | case EEXIST: 586 | err_msg = "the folder already exists"; 587 | status = true; 588 | break; 589 | case ELOOP: 590 | err_msg = "unable to resolve folder path"; 591 | break; 592 | case EMLINK: 593 | err_msg = "link count exceeded"; 594 | break; 595 | case ENAMETOOLONG: 596 | err_msg = "path length is too long"; 597 | break; 598 | case ENOENT: 599 | err_msg = "unable to resolve folder path, parent is missing"; 600 | break; 601 | case ENOSPC: 602 | err_msg = "not enough space to create folder"; 603 | break; 604 | case ENOTDIR: 605 | err_msg = "parent is not a folder"; 606 | break; 607 | case EROFS: 608 | err_msg = "parent file-system is read-only"; 609 | break; 610 | default: 611 | err_msg = "unknown error"; 612 | } 613 | _trace("credentials::folder_create_error: %s", err_msg); 614 | } 615 | return status; 616 | } 617 | 618 | static int write_credentials_file(struct configuration *config) { 619 | int status = -1; 620 | struct ini_config *to_write = NULL; 621 | 622 | char file_path[FILE_PATH_MAX+1] = {0}; 623 | strncpy(file_path, config->credentials_path, FILE_PATH_MAX+1); 624 | char *folder_path = dirname(file_path); 625 | if (!configuration_folder_exists(folder_path) && !configuration_folder_create(folder_path)) { 626 | _error("main::credentials: unable to create data folder %s", folder_path); 627 | goto _return; 628 | } 629 | 630 | const size_t count = config->credentials_count; 631 | to_write = get_ini_from_credentials(config->credentials, count); 632 | #if 0 633 | print_ini(to_write); 634 | #endif 635 | 636 | _debug("saving::credentials[%u]: %s", count, config->credentials_path); 637 | FILE *file = fopen(config->credentials_path, "w+"); 638 | if (NULL == file) { 639 | _warn("saving::credentials:failed: %s", config->credentials_path); 640 | goto _return; 641 | } 642 | status = write_ini_file(to_write, file); 643 | fclose(file); 644 | 645 | _return: 646 | if (NULL != to_write) { ini_config_free(to_write); } 647 | 648 | return status; 649 | } 650 | 651 | #endif // MPRIS_SCROBBLER_CONFIGURATION_H 652 | --------------------------------------------------------------------------------