├── .github └── ISSUE_TEMPLATE │ └── bug_report.md ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── docs └── command_state_machine.gv ├── lsan_suppr.txt ├── run_debug └── src ├── cmd.c ├── cmd.leg ├── config.c ├── config.h ├── disk.c ├── disk.h ├── json.c ├── json.h ├── linenoise.c ├── linenoise.h ├── main.c ├── menu.c ├── menu.h ├── mpv.c ├── mpv.h ├── net.c ├── net.h ├── playback.c ├── playback.h ├── shared.c └── shared.h /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report something not working as it should 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **jftui version** 11 | Please include the output of `jftui --version`. 12 | 13 | **Describe the bug** 14 | Please include a clear and concise description of what the bug is. 15 | 16 | **To Reproduce** 17 | Please explain how you reached the point where the bug manifested. 18 | 19 | **Expected behavior** 20 | Please include a clear and concise description of what you expected to happen. 21 | 22 | **SIGSEGV backtrace** 23 | If you experienced a Segmentation Fault and a core dump was generated, please include a copy of the backtrace. 24 | This [Arch Wiki article](https://wiki.archlinux.org/index.php/Core_dump#Examining_a_core_dump) applies to all systems using `coredumpctl`. Otherwise, you may refer to your distribution's documentation to locate the core dump or execute jftui directly through `gdb` and replicate the bug. 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | runtime 3 | docs/*.png 4 | .vscode -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | OFLAGS=-O2 -march=native 2 | WFLAGS=-Wall -Wpedantic -Wextra -Wconversion -Wstrict-prototypes -Werror=implicit-function-declaration -Werror=implicit-int -Werror=incompatible-pointer-types -Werror=int-conversion 3 | CFLAGS=`pkg-config --cflags libcurl yajl mpv` 4 | LFLAGS=`pkg-config --libs libcurl yajl mpv` -pthread 5 | DFLAGS=-g -O1 -fno-omit-frame-pointer -fno-optimize-sibling-calls -fsanitize=address -fsanitize=undefined -DJF_DEBUG 6 | 7 | SOURCES=src/linenoise.c src/shared.c src/config.c src/disk.c src/json.c src/menu.c src/playback.c src/net.c src/mpv.c src/main.c 8 | 9 | OBJECTS=build/linenoise.o build/menu.o build/shared.o build/config.o build/disk.o build/json.o build/net.o build/playback.o build/mpv.o build/main.o 10 | 11 | DOCS=docs/command_state_machine.png 12 | 13 | BUILD_DIR := build 14 | 15 | .PHONY: all debug install uninstall clean docs 16 | 17 | 18 | 19 | 20 | all: ${BUILD_DIR}/jftui 21 | 22 | debug: ${BUILD_DIR}/jftui_debug 23 | 24 | install: all 25 | install -Dm555 ${BUILD_DIR}/jftui $(DESTDIR)/usr/bin/jftui 26 | 27 | uninstall: 28 | rm $(DESTDIR)/usr/bin/jftui 29 | 30 | clean: 31 | rm -rf ${BUILD_DIR} runtime 32 | 33 | docs: $(DOCS) 34 | 35 | 36 | 37 | ${BUILD_DIR}: 38 | mkdir -p ${BUILD_DIR} 39 | 40 | ${BUILD_DIR}/jftui: ${BUILD_DIR} $(SOURCES) 41 | $(CC) $(CFLAGS) $(OFLAGS) $(SOURCES) $(LFLAGS) -g -o $@ 42 | 43 | ${BUILD_DIR}/jftui_debug: ${BUILD_DIR} $(OBJECTS) $(SOURCES) 44 | $(CC) $(WFLAGS) $(DFLAGS) $(OBJECTS) $(LFLAGS) -o $@ 45 | 46 | src/cmd.c: src/cmd.leg 47 | leg -o $@ $^ 48 | 49 | ${BUILD_DIR}/linenoise.o: src/linenoise.c 50 | $(CC) $(WFLAGS) $(CFLAGS) $(DFLAGS) -c -o $@ $^ 51 | 52 | ${BUILD_DIR}/menu.o: src/menu.c src/cmd.c 53 | $(CC) $(WFLAGS) $(CFLAGS) $(DFLAGS) -c -o $@ src/menu.c 54 | 55 | ${BUILD_DIR}/shared.o: src/shared.c 56 | $(CC) $(WFLAGS) $(CFLAGS) $(DFLAGS) -c -o $@ $^ 57 | 58 | ${BUILD_DIR}/config.o: src/config.c 59 | $(CC) $(WFLAGS) $(CFLAGS) $(DFLAGS) -c -o $@ $^ 60 | 61 | ${BUILD_DIR}/disk.o: src/disk.c 62 | $(CC) $(WFLAGS) $(CFLAGS) $(DFLAGS) -c -o $@ $^ 63 | 64 | ${BUILD_DIR}/json.o: src/json.c 65 | $(CC) $(WFLAGS) $(CFLAGS) $(DFLAGS) -c -o $@ $^ 66 | 67 | ${BUILD_DIR}/net.o: src/net.c 68 | $(CC) $(WFLAGS) $(CFLAGS) $(DFLAGS) -c -o $@ $^ 69 | 70 | ${BUILD_DIR}/playback.o: src/playback.c 71 | $(CC) $(WFLAGS) $(CFLAGS) $(DFLAGS) -c -o $@ $^ 72 | 73 | ${BUILD_DIR}/mpv.o: src/mpv.c 74 | $(CC) $(WFLAGS) $(CFLAGS) $(DFLAGS) -c -o $@ $^ 75 | 76 | ${BUILD_DIR}/main.o: src/main.c 77 | $(CC) $(WFLAGS) $(CFLAGS) $(DFLAGS) -c -o $@ $^ 78 | 79 | 80 | docs/command_state_machine.png: docs/command_state_machine.gv 81 | neato -T png $^ > $@ 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | jftui is a minimalistic, lightweight C99 command line client for the open source [Jellyfin](http://jellyfin.org/) media server. 2 | 3 | It is developed for the GNU/Linux OS only, although it may be possible to make it run on BSD's. 4 | 5 | # Installation 6 | The program must be built from source. 7 | 8 | For Arch Linux users, there is an AUR [package](https://aur.archlinux.org/packages/jftui/). 9 | 10 | ## Dependencies 11 | - [libcurl](https://curl.haxx.se/libcurl/) (runtime) 12 | - [libmpv](https://mpv.io) >= 1.24 (runtime) 13 | - [YAJL](https://lloyd.github.io/yajl/) >= 2.0 (runtime) 14 | - [PEG](http://piumarta.com/software/peg/) (development only) 15 | 16 | 17 | ## Building 18 | Make sure to checkout a release as the master branch is not guaranteed to work correctly or indeed compile at any time. 19 | 20 | Then, simply run 21 | ``` 22 | make && sudo make install 23 | ``` 24 | 25 | # Usage 26 | Run `jftui`. You will be prompted for a minimal interactive configuration on first run. 27 | 28 | **BEWARE**: jftui fetches `https://github.com/Aanok/jftui/releases/latest` on startup to check for newer versions. You can avoid this by passing the `--no-check-updates` argument. There is also a [settings file](https://github.com/Aanok/jftui/wiki/Settings) entry. 29 | 30 | The interface should be intuitive enough: select one or more entries by entering the corresponding index number. See below for a full description of the command syntax. 31 | 32 | jftui will drop into a command line instance of mpv when starting playback. It will use `mpv.conf` and `input.conf` files found in `$XDG_CONFIG_HOME/jftui` (this location can be overridden with the `--config-dir` argument). It will also try and load scripts found in the same folder, but no guarantees are made about them actually working correctly. 33 | 34 | It is recommended to consult the [wiki page](https://github.com/Aanok/jftui/wiki/mpv-commands) on configuring mpv commands to use jftui: a few special ones are required in particular to manipulate the playback playlist. 35 | 36 | ## Jftui commands 37 | The grammar defining jftui commands is as follows: 38 | ``` 39 | S ::= "q" (quits) 40 | | ( "help" | "?" ) (print a help menu) 41 | | "h" (go to "home" root menu) 42 | | ".." (go to previous menu) 43 | | "f" ( "c" | [pufrld]+ ) (filters: clear or played, unplayed, favorite, resumable, liked, disliked) 44 | | "m" ("p" | "u") Selector (marks items played or unplayed) 45 | | "m" ("f" | "uf") Selector (marks items favorite or unfavorite) 46 | | Selector (opens a single directory entry or sends a sequence of items to playback) 47 | Selector :: = '*' (everything in the current menu) 48 | | Items 49 | Items ::= Atom "," Items (list) 50 | | Atom 51 | Atom ::= n1 "-" n2 (range) 52 | | n (single item) 53 | ``` 54 | 55 | Whitespace may be scattered between tokens at will. Inexisting items are silently ignored. Both `quit` and `stop` mpv commands will drop you back to menu navigation. 56 | 57 | There is one further command that will be parsed, but it is left undocumented because its implementation is barely more than a stub. Caveat. 58 | 59 | 60 | # Plans and TODO 61 | - Search; 62 | - Explicit command to recursively navigate folders to send items to playback; 63 | - Transcoding. 64 | -------------------------------------------------------------------------------- /docs/command_state_machine.gv: -------------------------------------------------------------------------------- 1 | digraph "Command Parser State Machine" { 2 | pad=0.5 3 | overlap=false 4 | sep=10 5 | splines=curved 6 | 7 | // filters 8 | VALIDATE_START -> VALIDATE_FILTERS [label="\"f\""] 9 | VALIDATE_FILTERS -> VALIDATE_FILTERS [label="filter"] 10 | VALIDATE_FILTERS -> VALIDATE_OK [label="EOF"] 11 | VALIDATE_FILTERS -> FAIL_SYNTAX [label="match error"] 12 | 13 | // folder 14 | VALIDATE_START -> VALIDATE_FOLDER [label="folder"] 15 | VALIDATE_FOLDER -> FAIL_FOLDER [label="folder"] 16 | VALIDATE_FOLDER -> FAIL_FOLDER [label="atom"] 17 | VALIDATE_FOLDER -> VALIDATE_OK [label="EOF"] 18 | VALIDATE_FOLDER -> FAIL_SYNTAX [label="match error"] 19 | 20 | // atoms 21 | VALIDATE_START -> VALIDATE_ATOMS [label="atom"] 22 | VALIDATE_ATOMS -> VALIDATE_ATOMS [label="atom"] 23 | VALIDATE_ATOMS -> FAIL_FOLDER [label="folder"] 24 | VALIDATE_ATOMS -> VALIDATE_OK [label="EOF"] 25 | VALIDATE_ATOMS -> FAIL_SYNTAX [label="match error"] 26 | 27 | // dispatch 28 | VALIDATE_OK -> VALIDATE_OK [label="filter"] 29 | VALIDATE_OK -> VALIDATE_OK [label="folder"] 30 | VALIDATE_OK -> VALIDATE_OK [label="atom"] 31 | VALIDATE_OK -> FAIL_DISPATCH [label="dispatch error"] 32 | VALIDATE_OK -> OK [label="EOF"] 33 | 34 | // misc 35 | VALIDATE_START -> FAIL_SYNTAX [label="match error"] 36 | } 37 | -------------------------------------------------------------------------------- /lsan_suppr.txt: -------------------------------------------------------------------------------- 1 | leak:pa_xmalloc0 2 | -------------------------------------------------------------------------------- /run_debug: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | LSAN_OPTIONS=suppressions=lsan_suppr.txt build/jftui_debug "$@" 3 | -------------------------------------------------------------------------------- /src/cmd.leg: -------------------------------------------------------------------------------- 1 | %{ 2 | #define YYSTYPE unsigned long 3 | #include 4 | #include 5 | #include 6 | 7 | #include "shared.h" 8 | #include "menu.h" 9 | 10 | ////////// STATE MACHINE ////////// 11 | typedef enum jf_cmd_parser_state { 12 | // make sure to start from 0 so memset init works 13 | JF_CMD_VALIDATE_START = 0, 14 | JF_CMD_VALIDATE_ATOMS = 1, 15 | JF_CMD_VALIDATE_FOLDER = 2, 16 | JF_CMD_VALIDATE_FILTERS = 3, 17 | JF_CMD_VALIDATE_OK = 4, 18 | JF_CMD_SPECIAL = 5, 19 | JF_CMD_MARK_PLAYED = 6, 20 | JF_CMD_MARK_UNPLAYED = 7, 21 | JF_CMD_MARK_FAVORITE = 8, 22 | JF_CMD_MARK_UNFAVORITE = 9, 23 | JF_CMD_SUCCESS = 10, 24 | 25 | JF_CMD_FAIL_FOLDER = -1, 26 | JF_CMD_FAIL_SYNTAX = -2, 27 | JF_CMD_FAIL_DISPATCH = -3, 28 | JF_CMD_FAIL_SPECIAL = -4 29 | } jf_cmd_parser_state; 30 | 31 | #define JF_CMD_IS_FAIL(state) ((state) < 0) 32 | /////////////////////////////////// 33 | 34 | 35 | ////////// YY_CTX ////////// 36 | // forward declaration wrt PEG generated code 37 | typedef struct _yycontext yycontext; 38 | 39 | #define YY_CTX_LOCAL 40 | #define YY_CTX_MEMBERS \ 41 | jf_cmd_parser_state state; \ 42 | char *input; \ 43 | size_t read_input; 44 | //////////////////////////// 45 | 46 | 47 | ////////// INPUT PROCESSING ////////// 48 | #define YY_INPUT(ctx, buf, result, max_size) \ 49 | { \ 50 | size_t to_read = 0; \ 51 | while (to_read < max_size) { \ 52 | if (ctx->input[ctx->read_input + to_read] == '\0') { \ 53 | break; \ 54 | } \ 55 | to_read++; \ 56 | } \ 57 | memcpy(buf, ctx->input + ctx->read_input, to_read); \ 58 | ctx->read_input += to_read; \ 59 | result = to_read; \ 60 | } 61 | ////////////////////////////////////// 62 | 63 | 64 | ////////// FUNCTION PROTOTYPES ////////// 65 | jf_cmd_parser_state yy_cmd_get_parser_state(const yycontext *ctx); 66 | 67 | static void yy_cmd_filters_start(yycontext *ctx); 68 | static void yy_cmd_digest_filter(yycontext *ctx, const enum jf_filter filter); 69 | static void yy_cmd_digest(yycontext *ctx, const size_t n); 70 | static void yy_cmd_digest_range(yycontext *ctx, size_t l, size_t r); 71 | static void yy_cmd_finalize(yycontext *ctx, const bool parse_ok); 72 | ///////////////////////////////////////// 73 | %} 74 | 75 | 76 | ###################################################################################### 77 | 78 | Start = ( ws* 79 | ( ".." { yy->state = JF_CMD_SPECIAL; jf_menu_dotdot(); } 80 | | ("help" | "?" ) { yy->state = JF_CMD_SPECIAL; jf_menu_help(); } 81 | | "h" { yy->state = JF_CMD_SPECIAL; jf_menu_clear(); } 82 | # | "r" ws+ { yy->state = JF_CMD_RECURSIVE; } 83 | # Selector ws* 84 | | "s" ws+ < .+ > { yy->state = JF_CMD_SPECIAL; jf_menu_search(yytext); } 85 | | "q" { yy->state = JF_CMD_SPECIAL; jf_menu_quit(); } 86 | | "f" { yy_cmd_filters_start(yy); } 87 | ws+ ( "c" { yy_cmd_digest_filter(yy, JF_FILTER_NONE); } 88 | | Filters ) 89 | | "m" ws+ ("f" { yy->state = JF_CMD_MARK_FAVORITE; } 90 | | "uf" { yy->state = JF_CMD_MARK_UNFAVORITE; } 91 | | "p" { yy->state = JF_CMD_MARK_PLAYED; } 92 | | "u" { yy->state = JF_CMD_MARK_UNPLAYED; } 93 | ) ws+ Selector 94 | | Selector 95 | ) ws* !.) ~ { yy_cmd_finalize(yy, false); } { yy_cmd_finalize(yy, true); } 96 | 97 | Filters = Filter ws* Filters 98 | | Filter 99 | 100 | Filter = "p" { yy_cmd_digest_filter(yy, JF_FILTER_IS_PLAYED); } 101 | | "u" { yy_cmd_digest_filter(yy, JF_FILTER_IS_UNPLAYED); } 102 | | "r" { yy_cmd_digest_filter(yy, JF_FILTER_RESUMABLE); } 103 | | "f" { yy_cmd_digest_filter(yy, JF_FILTER_FAVORITE); } 104 | | "l" { yy_cmd_digest_filter(yy, JF_FILTER_LIKES); } 105 | | "d" { yy_cmd_digest_filter(yy, JF_FILTER_DISLIKES); } 106 | 107 | Selector = "*" { yy_cmd_digest_range(yy, 1, jf_menu_child_count()); } 108 | | Items 109 | 110 | Items = Atom ws* "," ws* Items 111 | | Atom 112 | 113 | Atom = l:num ws* "-" ws* r:num { yy_cmd_digest_range(yy, l, r); } 114 | | n:num { yy_cmd_digest(yy, n); } 115 | 116 | num = < [0-9]+ > { $$ = strtoul(yytext, NULL, 10); } 117 | 118 | ws = [ \t] 119 | 120 | ###################################################################################### 121 | 122 | 123 | %% 124 | jf_cmd_parser_state yy_cmd_get_parser_state(const yycontext *ctx) 125 | { 126 | return ctx->state; 127 | } 128 | 129 | 130 | static void yy_cmd_filters_start(yycontext *ctx) 131 | { 132 | switch (ctx->state) { 133 | case JF_CMD_VALIDATE_START: 134 | ctx->state = JF_CMD_VALIDATE_FILTERS; 135 | break; 136 | case JF_CMD_VALIDATE_OK: 137 | ctx->state = JF_CMD_SPECIAL; 138 | jf_menu_filters_clear(); 139 | break; 140 | default: 141 | fprintf(stderr, "Error: yy_cmd_filters_start: unexpected state transition. This is a bug.\n"); 142 | break; 143 | } 144 | } 145 | 146 | 147 | static void yy_cmd_digest_filter(yycontext *ctx, const enum jf_filter filter) 148 | { 149 | if (ctx->state != JF_CMD_SPECIAL || filter == JF_FILTER_NONE) return; 150 | 151 | if (jf_menu_filters_add(filter) == false) { 152 | ctx->state = JF_CMD_FAIL_SPECIAL; 153 | } 154 | } 155 | 156 | 157 | static void yy_cmd_digest(yycontext *ctx, size_t n) 158 | { 159 | jf_item_type item_type; 160 | 161 | // no-op on fail state 162 | if (JF_CMD_IS_FAIL(ctx->state)) { 163 | return; 164 | } 165 | 166 | // no-op if item does not exist (out of bounds) 167 | if ((item_type = jf_menu_child_get_type(n)) == JF_ITEM_TYPE_NONE) { 168 | return; 169 | } 170 | 171 | switch (ctx->state) { 172 | case JF_CMD_VALIDATE_START: 173 | if (JF_ITEM_TYPE_IS_FOLDER(item_type)) { 174 | ctx->state = JF_CMD_VALIDATE_FOLDER; 175 | } else { 176 | ctx->state = JF_CMD_VALIDATE_ATOMS; 177 | } 178 | break; 179 | case JF_CMD_VALIDATE_ATOMS: 180 | if (JF_ITEM_TYPE_IS_FOLDER(item_type)) { 181 | ctx->state = JF_CMD_FAIL_FOLDER; 182 | } 183 | break; 184 | case JF_CMD_VALIDATE_FOLDER: 185 | ctx->state = JF_CMD_FAIL_FOLDER; 186 | break; 187 | case JF_CMD_VALIDATE_OK: 188 | if (! jf_menu_child_dispatch(n)) { 189 | ctx->state = JF_CMD_FAIL_DISPATCH; 190 | } 191 | break; 192 | case JF_CMD_MARK_PLAYED: 193 | jf_menu_child_set_flag(n, JF_FLAG_TYPE_PLAYED, true); 194 | break; 195 | case JF_CMD_MARK_UNPLAYED: 196 | jf_menu_child_set_flag(n, JF_FLAG_TYPE_PLAYED, false); 197 | break; 198 | case JF_CMD_MARK_FAVORITE: 199 | jf_menu_child_set_flag(n, JF_FLAG_TYPE_FAVORITE, true); 200 | break; 201 | case JF_CMD_MARK_UNFAVORITE: 202 | jf_menu_child_set_flag(n, JF_FLAG_TYPE_FAVORITE, false); 203 | break; 204 | default: 205 | fprintf(stderr, "Error: yy_cmd_digest: unexpected state transition. This is a bug.\n"); 206 | break; 207 | } 208 | } 209 | 210 | 211 | static void yy_cmd_digest_range(yycontext *ctx, size_t l, size_t r) 212 | { 213 | // and now for our next trick: unsigned arithmetic! 214 | size_t step = l <= r ? 1 : (size_t)-1; 215 | l = jf_clamp_zu(l, 0, jf_menu_child_count()+1); 216 | r = jf_clamp_zu(r, 0, jf_menu_child_count()+1); 217 | while (true) { 218 | yy_cmd_digest(ctx, l); 219 | if (l == r) break; 220 | l += step; 221 | } 222 | } 223 | 224 | 225 | static void yy_cmd_finalize(yycontext *ctx, const bool parse_ok) 226 | { 227 | if (parse_ok == false) { 228 | ctx->state = JF_CMD_FAIL_SYNTAX; 229 | } else { 230 | switch (ctx->state) { 231 | case JF_CMD_VALIDATE_ATOMS: 232 | case JF_CMD_VALIDATE_FOLDER: 233 | case JF_CMD_VALIDATE_FILTERS: 234 | ctx->read_input = 0; 235 | ctx->state = JF_CMD_VALIDATE_OK; 236 | break; 237 | case JF_CMD_MARK_PLAYED: 238 | case JF_CMD_MARK_UNPLAYED: 239 | case JF_CMD_MARK_FAVORITE: 240 | case JF_CMD_MARK_UNFAVORITE: 241 | jf_menu_item_set_flag_await_all(); 242 | // no break 243 | case JF_CMD_VALIDATE_OK: 244 | case JF_CMD_SPECIAL: 245 | case JF_CMD_VALIDATE_START: // all items out of bounds 246 | ctx->state = JF_CMD_SUCCESS; 247 | break; 248 | case JF_CMD_FAIL_FOLDER: 249 | case JF_CMD_FAIL_SPECIAL: 250 | break; 251 | default: 252 | fprintf(stderr, "Error: yy_cmd_finalize: unexpected state transition. This is a bug.\n"); 253 | } 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/config.c: -------------------------------------------------------------------------------- 1 | #include "config.h" 2 | #include "shared.h" 3 | #include "net.h" 4 | #include "menu.h" 5 | #include "json.h" 6 | #include "disk.h" 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | ////////// GLOBAL VARIABLES ////////// 18 | extern jf_options g_options; 19 | extern jf_global_state g_state; 20 | ////////////////////////////////////// 21 | 22 | 23 | ////////// STATIC FUNCTIONS ////////// 24 | // Will fill in fields client, device, deviceid and version of the global 25 | // options struct, unless they're already filled in. 26 | static void jf_options_complete_with_defaults(void); 27 | ////////////////////////////////////// 28 | 29 | 30 | ////////// JF_OPTIONS ////////// 31 | static void jf_options_complete_with_defaults(void) 32 | { 33 | if (g_options.client == NULL) { 34 | assert((g_options.client = strdup(JF_CONFIG_CLIENT_DEFAULT)) != NULL); 35 | } 36 | if (g_options.device[0] == '\0') { 37 | if (gethostname(g_options.device, JF_CONFIG_DEVICE_SIZE) != 0) { 38 | strcpy(g_options.device, JF_CONFIG_DEVICE_DEFAULT); 39 | } 40 | g_options.device[JF_CONFIG_DEVICE_SIZE - 1] = '\0'; 41 | } 42 | if (g_options.deviceid[0] == '\0') { 43 | char *tmp = jf_generate_random_id(JF_CONFIG_DEVICEID_SIZE - 1); 44 | strcpy(g_options.deviceid, tmp); 45 | g_options.deviceid[JF_CONFIG_DEVICEID_SIZE - 1] = '\0'; 46 | free(tmp); 47 | } 48 | if (g_options.version == NULL) { 49 | assert((g_options.version = strdup(JF_CONFIG_VERSION_DEFAULT)) != NULL); 50 | } 51 | } 52 | 53 | 54 | void jf_options_init(void) 55 | { 56 | g_options = (jf_options){ 0 }; 57 | // these two must not be overwritten when calling _defaults() again 58 | // during config file parsing 59 | g_options.ssl_verifyhost = JF_CONFIG_SSL_VERIFYHOST_DEFAULT; 60 | g_options.check_updates = JF_CONFIG_CHECK_UPDATES_DEFAULT; 61 | jf_options_complete_with_defaults(); 62 | } 63 | 64 | 65 | void jf_options_clear(void) 66 | { 67 | free(g_options.server); 68 | free(g_options.token); 69 | free(g_options.userid); 70 | free(g_options.client); 71 | free(g_options.version); 72 | free(g_options.mpv_profile); 73 | } 74 | //////////////////////////////// 75 | 76 | 77 | ////////// CONFIGURATION FILE ////////// 78 | // NB return value will need to be free'd 79 | // returns NULL if $HOME not set 80 | char *jf_config_get_default_dir(void) 81 | { 82 | char *dir; 83 | if ((dir = getenv("XDG_CONFIG_HOME")) == NULL) { 84 | if ((dir = getenv("HOME")) != NULL) { 85 | dir = jf_concat(2, getenv("HOME"), "/.config/jftui"); 86 | } 87 | } else { 88 | dir = jf_concat(2, dir, "/jftui"); 89 | } 90 | return dir; 91 | } 92 | 93 | 94 | // TODO: allow whitespace 95 | void jf_config_read(const char *config_path) 96 | { 97 | FILE *config_file; 98 | char *line; 99 | size_t line_size = 1024; 100 | char *value; 101 | size_t value_len; 102 | 103 | assert(config_path != NULL); 104 | 105 | assert((line = malloc(line_size)) != NULL); 106 | 107 | assert((config_file = fopen(config_path, "r")) != NULL); 108 | 109 | // read from file 110 | while (getline(&line, &line_size, config_file) != -1) { 111 | assert(line != NULL); 112 | if ((value = strchr(line, '=')) == NULL) { 113 | // the line is malformed; issue a warning and skip it 114 | fprintf(stderr, 115 | "Warning: skipping malformed settings file line: %s", 116 | line); 117 | continue; 118 | } 119 | value += 1; // digest '=' 120 | // figure out which option key it is 121 | // NB options that start with a prefix of other options must go after those! 122 | if (JF_CONFIG_KEY_IS("server")) { 123 | JF_CONFIG_FILL_VALUE(server); 124 | g_options.server_len = value_len; 125 | } else if (JF_CONFIG_KEY_IS("token")) { 126 | JF_CONFIG_FILL_VALUE(token); 127 | } else if (JF_CONFIG_KEY_IS("userid")) { 128 | JF_CONFIG_FILL_VALUE(userid); 129 | } else if (JF_CONFIG_KEY_IS("ssl_verifyhost")) { 130 | JF_CONFIG_FILL_VALUE_BOOL(ssl_verifyhost); 131 | } else if (JF_CONFIG_KEY_IS("client")) { 132 | JF_CONFIG_FILL_VALUE(client); 133 | } else if (JF_CONFIG_KEY_IS("deviceid")) { 134 | JF_CONFIG_FILL_VALUE_ARRAY(deviceid, JF_CONFIG_DEVICEID_SIZE); 135 | } else if (JF_CONFIG_KEY_IS("device")) { 136 | JF_CONFIG_FILL_VALUE_ARRAY(device, JF_CONFIG_DEVICE_SIZE); 137 | } else if (JF_CONFIG_KEY_IS("version")) { 138 | JF_CONFIG_FILL_VALUE(version); 139 | } else if (JF_CONFIG_KEY_IS("mpv_profile")) { 140 | JF_CONFIG_FILL_VALUE(mpv_profile); 141 | } else if (JF_CONFIG_KEY_IS("check_updates")) { 142 | JF_CONFIG_FILL_VALUE_BOOL(check_updates); 143 | } else if (JF_CONFIG_KEY_IS("try_local_files")) { 144 | if (jf_strong_bool_parse(value, 0, &g_options.try_local_files_config) == false) { 145 | fprintf(stderr, 146 | "Warning: unrecognized value for config option \"try_local_files\": %s", 147 | value); 148 | } 149 | } else { 150 | // option key was not recognized; print a warning and go on 151 | fprintf(stderr, 152 | "Warning: unrecognized option key in settings file line: %s", 153 | line); 154 | } 155 | } 156 | 157 | // apply defaults for missing values 158 | jf_options_complete_with_defaults(); 159 | 160 | free(line); 161 | fclose(config_file); 162 | 163 | // figure out if we should try local files 164 | switch (g_options.try_local_files_config) { 165 | case JF_STRONG_BOOL_NO: 166 | g_options.try_local_files = false; 167 | break; 168 | case JF_STRONG_BOOL_YES: 169 | g_options.try_local_files = jf_net_url_is_localhost(g_options.server); 170 | break; 171 | case JF_STRONG_BOOL_FORCE: 172 | g_options.try_local_files = true; 173 | break; 174 | } 175 | } 176 | 177 | 178 | bool jf_config_write(const char *config_path) 179 | { 180 | FILE *tmp_file; 181 | char *tmp_path; 182 | 183 | if (jf_disk_is_file_accessible(g_state.config_dir) == false) { 184 | assert(mkdir(g_state.config_dir, S_IRWXU) != -1); 185 | } 186 | 187 | tmp_path = jf_concat(2, g_state.config_dir, "/settings.tmp"); 188 | 189 | if ((tmp_file = fopen(tmp_path, "w")) == NULL) { 190 | fprintf(stderr, 191 | "Warning: could not open temporary settings file (%s): %s.\nSettings could not be saved.\n", 192 | tmp_path, 193 | strerror(errno)); 194 | goto bad_exit; 195 | } 196 | JF_CONFIG_WRITE_VALUE(server); 197 | JF_CONFIG_WRITE_VALUE(token); 198 | JF_CONFIG_WRITE_VALUE(userid); 199 | fprintf(tmp_file, "ssl_verifyhost=%s\n", 200 | g_options.ssl_verifyhost ? "true" : "false" ); 201 | JF_CONFIG_WRITE_VALUE(client); 202 | JF_CONFIG_WRITE_VALUE(device); 203 | JF_CONFIG_WRITE_VALUE(deviceid); 204 | JF_CONFIG_WRITE_VALUE(version); 205 | if (g_options.mpv_profile != NULL) { 206 | JF_CONFIG_WRITE_VALUE(mpv_profile); 207 | } 208 | fprintf(tmp_file, "try_local_files=%s\n", 209 | jf_strong_bool_to_str(g_options.try_local_files_config)); 210 | // NB don't write check_updates, we want it set manually 211 | 212 | if (fclose(tmp_file) != 0) { 213 | fprintf(stderr, 214 | "Warning: could not close temporary settings file (%s): %s.\nSettings could not be saved.\n", 215 | tmp_path, 216 | strerror(errno)); 217 | goto bad_exit; 218 | } 219 | if (rename(tmp_path, config_path) != 0) { 220 | fprintf(stderr, 221 | "Warning: could not move temporary settings file to final location (%s): %s.\nSettings could not be saved.\n", 222 | config_path, 223 | strerror(errno)); 224 | goto bad_exit; 225 | } 226 | 227 | free(tmp_path); 228 | return true; 229 | 230 | bad_exit: 231 | free(tmp_path); 232 | return false; 233 | } 234 | //////////////////////////////////////// 235 | 236 | 237 | ////////// INTERACTIVE USER CONFIG ////////// 238 | void jf_config_ask_user_login(void) 239 | { 240 | struct termios old, new; 241 | char *username, *login_post; 242 | jf_growing_buffer password; 243 | jf_reply *login_reply; 244 | int c; 245 | 246 | password = jf_growing_buffer_new(128); 247 | 248 | while (true) { 249 | printf("Please enter your username.\n"); 250 | errno = 0; 251 | username = jf_menu_linenoise("> "); 252 | printf("Please enter your password.\n> "); 253 | tcgetattr(STDIN_FILENO, &old); 254 | new = old; 255 | new.c_lflag &= (unsigned int)~ECHO; 256 | tcsetattr(STDIN_FILENO, TCSANOW, &new); 257 | while ((c = getchar()) != '\n' && c != EOF) { 258 | jf_growing_buffer_append(password, &c, 1); 259 | } 260 | jf_growing_buffer_append(password, "", 1); 261 | tcsetattr(STDIN_FILENO, TCSANOW, &old); 262 | putchar('\n'); 263 | 264 | login_post = jf_json_generate_login_request(username, password->buf); 265 | free(username); 266 | memset(password->buf, 0, password->used); 267 | jf_growing_buffer_empty(password); 268 | login_reply = jf_net_request("/Users/authenticatebyname", 269 | JF_REQUEST_IN_MEMORY, 270 | JF_HTTP_POST, 271 | login_post); 272 | free(login_post); 273 | if (! JF_REPLY_PTR_HAS_ERROR(login_reply)) break; 274 | if (login_reply->state == JF_REPLY_ERROR_HTTP_401) { 275 | jf_reply_free(login_reply); 276 | if (jf_menu_user_ask_yn("Error: invalid login credentials. " 277 | " Would you like to try again?") == false) { 278 | jf_exit(EXIT_SUCCESS); 279 | } 280 | } else { 281 | fprintf(stderr, 282 | "FATAL: could not login: %s\n", 283 | jf_reply_error_string(login_reply)); 284 | jf_reply_free(login_reply); 285 | jf_exit(JF_EXIT_FAILURE); 286 | } 287 | } 288 | printf("Login successful.\n"); 289 | jf_json_parse_login_response(login_reply->payload); 290 | jf_reply_free(login_reply); 291 | jf_growing_buffer_free(password); 292 | } 293 | 294 | 295 | void jf_config_ask_user(void) 296 | { 297 | // login user input 298 | printf("Please enter the encoded URL of your Jellyfin server. Example: http://foo%%20bar.baz:8096/jf\n"); 299 | printf("(note: unless specified, ports will be the protocol's defaults, i.e. 80 for HTTP and 443 for HTTPS)\n"); 300 | while (true) { 301 | g_options.server = jf_menu_linenoise("> "); 302 | if (jf_net_url_is_valid(g_options.server)) { 303 | g_options.server_len = g_options.server == NULL ? 0 : strlen(g_options.server); 304 | break; 305 | } else { 306 | fprintf(stderr, "Error: malformed URL. Please try again.\n"); 307 | free(g_options.server); 308 | } 309 | } 310 | 311 | // critical network stuff: must be configured before network init 312 | if (jf_menu_user_ask_yn("Do you need jftui to ignore hostname validation (required e.g. if you're using Jellyfin's built-in SSL certificate)?")) { 313 | g_options.ssl_verifyhost = false; 314 | } 315 | 316 | jf_config_ask_user_login(); 317 | } 318 | ///////////////////////////////////////////// 319 | -------------------------------------------------------------------------------- /src/config.h: -------------------------------------------------------------------------------- 1 | #ifndef _JF_CONFIG 2 | #define _JF_CONFIG 3 | 4 | #include "shared.h" 5 | #include 6 | #include 7 | 8 | 9 | ////////// CODE MACROS ////////// 10 | #define JF_CONFIG_KEY_IS(key) strncmp(line, key, JF_STATIC_STRLEN(key)) == 0 11 | 12 | #define JF_CONFIG_FILL_VALUE(_key) \ 13 | do { \ 14 | value_len = strlen(value); \ 15 | if (value[value_len - 1] == '\n') value_len--; \ 16 | free(g_options._key); \ 17 | assert((g_options._key = strndup(value, value_len)) != NULL); \ 18 | } while (false) 19 | 20 | #define JF_CONFIG_FILL_VALUE_ARRAY(_key, _bufsize) \ 21 | do { \ 22 | value_len = strlen(value); \ 23 | if (value[value_len - 1] == '\n') value_len--; \ 24 | value_len = jf_clamp_zu(value_len, 0, _bufsize - 1); \ 25 | strncpy(g_options._key, value, value_len); \ 26 | g_options._key[value_len] = '\0'; \ 27 | } while (false) 28 | 29 | #define JF_CONFIG_FILL_VALUE_BOOL(_key) \ 30 | do { \ 31 | if (strncmp(value, "false", JF_STATIC_STRLEN("false")) == 0) { \ 32 | g_options._key= false; \ 33 | } \ 34 | } while (false) 35 | 36 | #define JF_CONFIG_WRITE_VALUE(key) fprintf(tmp_file, #key "=%s\n", g_options.key) 37 | ///////////////////////////////// 38 | 39 | 40 | ////////// JF_OPTIONS ////////// 41 | // defaults 42 | #define JF_CONFIG_SSL_VERIFYHOST_DEFAULT true 43 | #define JF_CONFIG_CLIENT_DEFAULT "jftui" 44 | #define JF_CONFIG_DEVICE_DEFAULT "Linux-PC" 45 | #define JF_CONFIG_DEVICE_SIZE 32 46 | #define JF_CONFIG_DEVICEID_SIZE 8 47 | #define JF_CONFIG_VERSION_DEFAULT JF_VERSION 48 | #define JF_CONFIG_MPV_PROFILE_DEFAULT "jftui" 49 | #define JF_CONFIG_CHECK_UPDATES_DEFAULT true 50 | 51 | 52 | typedef struct jf_options { 53 | char *server; 54 | size_t server_len; 55 | char *token; 56 | char *userid; 57 | bool ssl_verifyhost; 58 | char *client; 59 | char device[JF_CONFIG_DEVICE_SIZE]; 60 | char deviceid[JF_CONFIG_DEVICEID_SIZE]; 61 | char *version; 62 | char *mpv_profile; 63 | bool check_updates; 64 | bool try_local_files; 65 | jf_strong_bool try_local_files_config; 66 | } jf_options; 67 | 68 | 69 | void jf_options_init(void); 70 | void jf_options_clear(void); 71 | //////////////////////////////// 72 | 73 | 74 | ////////// USER CONFIGURATION ////////// 75 | char *jf_config_get_default_dir(void); 76 | void jf_config_read(const char *config_path); 77 | bool jf_config_write(const char *config_path); 78 | void jf_config_ask_user_login(void); 79 | void jf_config_ask_user(void); 80 | //////////////////////////////////////// 81 | 82 | #endif 83 | -------------------------------------------------------------------------------- /src/disk.c: -------------------------------------------------------------------------------- 1 | #include "disk.h" 2 | #include "shared.h" 3 | #include "menu.h" 4 | 5 | #include // malloc, getenv 6 | #include // fwrite etc. 7 | #include // unlink 8 | #include //mkdir 9 | #include 10 | #include 11 | 12 | 13 | ////////// GLOBALS ////////// 14 | extern jf_global_state g_state; 15 | ///////////////////////////// 16 | 17 | 18 | ////////// STATIC VARIABLES ////////// 19 | static jf_growing_buffer s_buffer; 20 | static char *s_file_prefix = NULL; 21 | static jf_file_cache s_payload = (jf_file_cache){ 0 }; 22 | static jf_file_cache s_playlist = (jf_file_cache){ 0 }; 23 | ////////////////////////////////////// 24 | 25 | 26 | ////////// STATIC FUNCTIONS /////////// 27 | static inline void jf_disk_align_to(jf_file_cache *cache, const size_t n); 28 | static inline void jf_disk_open(jf_file_cache *cache); 29 | static void jf_disk_add_next(jf_file_cache *cache, const jf_menu_item *item); 30 | static void jf_disk_add_item(jf_file_cache *cache, const jf_menu_item *item); 31 | static jf_menu_item *jf_disk_get_next(jf_file_cache *cache); 32 | static jf_menu_item *jf_disk_get_item(jf_file_cache *cache, const size_t n); 33 | static void jf_disk_read_to_null_to_buffer(jf_growing_buffer buffer, 34 | jf_file_cache *cache); 35 | /////////////////////////////////////// 36 | 37 | 38 | static inline void jf_disk_open(jf_file_cache *cache) 39 | { 40 | assert((cache->header = fopen(cache->header_path, "w+")) != NULL); 41 | assert((cache->body = fopen(cache->body_path, "w+")) != NULL); 42 | cache->count = 0; 43 | // no error checking on these two, nothing to do if they fail 44 | // at worst we pollute the temp dir, which is not the end of the world 45 | unlink(cache->header_path); 46 | unlink(cache->body_path); 47 | } 48 | 49 | 50 | static inline void 51 | jf_disk_align_to(jf_file_cache *cache, const size_t n) 52 | { 53 | long body_offset; 54 | assert(fseek(cache->header, (long)((n - 1) * sizeof(long)), SEEK_SET) == 0); 55 | assert(fread(&body_offset, sizeof(long), 1, cache->header) == 1); 56 | assert(fseek(cache->body, body_offset, SEEK_SET) == 0); 57 | } 58 | 59 | 60 | static void jf_disk_add_next(jf_file_cache *cache, const jf_menu_item *item) 61 | { 62 | size_t name_length, path_length, i; 63 | 64 | assert(fwrite(&(item->type), sizeof(jf_item_type), 1, cache->body) == 1); 65 | assert(fwrite(item->id, 1, sizeof(item->id), cache->body) == sizeof(item->id)); 66 | if ((name_length = jf_strlen(item->name)) == 0) { 67 | assert(fwrite(&"\0", 1, 1, cache->body) == 1); 68 | } else { 69 | assert(fwrite(item->name, 1, name_length, cache->body) == name_length); 70 | } 71 | if ((path_length = jf_strlen(item->path)) == 0) { 72 | assert(fwrite(&"\0", 1, 1, cache->body) == 1); 73 | } else { 74 | assert(fwrite(item->path, 1, path_length, cache->body) == path_length); 75 | } 76 | assert(fwrite(&(item->runtime_ticks), sizeof(long long), 1, cache->body) == 1); 77 | assert(fwrite(&(item->playback_ticks), sizeof(long long), 1, cache->body) == 1); 78 | assert(fwrite(&(item->children_count), sizeof(size_t), 1, cache->body) == 1); 79 | for (i = 0; i < item->children_count; i++) { 80 | jf_disk_add_next(cache, item->children[i]); 81 | } 82 | } 83 | 84 | 85 | static void jf_disk_add_item(jf_file_cache *cache, const jf_menu_item *item) 86 | { 87 | long starting_body_offset; 88 | 89 | assert(item != NULL); 90 | 91 | assert(fseek(cache->header, 0, SEEK_END) == 0); 92 | assert(fseek(cache->body, 0, SEEK_END) == 0); 93 | assert((starting_body_offset = ftell(cache->body)) != -1); 94 | assert(fwrite(&starting_body_offset, sizeof(long), 1, cache->header) == 1); 95 | 96 | jf_disk_add_next(cache, item); 97 | cache->count++; 98 | } 99 | 100 | 101 | static jf_menu_item *jf_disk_get_next(jf_file_cache *cache) 102 | { 103 | jf_menu_item tmp_item, *item; 104 | size_t path_offset, i; 105 | jf_growing_buffer buffer = jf_growing_buffer_new(256); 106 | 107 | assert(fread(&(tmp_item.type), sizeof(jf_item_type), 1, cache->body) == 1); 108 | assert(fread(tmp_item.id, 1, sizeof(tmp_item.id), cache->body) == sizeof(tmp_item.id)); 109 | jf_disk_read_to_null_to_buffer(buffer, cache); 110 | tmp_item.name = buffer->buf[0] == '\0' ? NULL : buffer->buf; 111 | path_offset = buffer->used; 112 | jf_disk_read_to_null_to_buffer(buffer, cache); 113 | tmp_item.path = buffer->buf[path_offset] == '\0' ? NULL : (buffer->buf + path_offset); 114 | assert(fread(&(tmp_item.runtime_ticks), sizeof(long long), 1, cache->body) == 1); 115 | assert(fread(&(tmp_item.playback_ticks), sizeof(long long), 1, cache->body) == 1); 116 | assert(fread(&(tmp_item.children_count), sizeof(size_t), 1, cache->body) == 1); 117 | if (tmp_item.children_count > 0) { 118 | assert((tmp_item.children = malloc(tmp_item.children_count * sizeof(jf_menu_item *))) != NULL); 119 | for (i = 0; i < tmp_item.children_count; i++) { 120 | tmp_item.children[i] = jf_disk_get_next(cache); 121 | } 122 | } else { 123 | tmp_item.children = NULL; 124 | } 125 | 126 | item = jf_menu_item_new(tmp_item.type, 127 | tmp_item.children, 128 | tmp_item.children_count, 129 | tmp_item.id, 130 | tmp_item.name, 131 | tmp_item.path, 132 | tmp_item.runtime_ticks, 133 | tmp_item.playback_ticks); 134 | 135 | jf_growing_buffer_free(buffer); 136 | 137 | return item; 138 | } 139 | 140 | 141 | static jf_menu_item *jf_disk_get_item(jf_file_cache *cache, const size_t n) 142 | { 143 | if (n == 0 || n > cache->count) return NULL; 144 | 145 | jf_disk_align_to(cache, n); 146 | return jf_disk_get_next(cache); 147 | } 148 | 149 | 150 | static void jf_disk_read_to_null_to_buffer(jf_growing_buffer buffer, 151 | jf_file_cache *cache) 152 | { 153 | char tmp; 154 | 155 | while (true) { 156 | assert(fread(&tmp, 1, 1, cache->body) == 1); 157 | jf_growing_buffer_append(buffer, &tmp, 1); 158 | if (tmp == '\0') break; 159 | } 160 | } 161 | 162 | 163 | void jf_disk_init(void) 164 | { 165 | char *tmp_dir; 166 | char *rand_id; 167 | 168 | if ((tmp_dir = getenv("TMPDIR")) == NULL) { 169 | #ifdef P_tmpdir 170 | tmp_dir = P_tmpdir; 171 | #else 172 | tmp_dir = "/tmp"; 173 | #endif 174 | } 175 | assert(jf_disk_is_file_accessible(tmp_dir)); 176 | assert((s_file_prefix = malloc((size_t)snprintf(NULL, 0, 177 | "%s/jftui_%d_XXXXXX", 178 | tmp_dir, getpid()) + 1)) != NULL); 179 | rand_id = jf_generate_random_id(6); 180 | sprintf(s_file_prefix, "%s/jftui_%d_%s", tmp_dir, getpid(), rand_id); 181 | free(rand_id); 182 | 183 | s_buffer = jf_growing_buffer_new(512); 184 | 185 | assert((s_payload.header_path = jf_concat(2, s_file_prefix, "_s_payload_header")) != NULL); 186 | assert((s_payload.body_path = jf_concat(2, s_file_prefix, "_s_payload_body")) != NULL); 187 | assert((s_playlist.header_path = jf_concat(2, s_file_prefix, "_s_playlist_header")) != NULL); 188 | assert((s_playlist.body_path = jf_concat(2, s_file_prefix, "_s_playlist_body")) != NULL); 189 | 190 | jf_disk_open(&s_payload); 191 | jf_disk_open(&s_playlist); 192 | } 193 | 194 | 195 | void jf_disk_refresh(void) 196 | { 197 | assert(fclose(s_payload.header) == 0); 198 | assert(fclose(s_payload.body) == 0); 199 | jf_disk_open(&s_payload); 200 | assert(fclose(s_playlist.header) == 0); 201 | assert(fclose(s_playlist.body) == 0); 202 | jf_disk_open(&s_playlist); 203 | } 204 | 205 | 206 | ////////// PAYLOAD /////////// 207 | void jf_disk_payload_add_item(const jf_menu_item *item) 208 | { 209 | if (item == NULL) return; 210 | jf_disk_add_item(&s_payload, item); 211 | } 212 | 213 | 214 | jf_menu_item *jf_disk_payload_get_item(const size_t n) 215 | { 216 | return jf_disk_get_item(&s_payload, n); 217 | } 218 | 219 | 220 | jf_item_type jf_disk_payload_get_type(const size_t n) 221 | { 222 | jf_item_type item_type; 223 | 224 | if (n == 0 || n > s_payload.count) { 225 | return JF_ITEM_TYPE_NONE; 226 | } 227 | 228 | jf_disk_align_to(&s_payload, n); 229 | if (fread(&(item_type), sizeof(jf_item_type), 1, s_payload.body) != 1) { 230 | fprintf(stderr, "Warning: jf_payload_get_type: could not read type for item %zu in s_payload.body.\n", n); 231 | return JF_ITEM_TYPE_NONE; 232 | } 233 | return item_type; 234 | } 235 | 236 | 237 | size_t jf_disk_payload_item_count(void) 238 | { 239 | return s_payload.count; 240 | } 241 | ////////////////////////////// 242 | 243 | 244 | ////////// PLAYLIST /////////// 245 | void jf_disk_playlist_add_item(const jf_menu_item *item) 246 | { 247 | if (item == NULL || JF_ITEM_TYPE_IS_FOLDER(item->type)) return; 248 | jf_disk_add_item(&s_playlist, item); 249 | } 250 | 251 | 252 | jf_menu_item *jf_disk_playlist_get_item(const size_t n) 253 | { 254 | return jf_disk_get_item(&s_playlist, n); 255 | } 256 | 257 | 258 | const char *jf_disk_playlist_get_item_name(const size_t n) 259 | { 260 | if (n == 0 || n > s_playlist.count) { 261 | return "Warning: requesting item out of bounds. This is a bug."; 262 | } 263 | 264 | jf_disk_align_to(&s_playlist, n); 265 | assert(fseek(s_playlist.body, 266 | // let him who hath understanding reckon the number of the beast! 267 | sizeof(jf_item_type) + sizeof(((jf_menu_item *)666)->id), 268 | SEEK_CUR) == 0); 269 | 270 | jf_growing_buffer_empty(s_buffer); 271 | jf_disk_read_to_null_to_buffer(s_buffer, &s_playlist); 272 | 273 | return (const char *)s_buffer->buf; 274 | } 275 | 276 | 277 | void jf_disk_playlist_swap_items(const size_t a, const size_t b) 278 | { 279 | long old_a_value; 280 | long old_b_value; 281 | 282 | if (a > s_playlist.count || b > s_playlist.count || a == b) return; 283 | 284 | // read offset a 285 | assert(fseek(s_playlist.header, (long)((a - 1) * sizeof(long)), SEEK_SET) == 0); 286 | assert(fread(&old_a_value, sizeof(long), 1, s_playlist.header) == 1); 287 | // read offset b 288 | assert(fseek(s_playlist.header, (long)((b - 1) * sizeof(long)), SEEK_SET) == 0); 289 | assert(fread(&old_b_value, sizeof(long), 1, s_playlist.header) == 1); 290 | // overwrite b 291 | assert(fseek(s_playlist.header, (long)((b - 1) * sizeof(long)), SEEK_SET) == 0); 292 | assert(fwrite(&old_a_value, sizeof(long), 1, s_playlist.header) == 1); 293 | // overwrite a 294 | assert(fseek(s_playlist.header, (long)((a - 1) * sizeof(long)), SEEK_SET) == 0); 295 | assert(fwrite(&old_b_value, sizeof(long), 1, s_playlist.header) == 1); 296 | } 297 | 298 | 299 | void jf_disk_playlist_replace_item(const size_t n, const jf_menu_item *item) 300 | { 301 | long starting_body_offset; 302 | 303 | assert(item != NULL); 304 | 305 | // overwrite old offset in header 306 | assert(fseek(s_playlist.header, (long)((n - 1) * sizeof(long)), SEEK_SET) == 0); 307 | assert(fseek(s_playlist.body, 0, SEEK_END) == 0); 308 | assert((starting_body_offset = ftell(s_playlist.body)) != -1); 309 | assert(fwrite(&starting_body_offset, sizeof(long), 1, s_playlist.header) == 1); 310 | 311 | // add replacement to tail 312 | jf_disk_add_next(&s_playlist, item); 313 | } 314 | 315 | 316 | size_t jf_disk_playlist_item_count(void) 317 | { 318 | return s_playlist.count; 319 | } 320 | /////////////////////////////// 321 | 322 | 323 | ////////// MISC BULLSHIT ////////// 324 | bool jf_disk_is_file_accessible(const char *path) 325 | { 326 | return access(path, F_OK) == 0; 327 | } 328 | ////////////////////////////////// 329 | -------------------------------------------------------------------------------- /src/disk.h: -------------------------------------------------------------------------------- 1 | #ifndef _JF_DISK 2 | #define _JF_DISK 3 | 4 | 5 | #include 6 | 7 | #include "shared.h" 8 | 9 | 10 | ////////// CONSTANTS ////////// 11 | #define JF_DISK_BUFFER_SIZE 1024 12 | /////////////////////////////// 13 | 14 | 15 | ////////// FILE CACHE ////////// 16 | typedef struct jf_file_cache { 17 | FILE *header; 18 | char *header_path; 19 | FILE *body; 20 | char *body_path; 21 | size_t count; 22 | } jf_file_cache; 23 | /////////////////////////////// 24 | 25 | 26 | ////////// FUNCTION STUBS ////////// 27 | void jf_disk_init(void); 28 | void jf_disk_refresh(void); 29 | 30 | 31 | void jf_disk_payload_add_item(const jf_menu_item *item); 32 | jf_menu_item *jf_disk_payload_get_item(const size_t n); 33 | jf_item_type jf_disk_payload_get_type(const size_t n); 34 | size_t jf_disk_payload_item_count(void); 35 | 36 | 37 | void jf_disk_playlist_add_item(const jf_menu_item *item); 38 | void jf_disk_playlist_replace_item(const size_t n, const jf_menu_item *item); 39 | void jf_disk_playlist_swap_items(const size_t a, const size_t b); 40 | jf_menu_item *jf_disk_playlist_get_item(const size_t n); 41 | const char *jf_disk_playlist_get_item_name(const size_t n); 42 | size_t jf_disk_playlist_item_count(void); 43 | 44 | 45 | bool jf_disk_is_file_accessible(const char *path); 46 | //////////////////////////////////// 47 | #endif 48 | -------------------------------------------------------------------------------- /src/json.c: -------------------------------------------------------------------------------- 1 | #include "json.h" 2 | #include "config.h" 3 | #include "shared.h" 4 | #include "menu.h" 5 | #include "disk.h" 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | 17 | ////////// GLOBALS ////////// 18 | extern jf_options g_options; 19 | extern jf_global_state g_state; 20 | ///////////////////////////// 21 | 22 | 23 | ////////// STATIC VARIABLES ////////// 24 | static char s_error_buffer[JF_PARSER_ERROR_BUFFER_SIZE]; 25 | ////////////////////////////////////// 26 | 27 | 28 | ////////// STATIC FUNCTIONS ////////// 29 | static int jf_sax_items_start_map(void *ctx); 30 | static int jf_sax_items_end_map(void *ctx); 31 | static int jf_sax_items_map_key(void *ctx, const unsigned char *key, size_t key_len); 32 | static int jf_sax_items_start_array(void *ctx); 33 | static int jf_sax_items_end_array(void *ctx); 34 | static int jf_sax_items_string(void *ctx, const unsigned char *string, size_t strins_len); 35 | static int jf_sax_items_number(void *ctx, const char *string, size_t strins_len); 36 | 37 | // Allocates a new yajl parser instance, registering callbacks and context and 38 | // setting yajl_allow_multiple_values to let it digest multiple JSON messages 39 | // in a row. 40 | // Failures cause SIGABRT. 41 | // 42 | // Parameters: 43 | // - callbacks: Pointer to callbacks struct to register. 44 | // - context: Pointer to json parser context to register. 45 | // 46 | // Returns: 47 | // The yajl_handle of the new parser. 48 | static inline yajl_handle jf_sax_yajl_parser_new(yajl_callbacks *callbacks, jf_sax_context *context); 49 | 50 | static inline bool jf_sax_current_item_is_valid(const jf_sax_context *context); 51 | static inline void jf_sax_current_item_make_and_print_name(jf_sax_context *context); 52 | static inline void jf_sax_context_init(jf_sax_context *context, jf_thread_buffer *tb); 53 | static inline void jf_sax_context_current_item_clear(jf_sax_context *context); 54 | 55 | // DO NOT USE THIS! Call the macro with the same name sans leading __ 56 | static inline yajl_val __jf_yajl_tree_get_assert(const int lineno, 57 | yajl_val parent, 58 | const char **path, 59 | yajl_type type); 60 | 61 | static jf_menu_item *jf_json_parse_versions(const jf_menu_item *item, const yajl_val media_sources); 62 | ////////////////////////////////////// 63 | 64 | 65 | ////////// SAX PARSER CALLBACKS ////////// 66 | static int jf_sax_items_start_map(void *ctx) 67 | { 68 | jf_sax_context *context = (jf_sax_context *)(ctx); 69 | switch (context->parser_state) { 70 | case JF_SAX_IDLE: 71 | context->tb->item_count = 0; 72 | jf_sax_context_current_item_clear(context); 73 | jf_disk_refresh(); 74 | context->parser_state = JF_SAX_IN_QUERYRESULT_MAP; 75 | break; 76 | case JF_SAX_IN_LATEST_ARRAY: 77 | context->latest_array = true; 78 | context->parser_state = JF_SAX_IN_ITEM_MAP; 79 | break; 80 | case JF_SAX_IN_ITEMS_ARRAY: 81 | context->parser_state = JF_SAX_IN_ITEM_MAP; 82 | break; 83 | case JF_SAX_IN_USERDATA_VALUE: 84 | context->parser_state = JF_SAX_IN_USERDATA_MAP; 85 | break; 86 | case JF_SAX_IN_ITEM_MAP: 87 | context->state_to_resume = JF_SAX_IN_ITEM_MAP; 88 | context->parser_state = JF_SAX_IGNORE; 89 | context->maps_ignoring = 1; 90 | break; 91 | case JF_SAX_IGNORE: 92 | context->maps_ignoring++; 93 | break; 94 | default: 95 | JF_SAX_BAD_STATE(); 96 | } 97 | return 1; 98 | } 99 | 100 | 101 | static int jf_sax_items_end_map(void *ctx) 102 | { 103 | jf_sax_context *context = (jf_sax_context *)(ctx); 104 | switch (context->parser_state) { 105 | case JF_SAX_IN_QUERYRESULT_MAP: 106 | context->parser_state = JF_SAX_IDLE; 107 | break; 108 | case JF_SAX_IN_ITEMS_VALUE: 109 | context->parser_state = JF_SAX_IN_QUERYRESULT_MAP; 110 | break; 111 | case JF_SAX_IN_USERDATA_MAP: 112 | context->parser_state = JF_SAX_IN_ITEM_MAP; 113 | break; 114 | case JF_SAX_IN_ITEM_MAP: 115 | if (jf_sax_current_item_is_valid(context)) { 116 | context->tb->item_count++; 117 | jf_sax_current_item_make_and_print_name(context); 118 | 119 | jf_menu_item *item = jf_menu_item_new(context->current_item_type, 120 | NULL, 0, 121 | (const char*)(context->parsed_content->buf 122 | + context->id_start), 123 | context->current_item_display_name->buf, 124 | context->current_item_path->used > 0 ? context->current_item_path->buf : 0, 125 | context->runtime_ticks, 126 | context->playback_ticks); 127 | jf_disk_payload_add_item(item); 128 | jf_menu_item_free(item); 129 | } 130 | jf_sax_context_current_item_clear(context); 131 | 132 | if (context->latest_array) { 133 | context->parser_state = JF_SAX_IN_LATEST_ARRAY; 134 | context->latest_array = false; 135 | } else { 136 | context->parser_state = JF_SAX_IN_ITEMS_ARRAY; 137 | } 138 | break; 139 | case JF_SAX_IGNORE: 140 | context->maps_ignoring--; 141 | if (context->maps_ignoring == 0 && context->arrays_ignoring == 0) { 142 | context->parser_state = context->state_to_resume; 143 | context->state_to_resume = JF_SAX_NO_STATE; 144 | } 145 | default: 146 | break; 147 | } 148 | return 1; 149 | } 150 | 151 | 152 | static int jf_sax_items_map_key(void *ctx, const unsigned char *key, size_t key_len) 153 | { 154 | jf_sax_context *context = (jf_sax_context *)(ctx); 155 | switch (context->parser_state) { 156 | case JF_SAX_IN_QUERYRESULT_MAP: 157 | if (JF_SAX_KEY_IS("Items")) { 158 | context->parser_state = JF_SAX_IN_ITEMS_VALUE; 159 | } 160 | break; 161 | case JF_SAX_IN_ITEM_MAP: 162 | if (JF_SAX_KEY_IS("Name")) { 163 | context->parser_state = JF_SAX_IN_ITEM_NAME_VALUE; 164 | } else if (JF_SAX_KEY_IS("Type")) { 165 | context->parser_state = JF_SAX_IN_ITEM_TYPE_VALUE; 166 | } else if (JF_SAX_KEY_IS("CollectionType")) { 167 | context->parser_state = JF_SAX_IN_ITEM_COLLECTION_TYPE_VALUE; 168 | } else if (JF_SAX_KEY_IS("Id")) { 169 | context->parser_state = JF_SAX_IN_ITEM_ID_VALUE; 170 | } else if (JF_SAX_KEY_IS("AlbumArtist")) { 171 | context->parser_state = JF_SAX_IN_ITEM_ALBUMARTIST_VALUE; 172 | } else if (JF_SAX_KEY_IS("Album")) { 173 | context->parser_state = JF_SAX_IN_ITEM_ALBUM_VALUE; 174 | } else if (JF_SAX_KEY_IS("SeriesName")) { 175 | context->parser_state = JF_SAX_IN_ITEM_SERIES_VALUE; 176 | } else if (JF_SAX_KEY_IS("ProductionYear")) { 177 | context->parser_state = JF_SAX_IN_ITEM_YEAR_VALUE; 178 | } else if (JF_SAX_KEY_IS("IndexNumber")) { 179 | context->parser_state = JF_SAX_IN_ITEM_INDEX_VALUE; 180 | } else if (JF_SAX_KEY_IS("ParentIndexNumber")) { 181 | context->parser_state = JF_SAX_IN_ITEM_PARENT_INDEX_VALUE; 182 | } else if (JF_SAX_KEY_IS("RunTimeTicks")) { 183 | context->parser_state = JF_SAX_IN_ITEM_RUNTIME_TICKS_VALUE; 184 | } else if (JF_SAX_KEY_IS("UserData")) { 185 | context->parser_state = JF_SAX_IN_USERDATA_VALUE; 186 | } else if (JF_SAX_KEY_IS("Path") && g_options.try_local_files) { 187 | context->parser_state = JF_SAX_IN_ITEM_PATH_VALUE; 188 | } 189 | break; 190 | case JF_SAX_IN_USERDATA_MAP: 191 | if (JF_SAX_KEY_IS("PlaybackPositionTicks")) { 192 | context->parser_state = JF_SAX_IN_USERDATA_TICKS_VALUE; 193 | } 194 | default: 195 | break; 196 | } 197 | return 1; 198 | } 199 | 200 | static int jf_sax_items_start_array(void *ctx) 201 | { 202 | jf_sax_context *context = (jf_sax_context *)(ctx); 203 | switch (context->parser_state) { 204 | case JF_SAX_IDLE: 205 | context->parser_state = JF_SAX_IN_LATEST_ARRAY; 206 | context->tb->item_count = 0; 207 | jf_sax_context_current_item_clear(context); 208 | break; 209 | case JF_SAX_IN_ITEMS_VALUE: 210 | context->parser_state = JF_SAX_IN_ITEMS_ARRAY; 211 | break; 212 | case JF_SAX_IN_ITEM_MAP: 213 | context->parser_state = JF_SAX_IGNORE; 214 | context->state_to_resume = JF_SAX_IN_ITEM_MAP; 215 | context->arrays_ignoring = 1; 216 | break; 217 | case JF_SAX_IGNORE: 218 | context->arrays_ignoring++; 219 | break; 220 | default: 221 | JF_SAX_BAD_STATE(); 222 | } 223 | return 1; 224 | } 225 | 226 | 227 | static int jf_sax_items_end_array(void *ctx) 228 | { 229 | jf_sax_context *context = (jf_sax_context *)(ctx); 230 | switch (context->parser_state) { 231 | case JF_SAX_IN_LATEST_ARRAY: 232 | context->parser_state = JF_SAX_IDLE; 233 | break; 234 | case JF_SAX_IN_ITEMS_ARRAY: 235 | context->parser_state = JF_SAX_IN_QUERYRESULT_MAP; 236 | break; 237 | case JF_SAX_IGNORE: 238 | context->arrays_ignoring--; 239 | if (context->arrays_ignoring == 0 && context->maps_ignoring == 0) { 240 | context->parser_state = context->state_to_resume; 241 | context->state_to_resume = JF_SAX_NO_STATE; 242 | } 243 | break; 244 | default: 245 | JF_SAX_BAD_STATE(); 246 | } 247 | return 1; 248 | } 249 | 250 | 251 | static int jf_sax_items_string(void *ctx, const unsigned char *string, size_t string_len) 252 | { 253 | jf_sax_context *context = (jf_sax_context *)(ctx); 254 | switch (context->parser_state) { 255 | case JF_SAX_IN_ITEM_NAME_VALUE: 256 | JF_SAX_ITEM_FILL(name); 257 | context->parser_state = JF_SAX_IN_ITEM_MAP; 258 | break; 259 | case JF_SAX_IN_ITEM_TYPE_VALUE: 260 | if (JF_SAX_STRING_IS("CollectionFolder") 261 | && context->current_item_type == JF_ITEM_TYPE_NONE) { 262 | // don't overwrite if we already got more specific information 263 | context->current_item_type = JF_ITEM_TYPE_COLLECTION; 264 | } else if (JF_SAX_STRING_IS("Folder") 265 | || JF_SAX_STRING_IS("UserView") 266 | || JF_SAX_STRING_IS("PlaylistsFolder")) { 267 | context->current_item_type = JF_ITEM_TYPE_FOLDER; 268 | } else if (JF_SAX_STRING_IS("Playlist")) { 269 | context->current_item_type = JF_ITEM_TYPE_PLAYLIST; 270 | } else if (JF_SAX_STRING_IS("Audio")) { 271 | context->current_item_type = JF_ITEM_TYPE_AUDIO; 272 | } else if (JF_SAX_STRING_IS("Artist") 273 | || JF_SAX_STRING_IS("MusicArtist")) { 274 | context->current_item_type = JF_ITEM_TYPE_ARTIST; 275 | } else if (JF_SAX_STRING_IS("MusicAlbum")) { 276 | context->current_item_type = JF_ITEM_TYPE_ALBUM; 277 | } else if (JF_SAX_STRING_IS("Episode")) { 278 | context->current_item_type = JF_ITEM_TYPE_EPISODE; 279 | } else if (JF_SAX_STRING_IS("Season")) { 280 | context->current_item_type = JF_ITEM_TYPE_SEASON; 281 | } else if (JF_SAX_STRING_IS("SeriesName") 282 | || JF_SAX_STRING_IS("Series")) { 283 | context->current_item_type = JF_ITEM_TYPE_SERIES; 284 | } else if (JF_SAX_STRING_IS("Movie")) { 285 | context->current_item_type = JF_ITEM_TYPE_MOVIE; 286 | } else if (JF_SAX_STRING_IS("MusicVideo")) { 287 | context->current_item_type = JF_ITEM_TYPE_MUSIC_VIDEO; 288 | } else if (JF_SAX_STRING_IS("AudioBook")) { 289 | context->current_item_type = JF_ITEM_TYPE_AUDIOBOOK; 290 | } 291 | context->parser_state = JF_SAX_IN_ITEM_MAP; 292 | break; 293 | case JF_SAX_IN_ITEM_COLLECTION_TYPE_VALUE: 294 | if (JF_SAX_STRING_IS("music")) { 295 | context->current_item_type = JF_ITEM_TYPE_COLLECTION_MUSIC; 296 | } else if (JF_SAX_STRING_IS("tvshows")) { 297 | context->current_item_type = JF_ITEM_TYPE_COLLECTION_SERIES; 298 | } else if (JF_SAX_STRING_IS("movies") || JF_SAX_STRING_IS("homevideos")) { 299 | context->current_item_type = JF_ITEM_TYPE_COLLECTION_MOVIES; 300 | } else if (JF_SAX_STRING_IS("musicvideos")) { 301 | context->current_item_type = JF_ITEM_TYPE_COLLECTION_MUSIC_VIDEOS; 302 | } else if (JF_SAX_STRING_IS("folders")) { 303 | context->current_item_type = JF_ITEM_TYPE_FOLDER; 304 | } 305 | context->parser_state = JF_SAX_IN_ITEM_MAP; 306 | break; 307 | case JF_SAX_IN_ITEM_ID_VALUE: 308 | JF_SAX_ITEM_FILL(id); 309 | context->parser_state = JF_SAX_IN_ITEM_MAP; 310 | break; 311 | case JF_SAX_IN_ITEM_ALBUMARTIST_VALUE: 312 | JF_SAX_ITEM_FILL(artist); 313 | context->parser_state = JF_SAX_IN_ITEM_MAP; 314 | break; 315 | case JF_SAX_IN_ITEM_ALBUM_VALUE: 316 | JF_SAX_ITEM_FILL(album); 317 | context->parser_state = JF_SAX_IN_ITEM_MAP; 318 | break; 319 | case JF_SAX_IN_ITEM_SERIES_VALUE: 320 | JF_SAX_ITEM_FILL(series); 321 | context->parser_state = JF_SAX_IN_ITEM_MAP; 322 | break; 323 | case JF_SAX_IN_ITEM_PATH_VALUE: 324 | jf_growing_buffer_append(context->current_item_path, string, string_len); 325 | jf_growing_buffer_append(context->current_item_path, "", 1); 326 | context->parser_state = JF_SAX_IN_ITEM_MAP; 327 | break; 328 | default: 329 | break; 330 | } 331 | return 1; 332 | } 333 | 334 | 335 | static int jf_sax_items_number(void *ctx, const char *string, size_t string_len) 336 | { 337 | jf_sax_context *context = (jf_sax_context *)(ctx); 338 | switch (context->parser_state) { 339 | case JF_SAX_IN_ITEM_RUNTIME_TICKS_VALUE: 340 | context->runtime_ticks = strtoll(string, NULL, 10); 341 | context->parser_state = JF_SAX_IN_ITEM_MAP; 342 | break; 343 | case JF_SAX_IN_USERDATA_TICKS_VALUE: 344 | context->playback_ticks = strtoll(string, NULL, 10); 345 | context->parser_state = JF_SAX_IN_USERDATA_MAP; 346 | break; 347 | case JF_SAX_IN_ITEM_YEAR_VALUE: 348 | JF_SAX_ITEM_FILL(year); 349 | context->parser_state = JF_SAX_IN_ITEM_MAP; 350 | break; 351 | case JF_SAX_IN_ITEM_INDEX_VALUE: 352 | JF_SAX_ITEM_FILL(index); 353 | context->parser_state = JF_SAX_IN_ITEM_MAP; 354 | break; 355 | case JF_SAX_IN_ITEM_PARENT_INDEX_VALUE: 356 | JF_SAX_ITEM_FILL(parent_index); 357 | context->parser_state = JF_SAX_IN_ITEM_MAP; 358 | break; 359 | default: 360 | // ignore everything else 361 | break; 362 | } 363 | return 1; 364 | } 365 | ////////////////////////////////////////// 366 | 367 | 368 | ////////// SAX PARSER ////////// 369 | static inline bool jf_sax_current_item_is_valid(const jf_sax_context *context) 370 | { 371 | if (JF_ITEM_TYPE_IS_FOLDER(context->current_item_type)) { 372 | return context->name_len != 0 && context->id_len != 0; 373 | } else { 374 | return context->name_len != 0 375 | && context->id_len != 0 376 | && context->runtime_ticks != 0; 377 | } 378 | } 379 | 380 | 381 | static inline void jf_sax_current_item_make_and_print_name(jf_sax_context *context) 382 | { 383 | jf_growing_buffer_empty(context->current_item_display_name); 384 | switch (context->current_item_type) { 385 | case JF_ITEM_TYPE_AUDIO: 386 | case JF_ITEM_TYPE_AUDIOBOOK: 387 | JF_SAX_PRINT_LEADER("T"); 388 | if (context->tb->promiscuous_context) { 389 | JF_SAX_TRY_APPEND_NAME("", artist, " - "); 390 | JF_SAX_TRY_APPEND_NAME("", album, " - "); 391 | } 392 | JF_SAX_TRY_APPEND_NAME("", parent_index, "."); 393 | JF_SAX_TRY_APPEND_NAME("", index, " - "); 394 | jf_growing_buffer_append(context->current_item_display_name, 395 | context->parsed_content->buf + context->name_start, 396 | context->name_len); 397 | break; 398 | case JF_ITEM_TYPE_ALBUM: 399 | JF_SAX_PRINT_LEADER("D"); 400 | if (context->tb->promiscuous_context) { 401 | JF_SAX_TRY_APPEND_NAME("", artist, " - "); 402 | } 403 | jf_growing_buffer_append(context->current_item_display_name, 404 | context->parsed_content->buf + context->name_start, 405 | context->name_len); 406 | JF_SAX_TRY_APPEND_NAME(" (", year, ")"); 407 | break; 408 | case JF_ITEM_TYPE_EPISODE: 409 | JF_SAX_PRINT_LEADER("V"); 410 | if (context->tb->promiscuous_context) { 411 | JF_SAX_TRY_APPEND_NAME("", series, " - "); 412 | JF_SAX_TRY_APPEND_NAME("S", parent_index, ""); 413 | } 414 | JF_SAX_TRY_APPEND_NAME("E", index, " "); 415 | jf_growing_buffer_append(context->current_item_display_name, 416 | context->parsed_content->buf + context->name_start, 417 | context->name_len); 418 | break; 419 | case JF_ITEM_TYPE_SEASON: 420 | JF_SAX_PRINT_LEADER("D"); 421 | if (context->tb->promiscuous_context) { 422 | JF_SAX_TRY_APPEND_NAME("", series, " - "); 423 | } 424 | jf_growing_buffer_append(context->current_item_display_name, 425 | context->parsed_content->buf + context->name_start, 426 | context->name_len); 427 | break; 428 | case JF_ITEM_TYPE_MOVIE: 429 | case JF_ITEM_TYPE_MUSIC_VIDEO: 430 | JF_SAX_PRINT_LEADER("V"); 431 | jf_growing_buffer_append(context->current_item_display_name, 432 | context->parsed_content->buf + context->name_start, 433 | context->name_len); 434 | JF_SAX_TRY_APPEND_NAME(" (", year, ")"); 435 | break; 436 | case JF_ITEM_TYPE_ARTIST: 437 | case JF_ITEM_TYPE_SERIES: 438 | case JF_ITEM_TYPE_PLAYLIST: 439 | case JF_ITEM_TYPE_FOLDER: 440 | case JF_ITEM_TYPE_COLLECTION: 441 | case JF_ITEM_TYPE_COLLECTION_MUSIC: 442 | case JF_ITEM_TYPE_COLLECTION_SERIES: 443 | case JF_ITEM_TYPE_COLLECTION_MOVIES: 444 | case JF_ITEM_TYPE_COLLECTION_MUSIC_VIDEOS: 445 | case JF_ITEM_TYPE_USER_VIEW: 446 | JF_SAX_PRINT_LEADER("D"); 447 | jf_growing_buffer_append(context->current_item_display_name, 448 | context->parsed_content->buf + context->name_start, 449 | context->name_len); 450 | break; 451 | default: 452 | fprintf(stderr, "Warning: jf_sax_items_end_map: unexpected jf_item_type. This is a bug.\n"); 453 | } 454 | 455 | jf_growing_buffer_append(context->current_item_display_name, "", 1); 456 | printf("%s\n", context->current_item_display_name->buf); 457 | } 458 | 459 | 460 | static inline yajl_handle jf_sax_yajl_parser_new(yajl_callbacks *callbacks, jf_sax_context *context) 461 | { 462 | yajl_handle parser; 463 | assert((parser = yajl_alloc(callbacks, NULL, (void *)(context))) != NULL); 464 | // allow persistent parser to digest many JSON objects 465 | assert(yajl_config(parser, yajl_allow_multiple_values, 1) != 0); 466 | return parser; 467 | } 468 | 469 | 470 | static inline void jf_sax_context_init(jf_sax_context *context, jf_thread_buffer *tb) 471 | { 472 | *context = (jf_sax_context){ 0 }; 473 | context->parser_state = JF_SAX_IDLE; 474 | context->state_to_resume = JF_SAX_NO_STATE; 475 | context->latest_array = false; 476 | context->tb = tb; 477 | context->current_item_type = JF_ITEM_TYPE_NONE; 478 | context->current_item_display_name = jf_growing_buffer_new(0); 479 | context->current_item_path = jf_growing_buffer_new(0); 480 | context->parsed_content = jf_growing_buffer_new(0); 481 | } 482 | 483 | 484 | static inline void jf_sax_context_current_item_clear(jf_sax_context *context) 485 | { 486 | context->current_item_type = JF_ITEM_TYPE_NONE; 487 | jf_growing_buffer_empty(context->parsed_content); 488 | jf_growing_buffer_empty(context->current_item_path); 489 | context->name_len = 0; 490 | context->id_len = 0; 491 | context->artist_len = 0; 492 | context->album_len = 0; 493 | context->series_len = 0; 494 | context->year_len = 0; 495 | context->index_len = 0; 496 | context->parent_index_len = 0; 497 | context->runtime_ticks = 0; 498 | context->playback_ticks = 0; 499 | } 500 | 501 | 502 | void *jf_json_sax_thread(void *arg) 503 | { 504 | jf_sax_context context; 505 | yajl_status status; 506 | yajl_handle parser; 507 | yajl_callbacks callbacks = { 508 | .yajl_null = NULL, 509 | .yajl_boolean = NULL, 510 | .yajl_integer = NULL, 511 | .yajl_double = NULL, 512 | .yajl_number = jf_sax_items_number, 513 | .yajl_string = jf_sax_items_string, 514 | .yajl_start_map = jf_sax_items_start_map, 515 | .yajl_map_key = jf_sax_items_map_key, 516 | .yajl_end_map = jf_sax_items_end_map, 517 | .yajl_start_array = jf_sax_items_start_array, 518 | .yajl_end_array = jf_sax_items_end_array 519 | }; 520 | unsigned char *error_str; 521 | 522 | jf_sax_context_init(&context, (jf_thread_buffer *)arg); 523 | 524 | assert((parser = jf_sax_yajl_parser_new(&callbacks, &context)) != NULL); 525 | 526 | pthread_mutex_lock(&context.tb->mut); 527 | while (true) { 528 | while (context.tb->state != JF_THREAD_BUFFER_STATE_PENDING_DATA) { 529 | pthread_cond_wait(&context.tb->cv_no_data, &context.tb->mut); 530 | } 531 | if ((status = yajl_parse(parser, (unsigned char*)context.tb->data, context.tb->used)) != yajl_status_ok) { 532 | error_str = yajl_get_error(parser, 1, (unsigned char*)context.tb->data, context.tb->used); 533 | strcpy(context.tb->data, "yajl_parse error: "); 534 | strncat(context.tb->data, (char *)error_str, JF_PARSER_ERROR_BUFFER_SIZE - strlen(context.tb->data)); 535 | context.tb->state = JF_THREAD_BUFFER_STATE_PARSER_ERROR; 536 | pthread_mutex_unlock(&context.tb->mut); 537 | yajl_free_error(parser, error_str); 538 | // the parser never recovers after an error; we must free and reallocate it 539 | yajl_free(parser); 540 | parser = jf_sax_yajl_parser_new(&callbacks, &context); 541 | } else if (context.parser_state == JF_SAX_IDLE) { 542 | // JSON fully parsed 543 | yajl_complete_parse(parser); 544 | context.tb->state = JF_THREAD_BUFFER_STATE_CLEAR; 545 | } else { 546 | // we've still more to go 547 | context.tb->state = JF_THREAD_BUFFER_STATE_AWAITING_DATA; 548 | } 549 | 550 | context.tb->used = 0; 551 | pthread_cond_signal(&context.tb->cv_has_data); 552 | } 553 | } 554 | //////////////////////////////// 555 | 556 | 557 | ////////// VIDEO PARSING ////////// 558 | static jf_menu_item *jf_json_parse_versions(const jf_menu_item *item, const yajl_val media_sources) 559 | { 560 | jf_menu_item **subs = NULL; 561 | size_t subs_count = 0; 562 | size_t i, j; 563 | char *tmp; 564 | yajl_val media_streams, source, stream; 565 | jf_growing_buffer buf; 566 | 567 | if (YAJL_GET_ARRAY(media_sources)->len > 1) { 568 | buf = jf_growing_buffer_new(512); 569 | 570 | jf_growing_buffer_sprintf(buf, 0, "\nThere are multiple versions available of %s.\n", item->name); 571 | jf_growing_buffer_sprintf(buf, 0, "Please choose one:\n"); 572 | for (i = 0; i < YAJL_GET_ARRAY(media_sources)->len; i++) { 573 | assert((source = YAJL_GET_ARRAY(media_sources)->values[i]) != NULL); 574 | jf_growing_buffer_sprintf(buf, 0, "%zu: %s (", 575 | i + 1, 576 | YAJL_GET_STRING(jf_yajl_tree_get_assert(source, 577 | ((const char *[]){ "Name", NULL }), 578 | yajl_t_string))); 579 | media_streams = jf_yajl_tree_get_assert(source, 580 | ((const char *[]){ "MediaStreams", NULL }), 581 | yajl_t_array); 582 | for (j = 0; j < YAJL_GET_ARRAY(media_streams)->len; j++) { 583 | assert((stream = YAJL_GET_ARRAY(media_streams)->values[j]) != NULL); 584 | jf_growing_buffer_sprintf(buf, 0, " %s", 585 | YAJL_GET_STRING(jf_yajl_tree_get_assert(stream, 586 | ((const char *[]){ "DisplayTitle", NULL }), 587 | yajl_t_string))); 588 | } 589 | jf_growing_buffer_sprintf(buf, 0, ")\n"); 590 | } 591 | i = jf_menu_user_ask_selection(buf->buf, 1, YAJL_GET_ARRAY(media_sources)->len); 592 | i--; 593 | 594 | jf_growing_buffer_free(buf); 595 | } else { 596 | i = 0; 597 | } 598 | 599 | // external subtitles 600 | assert((source = YAJL_GET_ARRAY(media_sources)->values[i]) != NULL); 601 | media_streams = jf_yajl_tree_get_assert(source, 602 | ((const char *[]){ "MediaStreams", NULL }), 603 | yajl_t_array); 604 | for (j = 0; j < YAJL_GET_ARRAY(media_streams)->len; j++) { 605 | assert((stream = YAJL_GET_ARRAY(media_streams)->values[j]) != NULL); 606 | char *codec = YAJL_GET_STRING(jf_yajl_tree_get_assert(stream, 607 | ((const char*[]){ "Codec", NULL}), 608 | yajl_t_string)); 609 | if (strcmp(YAJL_GET_STRING(jf_yajl_tree_get_assert(stream, 610 | ((const char *[]){ "Type", NULL }), 611 | yajl_t_string)), 612 | "Subtitle") == 0 613 | && YAJL_IS_TRUE(jf_yajl_tree_get_assert(stream, 614 | ((const char *[]){ "IsExternal", NULL }), 615 | yajl_t_any)) 616 | && strcmp(codec, "sub") != 0) { 617 | char *id = YAJL_GET_STRING(jf_yajl_tree_get_assert(source, ((const char *[]){ "Id", NULL }), yajl_t_string)); 618 | tmp = jf_concat(8, 619 | "/videos/", 620 | id, 621 | "/", 622 | id, 623 | "/subtitles/", 624 | YAJL_GET_NUMBER(jf_yajl_tree_get_assert(stream, ((const char *[]){ "Index", NULL }), yajl_t_number)), 625 | // 10.7.2 added routeStartPositionTicks 626 | g_state.server_version >= JF_SERVER_VERSION_MAKE(10,7,2) ? 627 | "/0/stream." : "/stream", 628 | codec); 629 | assert((subs = realloc(subs, ++subs_count * sizeof(jf_menu_item *))) != NULL); 630 | subs[subs_count - 1] = jf_menu_item_new(JF_ITEM_TYPE_VIDEO_SUB, 631 | NULL, 0, // children, children_count 632 | NULL, // id 633 | tmp, 634 | YAJL_GET_STRING(jf_yajl_tree_get_assert(stream, ((const char *[]){ "Path", NULL }), yajl_t_string)), 635 | 0, 0); // ticks 636 | free(tmp); 637 | if ((tmp = YAJL_GET_STRING(yajl_tree_get(stream, ((const char *[]){ "Language", NULL }), yajl_t_string))) == NULL) { 638 | subs[subs_count - 1]->id[0] = '\0'; 639 | } else { 640 | strncpy(subs[subs_count - 1]->id, tmp, 3); 641 | } 642 | strncpy(subs[subs_count - 1]->id + 3, 643 | YAJL_GET_STRING(jf_yajl_tree_get_assert(stream, ((const char *[]){ "DisplayTitle", NULL }), yajl_t_string)), 644 | JF_ID_LENGTH - 3); 645 | subs[subs_count - 1]->id[JF_ID_LENGTH] = '\0'; 646 | } 647 | } 648 | 649 | return jf_menu_item_new(JF_ITEM_TYPE_VIDEO_SOURCE, 650 | subs, subs_count, 651 | YAJL_GET_STRING(jf_yajl_tree_get_assert(source, ((const char *[]){ "Id", NULL }), yajl_t_string)), 652 | NULL, 653 | YAJL_GET_STRING(jf_yajl_tree_get_assert(source, ((const char *[]){ "Path", NULL }), yajl_t_string)), 654 | YAJL_GET_INTEGER(jf_yajl_tree_get_assert(source, ((const char *[]){ "RunTimeTicks", NULL }), yajl_t_number)), // RT ticks 655 | 0); 656 | } 657 | 658 | 659 | void jf_json_parse_video(jf_menu_item *item, const char *video, const char *additional_parts) 660 | { 661 | yajl_val parsed, part_count, part_item; 662 | size_t i; 663 | 664 | JF_JSON_TREE_PARSE_ASSERT( 665 | (parsed = yajl_tree_parse(video, 666 | s_error_buffer, 667 | JF_PARSER_ERROR_BUFFER_SIZE)) != NULL 668 | ); 669 | // PartCount is not defined when it is == 1 670 | if ((part_count = yajl_tree_get(parsed, (const char *[]){ "PartCount", NULL }, yajl_t_number)) == NULL) { 671 | item->children_count = 1; 672 | } else { 673 | item->children_count = (size_t)YAJL_GET_INTEGER(part_count); 674 | } 675 | assert((item->children = malloc(item->children_count * sizeof(jf_menu_item *))) != NULL); 676 | item->children[0] = jf_json_parse_versions(item, 677 | jf_yajl_tree_get_assert(parsed, 678 | ((const char *[]){ "MediaSources", NULL }), 679 | yajl_t_array)); 680 | yajl_tree_free(parsed); 681 | 682 | // check for additional parts 683 | if (item->children_count > 1) { 684 | s_error_buffer[0] = '\0'; 685 | if ((parsed = yajl_tree_parse(additional_parts, 686 | s_error_buffer, 687 | JF_PARSER_ERROR_BUFFER_SIZE)) == NULL) { 688 | fprintf(stderr, "FATAL: jf_json_parse_additional_parts: %s\n", 689 | s_error_buffer[0] == '\0' ? "yajl_tree_parse unknown error" : s_error_buffer); 690 | jf_exit(JF_EXIT_FAILURE); 691 | } 692 | for (i = 1; i < item->children_count; i++) { 693 | part_item = YAJL_GET_ARRAY(jf_yajl_tree_get_assert(parsed, 694 | ((const char *[]){ "Items", NULL }), 695 | yajl_t_array))->values[i - 1]; 696 | item->children[i] = jf_json_parse_versions(item, 697 | jf_yajl_tree_get_assert(part_item, 698 | ((const char *[]){ "MediaSources", NULL }), 699 | yajl_t_array)); 700 | } 701 | yajl_tree_free(parsed); 702 | } 703 | 704 | // the parent item refers the same part as the first child. for the sake 705 | // of the resume interface, copy playback_ticks from parent to firstborn 706 | item->children[0]->playback_ticks = item->playback_ticks; 707 | } 708 | 709 | 710 | void jf_json_parse_playback_ticks(jf_menu_item *item, const char *payload) 711 | { 712 | yajl_val parsed, ticks; 713 | 714 | JF_JSON_TREE_PARSE_ASSERT((parsed = yajl_tree_parse(payload, s_error_buffer, JF_PARSER_ERROR_BUFFER_SIZE)) != NULL); 715 | ticks = yajl_tree_get(parsed, (const char *[]){ "UserData", "PlaybackPositionTicks", NULL}, yajl_t_number); 716 | if (ticks != NULL) { 717 | item->playback_ticks = YAJL_GET_INTEGER(ticks); 718 | } 719 | yajl_tree_free(parsed); 720 | } 721 | /////////////////////////////////// 722 | 723 | 724 | ////////// MISCELLANEOUS GARBAGE ////////// 725 | char *jf_json_error_string(void) 726 | { 727 | return s_error_buffer; 728 | } 729 | 730 | 731 | static inline yajl_val __jf_yajl_tree_get_assert(const int lineno, 732 | yajl_val parent, 733 | const char **path, 734 | yajl_type type) 735 | { 736 | yajl_val v = yajl_tree_get(parent, path, type); 737 | if (v == NULL) { 738 | const char **curr = path; 739 | fprintf(stderr, "%s:%d: jf_yajl_tree_get_assert failed.\n", __FILE__, lineno); 740 | fprintf(stderr, "FATAL: couldn't find JSON element \""); 741 | while (*curr != NULL) { 742 | fprintf(stderr, ".%s", *curr); 743 | curr++; 744 | } 745 | fprintf(stderr, "\".\n"); 746 | jf_exit(JF_EXIT_FAILURE); 747 | } 748 | return v; 749 | } 750 | 751 | 752 | void jf_json_parse_login_response(const char *payload) 753 | { 754 | yajl_val parsed; 755 | char *tmp; 756 | 757 | JF_JSON_TREE_PARSE_ASSERT((parsed = yajl_tree_parse(payload, 758 | s_error_buffer, 759 | JF_PARSER_ERROR_BUFFER_SIZE)) != NULL); 760 | free(g_options.userid); 761 | assert((tmp = YAJL_GET_STRING(jf_yajl_tree_get_assert(parsed, 762 | ((const char *[]){ "User", "Id", NULL }), 763 | yajl_t_string))) != NULL); 764 | g_options.userid = strdup(tmp); 765 | free(g_options.token); 766 | assert((tmp = YAJL_GET_STRING(jf_yajl_tree_get_assert(parsed, 767 | ((const char *[]){ "AccessToken", NULL }), 768 | yajl_t_string))) != NULL); 769 | g_options.token = strdup(tmp); 770 | yajl_tree_free(parsed); 771 | } 772 | 773 | 774 | void jf_json_parse_system_info_response(const char *payload) 775 | { 776 | yajl_val parsed; 777 | char *tmp, *endptr; 778 | unsigned long major, minor, patch; 779 | 780 | JF_JSON_TREE_PARSE_ASSERT((parsed = yajl_tree_parse(payload, 781 | s_error_buffer, 782 | JF_PARSER_ERROR_BUFFER_SIZE)) != NULL); 783 | assert((tmp = YAJL_GET_STRING(jf_yajl_tree_get_assert(parsed, 784 | ((const char *[]){ "ServerName", NULL }), 785 | yajl_t_string))) != NULL); 786 | g_state.server_name = strdup(tmp); 787 | 788 | assert((tmp = YAJL_GET_STRING(jf_yajl_tree_get_assert(parsed, 789 | ((const char *[]){ "Version", NULL }), 790 | yajl_t_string))) != NULL); 791 | major = strtoul(tmp, &endptr, 10); 792 | assert(endptr != NULL && *endptr == '.'); 793 | tmp = endptr + 1; 794 | minor = strtoul(tmp, &endptr, 10); 795 | assert(endptr != NULL && *endptr == '.'); 796 | tmp = endptr + 1; 797 | patch = strtoul(tmp, &endptr, 10); 798 | assert(endptr != NULL && *endptr == '\0'); 799 | g_state.server_version = JF_SERVER_VERSION_MAKE(major, minor, patch); 800 | 801 | yajl_tree_free(parsed); 802 | } 803 | 804 | 805 | char *jf_json_generate_login_request(const char *username, const char *password) 806 | { 807 | yajl_gen gen; 808 | char *json = NULL; 809 | size_t json_len; 810 | 811 | assert((gen = yajl_gen_alloc(NULL)) != NULL); 812 | assert(yajl_gen_map_open(gen) == yajl_gen_status_ok); 813 | assert(yajl_gen_string(gen, (const unsigned char *)"Username", JF_STATIC_STRLEN("Username")) == yajl_gen_status_ok); 814 | assert(yajl_gen_string(gen, (const unsigned char *)username, strlen(username)) == yajl_gen_status_ok); 815 | assert(yajl_gen_string(gen, (const unsigned char *)"Pw", JF_STATIC_STRLEN("Pw")) == yajl_gen_status_ok); 816 | assert(yajl_gen_string(gen, (const unsigned char *)password, strlen(password)) == yajl_gen_status_ok); 817 | assert(yajl_gen_map_close(gen) == yajl_gen_status_ok); 818 | assert(yajl_gen_get_buf(gen, (const unsigned char **)&json, &json_len) == yajl_gen_status_ok); 819 | assert((json = strndup(json, json_len)) != NULL); 820 | 821 | yajl_gen_free(gen); 822 | return json; 823 | } 824 | 825 | 826 | char *jf_json_generate_progress_post(const char *id, const long long ticks) 827 | { 828 | yajl_gen gen; 829 | char *json = NULL; 830 | size_t json_len; 831 | 832 | assert((gen = yajl_gen_alloc(NULL)) != NULL); 833 | assert(yajl_gen_map_open(gen) == yajl_gen_status_ok); 834 | assert(yajl_gen_string(gen, 835 | (const unsigned char *)"ItemId", 836 | JF_STATIC_STRLEN("ItemId")) == yajl_gen_status_ok); 837 | assert(yajl_gen_string(gen, 838 | (const unsigned char *)id, 839 | JF_ID_LENGTH) == yajl_gen_status_ok); 840 | assert(yajl_gen_string(gen, 841 | (const unsigned char *)"PositionTicks", 842 | JF_STATIC_STRLEN("PositionTicks")) == yajl_gen_status_ok); 843 | assert(yajl_gen_integer(gen, ticks) == yajl_gen_status_ok); 844 | // by default IsPaused is false and the server accrues playback progress 845 | // even if we don't send any update 846 | // that's very stupid because the progress marker can run away if you've 847 | // lost contact with the server 848 | // we do it plex-style and maintain full control 849 | assert(yajl_gen_string(gen, 850 | (const unsigned char *)"IsPaused", 851 | JF_STATIC_STRLEN("IsPaused")) == yajl_gen_status_ok); 852 | assert(yajl_gen_bool(gen, 1) == yajl_gen_status_ok); 853 | assert(yajl_gen_map_close(gen) == yajl_gen_status_ok); 854 | assert(yajl_gen_get_buf(gen, 855 | (const unsigned char **)&json, 856 | &json_len) == yajl_gen_status_ok); 857 | assert((json = strndup(json, json_len)) != NULL); 858 | 859 | yajl_gen_free(gen); 860 | return json; 861 | } 862 | /////////////////////////////////////////// 863 | -------------------------------------------------------------------------------- /src/json.h: -------------------------------------------------------------------------------- 1 | #ifndef _JF_JSON 2 | #define _JF_JSON 3 | 4 | 5 | #include "shared.h" 6 | 7 | #include 8 | #include 9 | 10 | 11 | ////////// CODE MACROS ////////// 12 | #define JF_SAX_BAD_STATE() \ 13 | do { \ 14 | fprintf(stderr, \ 15 | "%s:%d: JF_SAX_BAD_STATE (%u).\n", \ 16 | __FILE__, __LINE__, \ 17 | context->parser_state); \ 18 | fprintf(stderr, "This is a bug.\n"); \ 19 | return 0; \ 20 | } while (false) 21 | 22 | #define JF_SAX_ITEM_FILL(field) \ 23 | do { \ 24 | context->field ## _start = context->parsed_content->used; \ 25 | jf_growing_buffer_append(context->parsed_content, string, string_len); \ 26 | context->field ## _len = string_len; \ 27 | } while (false) 28 | 29 | #define JF_SAX_CONTEXT_PTR_PARSED_DATA_LENGTH(_c) ((_c)->name_len \ 30 | + (_c)->id_len \ 31 | + (_c)->artist_len \ 32 | + (_c)->album_len \ 33 | + (_c)->series_len \ 34 | + (_c)->year_len \ 35 | + (_c)->index_len \ 36 | + (_c)->parent_index_len) 37 | 38 | #define JF_SAX_KEY_IS(name) (JF_STATIC_STRLEN(name) == key_len && strncmp((const char *)key, name, JF_STATIC_STRLEN(name)) == 0) 39 | 40 | #define JF_SAX_STRING_IS(name) (JF_STATIC_STRLEN(name) == string_len && strncmp((const char *)string, name, JF_STATIC_STRLEN(name)) == 0) 41 | 42 | #define JF_SAX_PRINT_LEADER(tag) printf(tag " %zu: ", context->tb->item_count) 43 | 44 | 45 | // NB THIS WILL NOT BE NULL-TERMINATED ON ITS OWN!!! 46 | #define JF_SAX_TRY_APPEND_NAME(prefix, field, suffix) \ 47 | do { \ 48 | if (context->field ## _len > 0) { \ 49 | jf_growing_buffer_append(context->current_item_display_name, \ 50 | prefix, JF_STATIC_STRLEN(prefix)); \ 51 | jf_growing_buffer_append(context->current_item_display_name, \ 52 | context->parsed_content->buf + context->field ## _start, \ 53 | context->field ## _len); \ 54 | jf_growing_buffer_append(context->current_item_display_name, \ 55 | suffix, JF_STATIC_STRLEN(suffix)); \ 56 | } \ 57 | } while (false) 58 | 59 | 60 | #define JF_JSON_TREE_PARSE_ASSERT(_s) \ 61 | do { \ 62 | s_error_buffer[0] = '\0'; \ 63 | bool _success = _s; \ 64 | if (! _success) { \ 65 | fprintf(stderr, "%s:%d: " #_s " failed.\n", __FILE__, __LINE__); \ 66 | fprintf(stderr, "FATAL: yajl_parse error: %s\n", \ 67 | s_error_buffer[0] == '\0' ? "unknown" : s_error_buffer); \ 68 | jf_exit(JF_EXIT_FAILURE); \ 69 | } \ 70 | } while (false) 71 | 72 | #define jf_yajl_tree_get_assert(_parent, _path, _type) __jf_yajl_tree_get_assert(__LINE__, (_parent), (_path), (_type)) 73 | ///////////////////////////////// 74 | 75 | 76 | ////////// SAX PARSER ////////// 77 | typedef enum jf_sax_parser_state { 78 | JF_SAX_NO_STATE = 0, 79 | JF_SAX_IDLE = 1, 80 | JF_SAX_IN_LATEST_ARRAY = 2, 81 | JF_SAX_IN_QUERYRESULT_MAP = 3, 82 | JF_SAX_IN_ITEMS_VALUE = 4, 83 | JF_SAX_IN_ITEMS_ARRAY = 5, 84 | JF_SAX_IN_ITEM_MAP = 6, 85 | JF_SAX_IN_ITEM_TYPE_VALUE = 7, 86 | JF_SAX_IN_ITEM_COLLECTION_TYPE_VALUE = 8, 87 | JF_SAX_IN_ITEM_NAME_VALUE = 9, 88 | JF_SAX_IN_ITEM_ID_VALUE = 10, 89 | JF_SAX_IN_ITEM_ALBUMARTIST_VALUE = 11, 90 | JF_SAX_IN_ITEM_ALBUM_VALUE = 12, 91 | JF_SAX_IN_ITEM_SERIES_VALUE = 13, 92 | JF_SAX_IN_ITEM_YEAR_VALUE = 14, 93 | JF_SAX_IN_ITEM_INDEX_VALUE = 15, 94 | JF_SAX_IN_ITEM_PARENT_INDEX_VALUE = 16, 95 | JF_SAX_IN_ITEM_RUNTIME_TICKS_VALUE = 17, 96 | JF_SAX_IN_ITEM_PATH_VALUE = 18, 97 | JF_SAX_IN_USERDATA_MAP = 19, 98 | JF_SAX_IN_USERDATA_VALUE = 20, 99 | JF_SAX_IN_USERDATA_TICKS_VALUE = 21, 100 | JF_SAX_IGNORE = 127 101 | } jf_sax_parser_state; 102 | 103 | 104 | #define JF_PARSER_ERROR_BUFFER_SIZE 1024 105 | 106 | 107 | typedef struct jf_sax_context { 108 | jf_sax_parser_state parser_state; 109 | jf_sax_parser_state state_to_resume; 110 | size_t maps_ignoring; 111 | size_t arrays_ignoring; 112 | bool latest_array; 113 | jf_thread_buffer *tb; 114 | jf_item_type current_item_type; 115 | jf_growing_buffer current_item_display_name; 116 | jf_growing_buffer current_item_path; 117 | jf_growing_buffer parsed_content; 118 | size_t name_start; size_t name_len; 119 | size_t id_start; size_t id_len; 120 | size_t artist_start; size_t artist_len; 121 | size_t album_start; size_t album_len; 122 | size_t series_start; size_t series_len; 123 | size_t year_start; size_t year_len; 124 | size_t index_start; size_t index_len; 125 | size_t parent_index_start; size_t parent_index_len; 126 | long long runtime_ticks; 127 | long long playback_ticks; 128 | } jf_sax_context; 129 | 130 | 131 | void *jf_json_sax_thread(void *arg); 132 | //////////////////////////////// 133 | 134 | 135 | ////////// VIDEO PARSING ////////// 136 | void jf_json_parse_video(jf_menu_item *item, const char *video, const char *additional_parts); 137 | void jf_json_parse_playback_ticks(jf_menu_item *item, const char *payload); 138 | /////////////////////////////////// 139 | 140 | 141 | ////////// MISCELLANEOUS GARBAGE ////////// 142 | char *jf_json_error_string(void); 143 | void jf_json_parse_login_response(const char *payload); 144 | void jf_json_parse_system_info_response(const char *payload); 145 | char *jf_json_generate_login_request(const char *username, const char *password); 146 | char *jf_json_generate_progress_post(const char *id, const long long ticks); 147 | /////////////////////////////////////////// 148 | #endif 149 | -------------------------------------------------------------------------------- /src/linenoise.h: -------------------------------------------------------------------------------- 1 | /* linenoise.h -- VERSION 1.0 2 | * 3 | * Guerrilla line editing library against the idea that a line editing lib 4 | * needs to be 20,000 lines of C code. 5 | * 6 | * See linenoise.c for more information. 7 | * 8 | * ------------------------------------------------------------------------ 9 | * 10 | * Copyright (c) 2010-2014, Salvatore Sanfilippo 11 | * Copyright (c) 2010-2013, Pieter Noordhuis 12 | * 13 | * All rights reserved. 14 | * 15 | * Redistribution and use in source and binary forms, with or without 16 | * modification, are permitted provided that the following conditions are 17 | * met: 18 | * 19 | * * Redistributions of source code must retain the above copyright 20 | * notice, this list of conditions and the following disclaimer. 21 | * 22 | * * Redistributions in binary form must reproduce the above copyright 23 | * notice, this list of conditions and the following disclaimer in the 24 | * documentation and/or other materials provided with the distribution. 25 | * 26 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 27 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 28 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 29 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 30 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 31 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 32 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 33 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 34 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 35 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 36 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 37 | */ 38 | 39 | #ifndef __LINENOISE_H 40 | #define __LINENOISE_H 41 | 42 | #ifdef __cplusplus 43 | extern "C" { 44 | #endif 45 | 46 | typedef struct linenoiseCompletions { 47 | size_t len; 48 | char **cvec; 49 | } linenoiseCompletions; 50 | 51 | typedef void(linenoiseCompletionCallback)(const char *, linenoiseCompletions *); 52 | typedef char*(linenoiseHintsCallback)(const char *, int *color, int *bold); 53 | typedef void(linenoiseFreeHintsCallback)(void *); 54 | void linenoiseSetCompletionCallback(linenoiseCompletionCallback *); 55 | void linenoiseSetHintsCallback(linenoiseHintsCallback *); 56 | void linenoiseSetFreeHintsCallback(linenoiseFreeHintsCallback *); 57 | void linenoiseAddCompletion(linenoiseCompletions *, const char *); 58 | 59 | char *linenoise(const char *prompt); 60 | void linenoiseFree(void *ptr); 61 | int linenoiseHistoryAdd(const char *line); 62 | int linenoiseHistorySetMaxLen(int len); 63 | int linenoiseHistorySave(const char *filename); 64 | int linenoiseHistoryLoad(const char *filename); 65 | void linenoiseClearScreen(void); 66 | void linenoiseSetMultiLine(int ml); 67 | void linenoisePrintKeyCodes(void); 68 | void linenoiseMaskModeEnable(void); 69 | void linenoiseMaskModeDisable(void); 70 | 71 | #ifdef __cplusplus 72 | } 73 | #endif 74 | 75 | #endif /* __LINENOISE_H */ 76 | -------------------------------------------------------------------------------- /src/main.c: -------------------------------------------------------------------------------- 1 | #include "shared.h" 2 | #include "net.h" 3 | #include "json.h" 4 | #include "config.h" 5 | #include "disk.h" 6 | #include "playback.h" 7 | #include "menu.h" 8 | 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | 23 | ////////// GLOBAL VARIABLES ////////// 24 | jf_options g_options; 25 | jf_global_state g_state; 26 | mpv_handle *g_mpv_ctx = NULL; 27 | ////////////////////////////////////// 28 | 29 | 30 | ////////// STATIC FUNCTIONS ////////// 31 | static void jf_print_usage(void); 32 | static inline void jf_missing_arg(const char *arg); 33 | static inline void jf_mpv_event_dispatch(const mpv_event *event); 34 | ////////////////////////////////////// 35 | 36 | 37 | ////////// PROGRAM TERMINATION ////////// 38 | // Note: the signature and description of this function are in shared.h 39 | void jf_exit(int sig) 40 | { 41 | // some of this is not async-signal-safe 42 | // but what's the worst that can happen, a crash? :^) 43 | g_state.state = sig == JF_EXIT_SUCCESS ? JF_STATE_USER_QUIT : JF_STATE_FAIL; 44 | if (sig == SIGABRT) { 45 | perror("FATAL"); 46 | } 47 | jf_net_clear(); 48 | mpv_terminate_destroy(g_mpv_ctx); 49 | _exit(sig == JF_EXIT_SUCCESS ? EXIT_SUCCESS : EXIT_FAILURE); 50 | } 51 | ///////////////////////////////////////// 52 | 53 | 54 | ////////// STARTUP STUFF ////////// 55 | static void jf_print_usage(void) { 56 | printf("Usage:\n"); 57 | printf("\t--help\n"); 58 | printf("\t--version\n"); 59 | printf("\t--config-dir (default: $XDG_CONFIG_HOME/jftui)\n"); 60 | printf("\t--login.\n"); 61 | printf("\t--no-check-updates\n"); 62 | } 63 | 64 | 65 | static inline void jf_missing_arg(const char *arg) 66 | { 67 | fprintf(stderr, "FATAL: missing parameter for argument %s\n", arg); 68 | jf_print_usage(); 69 | } 70 | /////////////////////////////////// 71 | 72 | 73 | ////////// MISCELLANEOUS GARBAGE ////////// 74 | static inline void jf_mpv_event_dispatch(const mpv_event *event) 75 | { 76 | int64_t playback_ticks; 77 | mpv_node *node; 78 | 79 | JF_DEBUG_PRINTF("state: %d, event: %s\n", g_state.state, mpv_event_name(event->event_id)); 80 | switch (event->event_id) { 81 | case MPV_EVENT_CLIENT_MESSAGE: 82 | // playlist controls 83 | if (((mpv_event_client_message *)event->data)->num_args > 0) { 84 | if (strcmp(((mpv_event_client_message *)event->data)->args[0], 85 | "jftui-playlist-next") == 0) { 86 | jf_playback_update_stopped(g_state.now_playing->playback_ticks); 87 | jf_playback_next(); 88 | } else if (strcmp(((mpv_event_client_message *)event->data)->args[0], 89 | "jftui-playlist-prev") == 0) { 90 | jf_playback_update_stopped(g_state.now_playing->playback_ticks); 91 | jf_playback_previous(); 92 | } else if (strcmp(((mpv_event_client_message *)event->data)->args[0], 93 | "jftui-playlist-print") == 0) { 94 | jf_playback_print_playlist(0); 95 | } else if (strcmp(((mpv_event_client_message *)event->data)->args[0], 96 | "jftui-playlist-shuffle") == 0) { 97 | jf_playback_shuffle_playlist(); 98 | } 99 | } 100 | break; 101 | case MPV_EVENT_START_FILE: 102 | jf_playback_load_external_subtitles(); 103 | // if we're issuing playlist_next/prev very quickly, mpv will not 104 | // go into idle mode at all 105 | // in those cases, we digest the INIT state here 106 | if (g_state.state == JF_STATE_PLAYBACK_INIT || g_state.state == JF_STATE_PLAYLIST_SEEKING) { 107 | g_state.state = JF_STATE_PLAYBACK; 108 | } 109 | // open the playback session 110 | jf_playback_update_playing(g_state.now_playing->playback_ticks); 111 | break; 112 | case MPV_EVENT_END_FILE: 113 | // tell server file playback stopped so it won't keep accruing progress 114 | // EXCEPT in a playlist skip, which has already handled it 115 | // because by now g_state.now_playing is already the new item 116 | // (when the user triggers a playlist skip, we get the END_FILE after 117 | // the input was processed and the loadfile command was issued) 118 | if (g_state.state != JF_STATE_PLAYBACK_START_MARK && g_state.state != JF_STATE_PLAYLIST_SEEKING) { 119 | playback_ticks = 120 | mpv_get_property(g_mpv_ctx, "time-pos", MPV_FORMAT_INT64, &playback_ticks) == 0 ? 121 | JF_SECS_TO_TICKS(playback_ticks) : g_state.now_playing->playback_ticks; 122 | jf_playback_update_stopped(playback_ticks); 123 | } 124 | 125 | // why did we get an END_FILE? 126 | if (((mpv_event_end_file *)event->data)->reason != MPV_END_FILE_REASON_EOF 127 | || jf_playback_next() == false) { 128 | // if not EOF, it was an abnormal stop and the engine will abort 129 | // if EOF, let's try moving ahead in the playlist: oh, we found nothing more 130 | // so the engine will also abort 131 | g_state.state = JF_STATE_PLAYBACK_STOPPING; 132 | } 133 | // otherwise we're skipping ahead in the playlist 134 | break; 135 | case MPV_EVENT_SEEK: 136 | // syncing to user progress marker 137 | if (g_state.state == JF_STATE_PLAYBACK_START_MARK) { 138 | JF_MPV_ASSERT(mpv_set_property_string(g_mpv_ctx, "start", "none")); 139 | // ensure parent playback ticks refer to merged item 140 | playback_ticks = 141 | mpv_get_property(g_mpv_ctx, "time-pos", MPV_FORMAT_INT64, &playback_ticks) == 0 ? 142 | JF_SECS_TO_TICKS(playback_ticks) : 0; 143 | g_state.now_playing->playback_ticks = playback_ticks; 144 | // open playback session 145 | jf_playback_update_playing(g_state.now_playing->playback_ticks); 146 | g_state.state = JF_STATE_PLAYBACK; 147 | break; 148 | } 149 | // no need to update progress as a time-pos event gets fired 150 | // immediately after 151 | break; 152 | case MPV_EVENT_PROPERTY_CHANGE: 153 | JF_DEBUG_PRINTF("\tproperty: %s\n", ((mpv_event_property *)event->data)->name); 154 | if (((mpv_event_property *)event->data)->format == MPV_FORMAT_NONE) break; 155 | if (strcmp("time-pos", ((mpv_event_property *)event->data)->name) == 0) { 156 | // event valid, check if need to update the server 157 | playback_ticks = JF_SECS_TO_TICKS(*(int64_t *)((mpv_event_property *)event->data)->data); 158 | if (llabs(playback_ticks - g_state.now_playing->playback_ticks) < JF_SECS_TO_TICKS(10)) break; 159 | // good for update; from jf 10.10.7 this no longer automatically starts a playback session 160 | jf_playback_update_progress(playback_ticks); 161 | } else if (strcmp("sid", ((mpv_event_property *)event->data)->name) == 0) { 162 | // subtitle track change, go and see if we need to align for split-part 163 | jf_playback_align_subtitle(*(int64_t *)((mpv_event_property *)event->data)->data); 164 | } else if (strcmp("options/loop-playlist", ((mpv_event_property *)event->data)->name) == 0) { 165 | if (g_state.loop_state == JF_LOOP_STATE_RESYNCING) { 166 | g_state.loop_state = JF_LOOP_STATE_IN_SYNC; 167 | break; 168 | } 169 | if (g_state.loop_state == JF_LOOP_STATE_OUT_OF_SYNC) { 170 | // we're digesting a decrement caused by an EOF 171 | // mid-jftui playlist 172 | JF_MPV_ASSERT(mpv_set_property(g_mpv_ctx, 173 | "options/loop-playlist", 174 | MPV_FORMAT_INT64, 175 | &g_state.playlist_loops)); 176 | g_state.loop_state = JF_LOOP_STATE_RESYNCING; 177 | break; 178 | } 179 | // the loop counter is in sync, this means the property change 180 | // is user-triggered and we should abide by it 181 | node = (((mpv_event_property *)event->data)->data); 182 | switch (node->format) { 183 | case MPV_FORMAT_FLAG: 184 | // "no" 185 | g_state.playlist_loops = 0; 186 | break; 187 | case MPV_FORMAT_INT64: 188 | // a (guaranteed positive) numeral 189 | g_state.playlist_loops = node->u.int64; 190 | break; 191 | case MPV_FORMAT_STRING: 192 | // "yes", "inf" or "force", which we treat the same 193 | g_state.playlist_loops = -1; 194 | break; 195 | default: 196 | ; 197 | } 198 | g_state.loop_state = JF_LOOP_STATE_IN_SYNC; 199 | } 200 | break; 201 | case MPV_EVENT_IDLE: 202 | switch (g_state.state) { 203 | case JF_STATE_PLAYBACK_START_MARK: 204 | // going too quick: do nothing but wait for the SEEK 205 | break; 206 | case JF_STATE_PLAYBACK_INIT: 207 | // normal: open playback session and digest state transition 208 | // jf_playback_update_playing(g_state.now_playing->playback_ticks); 209 | g_state.state = JF_STATE_PLAYBACK; 210 | break; 211 | case JF_STATE_PLAYBACK: 212 | // nothing left to play: leave 213 | jf_playback_end(); 214 | break; 215 | default: 216 | fprintf(stderr, 217 | "Warning: received MPV_EVENT_IDLE under global state %d. This is a bug.\n", 218 | g_state.state); 219 | } 220 | break; 221 | case MPV_EVENT_SHUTDOWN: 222 | // in case we're aborting abnormally, we likely skipped the MPV_EVENT_END_FILE 223 | // so we must tell from here to Jellyfin that playback stopped 224 | if (g_state.state != JF_STATE_PLAYBACK_STOPPING && g_state.now_playing != NULL) { 225 | // NB we can't call mpv_get_property because mpv core has aborted 226 | jf_playback_update_stopped(g_state.now_playing->playback_ticks); 227 | } 228 | jf_playback_end(); 229 | break; 230 | default: 231 | // no-op on everything else 232 | break; 233 | } 234 | } 235 | /////////////////////////////////////////// 236 | 237 | 238 | ////////// MAIN LOOP ////////// 239 | int main(int argc, char *argv[]) 240 | { 241 | // VARIABLES 242 | int i; 243 | char *config_path; 244 | jf_reply *reply, *reply_alt; 245 | 246 | 247 | // SIGNAL HANDLERS 248 | { 249 | struct sigaction sa; 250 | sa.sa_handler = jf_exit; 251 | sigemptyset(&sa.sa_mask); 252 | sa.sa_flags = 0; 253 | sa.sa_sigaction = NULL; 254 | assert(sigaction(SIGABRT, &sa, NULL) == 0); 255 | assert(sigaction(SIGINT, &sa, NULL) == 0); 256 | // for the sake of multithreaded libcurl 257 | sa.sa_handler = SIG_IGN; 258 | assert(sigaction(SIGPIPE, &sa, NULL) == 0); 259 | } 260 | ////////////////// 261 | 262 | 263 | // LIBMPV VERSION CHECK 264 | // required for "osc" option 265 | { 266 | unsigned long mpv_version = mpv_client_api_version(); 267 | if (mpv_version < MPV_MAKE_VERSION(1,24)) { 268 | fprintf(stderr, 269 | "FATAL: found libmpv version %lu.%lu, but 1.24 or greater is required.\n", 270 | mpv_version >> 16, mpv_version & 0xFFFF); 271 | jf_exit(JF_EXIT_FAILURE); 272 | } 273 | if (mpv_version != MPV_CLIENT_API_VERSION) { 274 | fprintf(stderr, 275 | "FATAL: found libmpv version %lu.%lu, but jftui was compiled against %lu.%lu. Please recompile the program.\n", 276 | mpv_version >> 16, mpv_version & 0xFFFF, 277 | MPV_CLIENT_API_VERSION >> 16, MPV_CLIENT_API_VERSION & 0xFFFF); 278 | jf_exit(JF_EXIT_FAILURE); 279 | } 280 | // future proofing 281 | if (mpv_version >= MPV_MAKE_VERSION(3,0)) { 282 | fprintf(stderr, 283 | "Warning: found libmpv version %lu.%lu, but jftui expects 1.xx or 2.xx. mpv will probably not work.\n", 284 | mpv_version >> 16, mpv_version & 0xFFFF); 285 | } 286 | } 287 | /////////////////////// 288 | 289 | 290 | // SETUP OPTIONS 291 | jf_options_init(); 292 | //////////////// 293 | 294 | 295 | // SETUP GLOBAL STATE 296 | srandom((unsigned)time(NULL)); 297 | g_state = (jf_global_state){ 0 }; 298 | assert((g_state.session_id = jf_generate_random_id(0)) != NULL); 299 | ///////////////////// 300 | 301 | 302 | // COMMAND LINE ARGUMENTS 303 | i = 0; 304 | while (++i < argc) { 305 | if (strcmp(argv[i], "--help") == 0) { 306 | jf_print_usage(); 307 | jf_exit(JF_EXIT_SUCCESS); 308 | } else if (strcmp(argv[i], "--config-dir") == 0) { 309 | if (++i >= argc) { 310 | jf_missing_arg("--config-dir"); 311 | jf_exit(JF_EXIT_FAILURE); 312 | } 313 | assert((g_state.config_dir = strdup(argv[i])) != NULL); 314 | } else if (strcmp(argv[i], "--login") == 0) { 315 | g_state.state = JF_STATE_STARTING_LOGIN; 316 | } else if (strcmp(argv[i], "--no-check-updates") == 0) { 317 | g_options.check_updates = false; 318 | } else if (strcmp(argv[i], "--version") == 0) { 319 | printf("jftui %s, libmpv %lu.%lu, libcurl %s %s, yajl %d\n", 320 | g_options.version, 321 | mpv_client_api_version() >> 16, 322 | mpv_client_api_version() & 0xFFFF, 323 | curl_version_info(CURLVERSION_NOW)->version, 324 | curl_version_info(CURLVERSION_NOW)->ssl_version, 325 | yajl_version()); 326 | jf_exit(JF_EXIT_SUCCESS); 327 | } else { 328 | fprintf(stderr, "FATAL: unrecognized argument %s.\n", argv[i]); 329 | jf_print_usage(); 330 | jf_exit(JF_EXIT_FAILURE); 331 | } 332 | } 333 | ///////////////////////// 334 | 335 | 336 | // SETUP DISK 337 | jf_disk_init(); 338 | ///////////// 339 | 340 | 341 | // READ AND PARSE CONFIGURATION FILE 342 | // apply config directory location default unless there was user override 343 | if (g_state.config_dir == NULL 344 | && (g_state.config_dir = jf_config_get_default_dir()) == NULL) { 345 | fprintf(stderr, "FATAL: could not acquire configuration directory location. $HOME could not be read and --config-dir was not passed.\n"); 346 | jf_exit(JF_EXIT_FAILURE); 347 | } 348 | // get expected location of config file 349 | config_path = jf_concat(2, g_state.config_dir, "/settings"); 350 | 351 | // check config file exists 352 | if (jf_disk_is_file_accessible(config_path)) { 353 | // it's there: read it 354 | jf_config_read(config_path); 355 | if (strcmp(g_options.version, JF_VERSION) < 0) { 356 | printf("Attention: jftui was updated from the last time it was run. Check the changelog on Github.\n"); 357 | free(g_options.version); 358 | assert((g_options.version = strdup(JF_VERSION)) != NULL); 359 | } 360 | // if fundamental fields are missing (file corrupted for some reason) 361 | if (g_options.server == NULL 362 | || g_options.userid == NULL 363 | || g_options.token == NULL) { 364 | if (! jf_menu_user_ask_yn("Error: settings file missing fundamental fields. Would you like to go through manual configuration?")) { 365 | jf_exit(JF_EXIT_SUCCESS); 366 | } 367 | free(g_options.server); 368 | free(g_options.userid); 369 | free(g_options.token); 370 | g_state.state = JF_STATE_STARTING_FULL_CONFIG; 371 | } 372 | } else if (errno == ENOENT) { 373 | // it's not there 374 | if (! jf_menu_user_ask_yn("Settings file not found. Would you like to configure jftui?")) { 375 | jf_exit(JF_EXIT_SUCCESS); 376 | } 377 | g_state.state = JF_STATE_STARTING_FULL_CONFIG; 378 | } else { 379 | fprintf(stderr, "FATAL: access for settings file at location %s: %s.\n", 380 | config_path, strerror(errno)); 381 | jf_exit(JF_EXIT_FAILURE); 382 | } 383 | //////////////////////////////////// 384 | 385 | 386 | // UPDATE CHECK 387 | // it runs asynchronously while we do other stuff 388 | if (g_options.check_updates) { 389 | reply_alt = jf_net_request(NULL, JF_REQUEST_CHECK_UPDATE, JF_HTTP_GET, NULL); 390 | } 391 | /////////////// 392 | 393 | 394 | // INTERACTIVE CONFIG 395 | if (g_state.state == JF_STATE_STARTING_FULL_CONFIG) { 396 | jf_config_ask_user(); 397 | } else if (g_state.state == JF_STATE_STARTING_LOGIN) { 398 | jf_config_ask_user_login(); 399 | } 400 | 401 | // save to disk 402 | if (g_state.state == JF_STATE_STARTING_FULL_CONFIG 403 | || g_state.state == JF_STATE_STARTING_LOGIN) { 404 | if (jf_config_write(config_path)) { 405 | printf("Please restart to apply the new settings.\n"); 406 | jf_exit(JF_EXIT_SUCCESS); 407 | } else { 408 | fprintf(stderr, "FATAL: Configuration failed.\n"); 409 | jf_exit(JF_EXIT_FAILURE); 410 | } 411 | } else { 412 | // we don't consider a failure to save config fatal during normal startup 413 | jf_config_write(config_path); 414 | free(config_path); 415 | } 416 | ///////////////////// 417 | 418 | 419 | // SERVER NAME AND VERSION 420 | // this doubles up as a check for connectivity and correct login parameters 421 | reply = jf_net_request("/system/info", JF_REQUEST_IN_MEMORY, JF_HTTP_GET, NULL); 422 | if (JF_REPLY_PTR_HAS_ERROR(reply)) { 423 | fprintf(stderr, "FATAL: could not reach server: %s.\n", jf_reply_error_string(reply)); 424 | jf_exit(JF_EXIT_FAILURE); 425 | } 426 | jf_json_parse_system_info_response(reply->payload); 427 | jf_reply_free(reply); 428 | ////////////// 429 | 430 | 431 | // SETUP MENU 432 | jf_menu_init(); 433 | ///////////////// 434 | 435 | 436 | // SETUP MPV 437 | if (setlocale(LC_NUMERIC, "C") == NULL) { 438 | fprintf(stderr, "Warning: could not set numeric locale to sane standard. mpv might refuse to work.\n"); 439 | } 440 | //////////// 441 | 442 | 443 | // resolve update check 444 | if (g_options.check_updates) { 445 | jf_net_await(reply_alt); 446 | if (JF_REPLY_PTR_HAS_ERROR(reply_alt)) { 447 | fprintf(stderr, "Warning: could not fetch latest version info: %s.\n", 448 | jf_reply_error_string(reply_alt)); 449 | } else if (strcmp(JF_VERSION, reply_alt->payload) < 0) { 450 | printf("Attention: jftui v%s is available for update.\n", 451 | reply_alt->payload); 452 | } 453 | jf_reply_free(reply_alt); 454 | } 455 | /////////////////////// 456 | 457 | 458 | ////////// MAIN LOOP ////////// 459 | while (true) { 460 | switch (g_state.state) { 461 | case JF_STATE_STARTING: 462 | case JF_STATE_STARTING_FULL_CONFIG: 463 | case JF_STATE_STARTING_LOGIN: 464 | g_state.state = JF_STATE_MENU_UI; 465 | // no reason to break 466 | case JF_STATE_MENU_UI: 467 | jf_menu_ui(); 468 | break; 469 | case JF_STATE_PLAYBACK: 470 | case JF_STATE_PLAYBACK_INIT: 471 | case JF_STATE_PLAYBACK_START_MARK: 472 | case JF_STATE_PLAYLIST_SEEKING: 473 | case JF_STATE_PLAYBACK_STOPPING: 474 | jf_mpv_event_dispatch(mpv_wait_event(g_mpv_ctx, -1)); 475 | break; 476 | case JF_STATE_USER_QUIT: 477 | jf_exit(JF_EXIT_SUCCESS); 478 | break; 479 | case JF_STATE_FAIL: 480 | jf_exit(JF_EXIT_FAILURE); 481 | break; 482 | } 483 | } 484 | /////////////////////////////// 485 | 486 | 487 | // never reached 488 | jf_exit(JF_EXIT_SUCCESS); 489 | } 490 | /////////////////////////////// 491 | -------------------------------------------------------------------------------- /src/menu.h: -------------------------------------------------------------------------------- 1 | #ifndef _JF_MENU 2 | #define _JF_MENU 3 | 4 | 5 | #include "shared.h" 6 | 7 | #include 8 | 9 | 10 | ////////// CODE MACROS ////////// 11 | #define JF_FILTER_URL_APPEND(_f, _s) \ 12 | if (s_filters & (_f)) { \ 13 | s_filters_print_string[s_filters_print_len] = ' '; \ 14 | s_filters_print_len++; \ 15 | if (first_filter == false) { \ 16 | s_filters_query_string[s_filters_len] = ','; \ 17 | s_filters_len++; \ 18 | s_filters_print_string[s_filters_print_len] = ','; \ 19 | s_filters_print_len++; \ 20 | } \ 21 | strncpy(s_filters_query_string + s_filters_len, \ 22 | (_s), \ 23 | JF_STATIC_STRLEN((_s))); \ 24 | s_filters_len += JF_STATIC_STRLEN((_s)); \ 25 | first_filter = false; \ 26 | strncpy(s_filters_print_string + s_filters_print_len, \ 27 | (_s), \ 28 | JF_STATIC_STRLEN((_s))); \ 29 | s_filters_print_len += JF_STATIC_STRLEN((_s)); \ 30 | first_filter = false; \ 31 | } 32 | ///////////////////////////////// 33 | 34 | 35 | ////////// QUERY FILTERS ////////// 36 | typedef uint8_t jf_filter_mask; 37 | 38 | typedef enum jf_filter { 39 | JF_FILTER_NONE = 0, 40 | JF_FILTER_IS_PLAYED = 1 << 0, 41 | JF_FILTER_IS_UNPLAYED = 1 << 1, 42 | JF_FILTER_RESUMABLE = 1 << 2, 43 | JF_FILTER_FAVORITE = 1 << 3, // blasted american english 44 | JF_FILTER_LIKES = 1 << 4, 45 | JF_FILTER_DISLIKES = 1 << 5 46 | } jf_filter; 47 | 48 | 49 | void jf_menu_filters_clear(void); 50 | bool jf_menu_filters_add(const enum jf_filter filter); 51 | /////////////////////////////////// 52 | 53 | 54 | ////////// PLAYED STATUS & favoriteS ////////// 55 | #define JF_FLAG_CHANGE_REQUESTS_LEN (JF_NET_ASYNC_THREADS * 4) 56 | 57 | typedef enum jf_flag_type { 58 | JF_FLAG_TYPE_PLAYED = 0, 59 | JF_FLAG_TYPE_FAVORITE = 1 60 | } jf_flag_type; 61 | 62 | void jf_menu_child_set_flag(const size_t n, const jf_flag_type flag_type, const bool flag_status); 63 | void jf_menu_item_set_flag_detach(const jf_menu_item *item, const jf_flag_type flag_type, const bool flag_status); 64 | void jf_menu_item_set_flag_await_all(void); 65 | /////////////////////////////////// 66 | 67 | 68 | ////////// JF_MENU_STACK ////////// 69 | typedef struct jf_menu_stack { 70 | jf_menu_item **items; 71 | size_t size; 72 | size_t used; 73 | } jf_menu_stack; 74 | /////////////////////////////////// 75 | 76 | 77 | ////////// USER INTERFACE LOOP ////////// 78 | jf_item_type jf_menu_child_get_type(size_t n); 79 | size_t jf_menu_child_count(void); 80 | bool jf_menu_child_dispatch(const size_t n); 81 | 82 | void jf_menu_help(void); 83 | 84 | void jf_menu_dotdot(void); 85 | void jf_menu_quit(void); 86 | void jf_menu_search(const char *s); 87 | 88 | void jf_menu_ui(void); 89 | ///////////////////////////////////////// 90 | 91 | 92 | ////////// AGNOSTIC USER PROMPTS ////////// 93 | enum jf_ync { 94 | JF_YNC_YES, 95 | JF_YNC_NO, 96 | JF_YNC_CANCEL 97 | }; 98 | 99 | 100 | // Prompts user for a question meant for a binary answer and reads reply from 101 | // stdin. 102 | // 103 | // Returns: 104 | // - true if reply starts with 'y' or 'Y'; 105 | // - false if reply starts with 'n' or 'N'. 106 | // CAN'T FAIL. 107 | bool jf_menu_user_ask_yn(const char *question); 108 | 109 | 110 | // Prompts user for a question meant for a yes/no/cancel answer and reads reply 111 | // from stdin. 112 | // 113 | // Returns: 114 | // - JF_YNC_YES if reply starts with 'y' or 'Y'; 115 | // - JF_YNC_NO if reply starts with 'n' or 'N'. 116 | // - JF_YNC_CANCEL if reply starts with 'c' or 'C'. 117 | // CAN'T FAIL. 118 | enum jf_ync jf_menu_user_ask_ync(const char *question); 119 | 120 | 121 | size_t jf_menu_user_ask_selection(const char *prompt_preamble, const size_t l, const size_t r); 122 | /////////////////////////////////////////// 123 | 124 | 125 | ////////// MISCELLANEOUS ////////// 126 | char *jf_menu_item_get_request_url(const jf_menu_item *item); 127 | bool jf_menu_ask_resume(jf_menu_item *item); 128 | 129 | 130 | // Initializes linenoise history and the static menu stack struct. 131 | // CAN FATAL. 132 | void jf_menu_init(void); 133 | 134 | 135 | // Clears the contents of the static menu stack, forcibly deallocating all items 136 | // regardless of their persistency bit. 137 | // CAN'T FAIL. 138 | void jf_menu_clear(void); 139 | 140 | 141 | // Wrapper. Takes care of Ctrl-C (SIGINT) and other IO errors. 142 | // CAN FATAL. 143 | char *jf_menu_linenoise(const char *prompt); 144 | /////////////////////////////////// 145 | #endif 146 | -------------------------------------------------------------------------------- /src/mpv.c: -------------------------------------------------------------------------------- 1 | #include "mpv.h" 2 | #include "shared.h" 3 | #include "config.h" 4 | #include "disk.h" 5 | 6 | #include 7 | #include 8 | 9 | 10 | ////////// GLOBAL VARIABLES ////////// 11 | extern jf_global_state g_state; 12 | extern jf_options g_options; 13 | extern mpv_handle *g_mpv_ctx; 14 | ////////////////////////////////////// 15 | 16 | 17 | ////////// STATIC VARIABLES ////////// 18 | static int mpv_flag_yes = 1; 19 | static int mpv_flag_no = 0; 20 | ////////////////////////////////////// 21 | 22 | 23 | ////////// STATIC FUNCTIONS ////////// 24 | static void jf_mpv_init_cache_dirs(mpv_handle *mpv_ctx); 25 | ////////////////////////////////////// 26 | 27 | 28 | // acrobatically guess the default mpv cache dir and point as many cache 29 | // dir properties there as possible (except for the demux cache) 30 | // (this code is possibly in breach of GPL. sue me) 31 | // CAN FATAL. 32 | #if MPV_CLIENT_API_VERSION >= MPV_MAKE_VERSION(2,1) 33 | static void jf_mpv_init_cache_dirs(mpv_handle *mpv_ctx) 34 | { 35 | char *icc_cache_dir; 36 | char *gpu_shader_cache_dir; 37 | 38 | if (g_state.mpv_cache_dir == NULL) { 39 | char *home = getenv("HOME"); 40 | char *xdg_config = getenv("XDG_CONFIG_HOME"); 41 | 42 | char *old_mpv_home = NULL; 43 | char *mpv_home = NULL; 44 | 45 | // Maintain compatibility with old ~/.mpv 46 | if (home && home[0]) { 47 | old_mpv_home = jf_concat(2, home, "/.mpv"); 48 | } 49 | 50 | if (xdg_config && xdg_config[0]) { 51 | mpv_home = jf_concat(2, xdg_config, "/mpv"); 52 | } else if (home && home[0]) { 53 | mpv_home = jf_concat(2, home, "/.config/mpv"); 54 | } 55 | 56 | // If the old ~/.mpv exists, and the XDG config dir doesn't, use the old 57 | // config dir only. Also do not use any other XDG directories. 58 | if (jf_disk_is_file_accessible(old_mpv_home) && !jf_disk_is_file_accessible(mpv_home)) { 59 | g_state.mpv_cache_dir = old_mpv_home; 60 | } else { 61 | char *xdg_cache = getenv("XDG_CACHE_HOME"); 62 | 63 | if (xdg_cache && xdg_cache[0]) { 64 | g_state.mpv_cache_dir = jf_concat(2, xdg_cache, "/mpv"); 65 | } else if (home && home[0]) { 66 | g_state.mpv_cache_dir = jf_concat(2, home, "/.cache/mpv"); 67 | } 68 | } 69 | 70 | free(old_mpv_home); 71 | free(mpv_home); 72 | } 73 | 74 | // read property beforehand and honour user preference 75 | JF_MPV_ASSERT(mpv_get_property(mpv_ctx, "icc-cache-dir", MPV_FORMAT_STRING, &icc_cache_dir)); 76 | if (icc_cache_dir == NULL || icc_cache_dir[0] == '\0') { 77 | JF_MPV_ASSERT(JF_MPV_SET_OPTPROP(mpv_ctx, "icc-cache-dir", MPV_FORMAT_STRING, &g_state.mpv_cache_dir)); 78 | } 79 | 80 | JF_MPV_ASSERT(mpv_get_property(mpv_ctx, "gpu-shader-cache-dir", MPV_FORMAT_STRING, &gpu_shader_cache_dir)); 81 | if (gpu_shader_cache_dir == NULL || gpu_shader_cache_dir[0] == '\0') { 82 | JF_MPV_ASSERT(JF_MPV_SET_OPTPROP(mpv_ctx, "gpu-shader-cache-dir", MPV_FORMAT_STRING, &g_state.mpv_cache_dir)); 83 | } 84 | 85 | // let's make a macro if these ever become 3+... 86 | } 87 | #endif 88 | 89 | 90 | mpv_handle *jf_mpv_create(void) 91 | { 92 | mpv_handle *mpv_ctx; 93 | char *x_emby_token; 94 | 95 | // init mpv core 96 | assert((mpv_ctx = mpv_create()) != NULL); 97 | JF_MPV_ASSERT(JF_MPV_SET_OPTPROP(mpv_ctx, "config-dir", MPV_FORMAT_STRING, &g_state.config_dir)); 98 | JF_MPV_ASSERT(JF_MPV_SET_OPTPROP(mpv_ctx, "config", MPV_FORMAT_FLAG, &mpv_flag_yes)); 99 | JF_MPV_ASSERT(JF_MPV_SET_OPTPROP(mpv_ctx, "osc", MPV_FORMAT_FLAG, &mpv_flag_yes)); 100 | JF_MPV_ASSERT(JF_MPV_SET_OPTPROP(mpv_ctx, "input-default-bindings", MPV_FORMAT_FLAG, &mpv_flag_yes)); 101 | JF_MPV_ASSERT(JF_MPV_SET_OPTPROP(mpv_ctx, "input-vo-keyboard", MPV_FORMAT_FLAG, &mpv_flag_yes)); 102 | JF_MPV_ASSERT(JF_MPV_SET_OPTPROP(mpv_ctx, "input-terminal", MPV_FORMAT_FLAG, &mpv_flag_yes)); 103 | assert((x_emby_token = jf_concat(2, "x-emby-token: ", g_options.token)) != NULL); 104 | JF_MPV_ASSERT(JF_MPV_SET_OPTPROP_STRING(mpv_ctx, "http-header-fields", x_emby_token)); 105 | free(x_emby_token); 106 | JF_MPV_ASSERT(mpv_observe_property(mpv_ctx, 0, "time-pos", MPV_FORMAT_INT64)); 107 | JF_MPV_ASSERT(mpv_observe_property(mpv_ctx, 0, "sid", MPV_FORMAT_INT64)); 108 | JF_MPV_ASSERT(mpv_observe_property(mpv_ctx, 0, "options/loop-playlist", MPV_FORMAT_NODE)); 109 | 110 | JF_MPV_ASSERT(mpv_initialize(mpv_ctx)); 111 | 112 | // profile must be applied as a command 113 | if (g_options.mpv_profile != NULL) { 114 | const char *apply_profile[] = { "apply-profile", g_options.mpv_profile, NULL }; 115 | if (mpv_command(mpv_ctx, apply_profile) < 0) { 116 | fprintf(stderr, 117 | "FATAL: could not apply mpv profile \"%s\". Are you sure it exists in mpv.conf?\n", 118 | g_options.mpv_profile); 119 | jf_exit(JF_EXIT_FAILURE); 120 | } 121 | } 122 | 123 | // cache dirs may be set by user in profile 124 | #if MPV_CLIENT_API_VERSION >= MPV_MAKE_VERSION(2,1) 125 | jf_mpv_init_cache_dirs(mpv_ctx); 126 | #endif 127 | 128 | return mpv_ctx; 129 | } 130 | 131 | 132 | void jf_mpv_terminal(mpv_handle *mpv_ctx, bool enable) 133 | { 134 | JF_MPV_ASSERT(JF_MPV_SET_OPTPROP(mpv_ctx, "terminal", MPV_FORMAT_FLAG, enable ? &mpv_flag_yes : &mpv_flag_no)); 135 | } 136 | -------------------------------------------------------------------------------- /src/mpv.h: -------------------------------------------------------------------------------- 1 | #ifndef _JF_MPV 2 | #define _JF_MPV 3 | 4 | #include 5 | #include 6 | 7 | 8 | // workaround for mpv bug #3988 9 | #if MPV_CLIENT_API_VERSION <= MPV_MAKE_VERSION(1,24) 10 | #define JF_MPV_SET_OPTPROP mpv_set_option 11 | #define JF_MPV_SET_OPTPROP_STRING mpv_set_option_string 12 | #else 13 | #define JF_MPV_SET_OPTPROP mpv_set_property 14 | #define JF_MPV_SET_OPTPROP_STRING mpv_set_property_string 15 | #endif 16 | 17 | 18 | mpv_handle *jf_mpv_create(void); 19 | void jf_mpv_terminal(mpv_handle *mpv_ctx, bool enable); 20 | 21 | #endif 22 | -------------------------------------------------------------------------------- /src/net.c: -------------------------------------------------------------------------------- 1 | #include "net.h" 2 | #include "config.h" 3 | #include "shared.h" 4 | #include "json.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | 15 | 16 | ////////// GLOBAL VARIABLES ////////// 17 | extern jf_options g_options; 18 | extern jf_global_state g_state; 19 | ////////////////////////////////////// 20 | 21 | 22 | ////////// STATIC VARIABLES ////////// 23 | static CURL *s_handle = NULL; 24 | static struct curl_slist *s_headers = NULL; 25 | static struct curl_slist *s_headers_POST = NULL; 26 | static char s_curl_errorbuffer[CURL_ERROR_SIZE + 1]; 27 | static jf_thread_buffer s_tb; 28 | static pthread_mutex_t s_mut = PTHREAD_MUTEX_INITIALIZER; 29 | static CURLSH *s_curl_sh = NULL; 30 | static pthread_rwlock_t s_share_cookie_rw; 31 | static pthread_rwlock_t s_share_dns_rw; 32 | static pthread_rwlock_t s_share_connect_rw; 33 | static pthread_rwlock_t s_share_ssl_rw; 34 | #if JF_CURL_VERSION_GE(7,61) 35 | static pthread_rwlock_t s_share_psl_rw; 36 | #endif 37 | pthread_t s_async_threads[JF_NET_ASYNC_THREADS]; 38 | static jf_synced_queue *s_async_queue = NULL; 39 | static pthread_mutex_t s_async_mut; 40 | static pthread_cond_t s_async_cv; 41 | ////////////////////////////////////// 42 | 43 | 44 | ////////// STATIC FUNCTIONS ////////// 45 | static void jf_net_init(void); 46 | 47 | static void jf_thread_buffer_wait_parsing_done(void); 48 | 49 | static size_t jf_reply_callback(char *payload, 50 | size_t size, 51 | size_t nmemb, 52 | void *userdata); 53 | 54 | static size_t jf_thread_buffer_callback(char *payload, 55 | size_t size, 56 | size_t nmemb, 57 | void *userdata); 58 | 59 | static size_t jf_detach_callback(char *payload, 60 | size_t size, 61 | size_t nmemb, 62 | void *userdata); 63 | 64 | static size_t jf_check_update_header_callback(char *payload, 65 | size_t size, 66 | size_t nmemb, 67 | void *userdata); 68 | 69 | static CURL *jf_net_handle_init(void); 70 | 71 | static void jf_net_handle_before_perform(CURL *handle, 72 | const char *resource, 73 | const jf_request_type request_type, 74 | const jf_http_method method, 75 | const char *payload, 76 | const jf_reply *reply); 77 | 78 | static void jf_net_handle_after_perform(CURL *handle, 79 | const CURLcode result, 80 | const jf_request_type request_type, 81 | jf_reply *reply); 82 | 83 | static jf_async_request *jf_async_request_new(const char *resource, 84 | const jf_request_type request_type, 85 | const jf_http_method method, 86 | const char *payload); 87 | 88 | // NB DOES NOT FREE a_r->reply!!! 89 | static void jf_async_request_free(jf_async_request *a_r); 90 | 91 | static void *jf_net_async_worker_thread(void *arg); 92 | 93 | static inline pthread_rwlock_t * 94 | jf_net_get_lock_for_data(curl_lock_data data); 95 | 96 | static void jf_net_share_lock(CURL *handle, 97 | curl_lock_data data, 98 | curl_lock_access access, 99 | void *userptr); 100 | 101 | static void jf_net_share_unlock(CURL *handle, 102 | curl_lock_data data, 103 | void *userptr); 104 | ////////////////////////////////////// 105 | 106 | 107 | ////////// JF_REPLY ////////// 108 | jf_reply *jf_reply_new(void) 109 | { 110 | jf_reply *r; 111 | assert((r = malloc(sizeof(jf_reply))) != NULL); 112 | r->payload = NULL; 113 | r->size = 0; 114 | r->state = JF_REPLY_PENDING; 115 | return r; 116 | } 117 | 118 | 119 | void jf_reply_free(jf_reply *r) 120 | { 121 | if (r == NULL) return; 122 | if (r->state == JF_REPLY_PENDING) return; // better a leak than a segfault 123 | if (JF_REPLY_PTR_SHOULD_FREE_PAYLOAD(r)) { 124 | free(r->payload); 125 | } 126 | free(r); 127 | } 128 | 129 | 130 | char *jf_reply_error_string(const jf_reply *r) 131 | { 132 | if (r == NULL) { 133 | return "jf_reply is NULL"; 134 | } 135 | 136 | if (! JF_REPLY_PTR_HAS_ERROR(r)) { 137 | return "no error"; 138 | } 139 | 140 | switch (r->state) { 141 | case JF_REPLY_ERROR_STUB: 142 | return "stub functionality"; 143 | case JF_REPLY_ERROR_HTTP_401: 144 | return "http request returned error 401: unauthorized; you likely need to renew your auth token. Restart with --login"; 145 | break; 146 | case JF_REPLY_ERROR_MALLOC: 147 | return "memory allocation failed"; 148 | case JF_REPLY_ERROR_CONCAT: 149 | return "string concatenation failed"; 150 | case JF_REPLY_ERROR_X_EMBY_AUTH: 151 | return "appending x-emby-authorization failed"; 152 | case JF_REPLY_ERROR_BAD_LOCATION: 153 | return "\"location\" header from redirect was missing or not formatted as expected"; 154 | case JF_REPLY_ERROR_EXIT_REQUEST: 155 | return "exit request"; 156 | case JF_REPLY_ERROR_HTTP_400: 157 | case JF_REPLY_ERROR_NETWORK: 158 | case JF_REPLY_ERROR_HTTP_NOT_OK: 159 | case JF_REPLY_ERROR_PARSER: 160 | return r->payload; 161 | default: 162 | return "unknown error. This is a bug"; 163 | } 164 | } 165 | 166 | 167 | static size_t jf_reply_callback(char *payload, size_t size, size_t nmemb, void *userdata) 168 | { 169 | size_t real_size = size * nmemb; 170 | jf_reply *reply = (jf_reply *)userdata; 171 | 172 | if (JF_STATE_IS_EXITING(g_state.state)) return 0; 173 | assert(reply != NULL); 174 | assert((reply->payload = realloc(reply->payload, 175 | reply->size + real_size + 1)) != NULL); 176 | memcpy(reply->payload + reply->size, payload, real_size); 177 | reply->size += real_size; 178 | reply->payload[reply->size] = '\0'; 179 | return real_size; 180 | } 181 | ////////////////////////////// 182 | 183 | 184 | ////////// PARSER THREAD COMMUNICATION ////////// 185 | static void jf_thread_buffer_wait_parsing_done(void) 186 | { 187 | pthread_mutex_lock(&s_tb.mut); 188 | while (true) { 189 | switch (s_tb.state) { 190 | case JF_THREAD_BUFFER_STATE_AWAITING_DATA: 191 | pthread_cond_wait(&s_tb.cv_no_data, &s_tb.mut); 192 | break; 193 | case JF_THREAD_BUFFER_STATE_PENDING_DATA: 194 | pthread_cond_wait(&s_tb.cv_has_data, &s_tb.mut); 195 | break; 196 | default: 197 | pthread_mutex_unlock(&s_tb.mut); 198 | return; 199 | } 200 | } 201 | } 202 | 203 | 204 | size_t jf_thread_buffer_callback(char *payload, size_t size, size_t nmemb, void *userdata) 205 | { 206 | size_t real_size = size * nmemb; 207 | size_t written_data = 0; 208 | size_t chunk_size; 209 | jf_reply *r = (jf_reply *)userdata; 210 | 211 | pthread_mutex_lock(&s_tb.mut); 212 | while (written_data < real_size) { 213 | // wait for parser 214 | while (s_tb.state == JF_THREAD_BUFFER_STATE_PENDING_DATA 215 | && ! JF_STATE_IS_EXITING(g_state.state)) { 216 | pthread_cond_wait(&s_tb.cv_has_data, &s_tb.mut); 217 | } 218 | // check errors 219 | if (JF_STATE_IS_EXITING(g_state.state)) return 0; 220 | if (s_tb.state == JF_THREAD_BUFFER_STATE_PARSER_ERROR) { 221 | r->payload = strndup(s_tb.data, s_tb.used); 222 | r->state = JF_REPLY_ERROR_PARSER; 223 | return 0; 224 | } 225 | // send data 226 | chunk_size = real_size - written_data < JF_THREAD_BUFFER_DATA_SIZE - 1 227 | ? real_size - written_data 228 | : JF_THREAD_BUFFER_DATA_SIZE - 2; 229 | memcpy(s_tb.data, payload + written_data, chunk_size); 230 | written_data += chunk_size; 231 | s_tb.data[chunk_size + 1] = '\0'; 232 | s_tb.used = chunk_size; 233 | s_tb.state = JF_THREAD_BUFFER_STATE_PENDING_DATA; 234 | pthread_cond_signal(&s_tb.cv_no_data); 235 | } 236 | pthread_mutex_unlock(&s_tb.mut); 237 | 238 | return written_data; 239 | } 240 | 241 | 242 | size_t jf_thread_buffer_item_count(void) 243 | { 244 | return s_tb.item_count; 245 | } 246 | 247 | 248 | void jf_thread_buffer_clear_error(void) 249 | { 250 | pthread_mutex_lock(&s_tb.mut); 251 | s_tb.data[0] = '\0'; 252 | s_tb.used = 0; 253 | s_tb.state = JF_THREAD_BUFFER_STATE_CLEAR; 254 | pthread_mutex_unlock(&s_tb.mut); 255 | } 256 | ///////////////////////////////////////////////// 257 | 258 | 259 | ////////// NETWORK UNIT ////////// 260 | static void jf_net_init(void) 261 | { 262 | char *tmp; 263 | pthread_t sax_parser_thread; 264 | int i; 265 | 266 | assert(pthread_mutex_lock(&s_mut) == 0); 267 | if (s_handle != NULL) { 268 | pthread_mutex_unlock(&s_mut); 269 | return; 270 | } 271 | 272 | // TODO check libcurl version 273 | 274 | // global config stuff 275 | assert(curl_global_init(CURL_GLOBAL_ALL | CURL_GLOBAL_SSL) == 0); 276 | // security bypass 277 | if (! g_options.ssl_verifyhost) { 278 | curl_easy_setopt(s_handle, CURLOPT_SSL_VERIFYHOST, 0); 279 | } 280 | // headers 281 | assert((s_headers = curl_slist_append(s_headers, 282 | "accept: application/json; charset=utf-8")) != NULL); 283 | if (g_state.state == JF_STATE_STARTING_LOGIN 284 | || g_state.state == JF_STATE_STARTING_FULL_CONFIG) { 285 | // the only thing we will do is a POST for login 286 | tmp = jf_concat(9, 287 | "x-emby-authorization: Mediabrowser Client=\"", g_options.client, 288 | "\", Device=\"", g_options.device, 289 | "\", DeviceId=\"", g_options.deviceid, 290 | "\", Version=\"", g_options.version, 291 | "\""); 292 | assert((s_headers_POST = curl_slist_append(s_headers, tmp)) != NULL); 293 | } else { 294 | // main behaviour 295 | tmp = jf_concat(2, "x-mediabrowser-token: ", g_options.token); 296 | assert((s_headers = curl_slist_append(s_headers, tmp)) != NULL); 297 | } 298 | free(tmp); 299 | assert((s_headers_POST = curl_slist_append(s_headers_POST == NULL ? 300 | s_headers : s_headers_POST, 301 | "content-type: application/json; charset=utf-8")) != NULL); 302 | 303 | // setup sharing 304 | assert((s_curl_sh = curl_share_init()) != NULL); 305 | assert(pthread_rwlock_init(&s_share_cookie_rw, NULL) == 0); 306 | JF_CURL_SHARE_ASSERT(curl_share_setopt(s_curl_sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_COOKIE)); 307 | assert(pthread_rwlock_init(&s_share_dns_rw, NULL) == 0); 308 | JF_CURL_SHARE_ASSERT(curl_share_setopt(s_curl_sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS)); 309 | assert(pthread_rwlock_init(&s_share_ssl_rw, NULL) == 0); 310 | JF_CURL_SHARE_ASSERT(curl_share_setopt(s_curl_sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION)); 311 | assert(pthread_rwlock_init(&s_share_connect_rw, NULL) == 0); 312 | JF_CURL_SHARE_ASSERT(curl_share_setopt(s_curl_sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_CONNECT)); 313 | #if JF_CURL_VERSION_GE(7,61) 314 | if (curl_version_info(CURLVERSION_NOW)->features & CURL_VERSION_PSL) { 315 | assert(pthread_rwlock_init(&s_share_psl_rw, NULL) == 0); 316 | JF_CURL_SHARE_ASSERT(curl_share_setopt(s_curl_sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_PSL)); 317 | } 318 | #endif 319 | JF_CURL_SHARE_ASSERT(curl_share_setopt(s_curl_sh, CURLSHOPT_LOCKFUNC, jf_net_share_lock)); 320 | JF_CURL_SHARE_ASSERT(curl_share_setopt(s_curl_sh, CURLSHOPT_UNLOCKFUNC, jf_net_share_unlock)); 321 | 322 | s_handle = jf_net_handle_init(); 323 | 324 | // sax parser thread 325 | jf_thread_buffer_init(&s_tb); 326 | assert(pthread_create(&sax_parser_thread, NULL, jf_json_sax_thread, (void *)&(s_tb)) != -1); 327 | assert(pthread_detach(sax_parser_thread) == 0); 328 | 329 | // async networking 330 | s_async_queue = jf_synced_queue_new(16); 331 | assert(pthread_mutex_init(&s_async_mut, NULL) == 0); 332 | assert(pthread_cond_init(&s_async_cv, NULL) == 0); 333 | 334 | for (i = 0; i < JF_NET_ASYNC_THREADS; i++) { 335 | assert(pthread_create(s_async_threads + i, NULL, jf_net_async_worker_thread, NULL) != -1); 336 | } 337 | 338 | assert(pthread_mutex_unlock(&s_mut) == 0); 339 | } 340 | 341 | 342 | void jf_net_clear(void) 343 | { 344 | int i; 345 | 346 | assert(pthread_mutex_lock(&s_mut) == 0); 347 | if (s_handle == NULL) { 348 | pthread_mutex_unlock(&s_mut); 349 | return; 350 | } 351 | 352 | for (i = 0; i < JF_NET_ASYNC_THREADS; i++) { 353 | jf_synced_queue_enqueue(s_async_queue, 354 | jf_async_request_new(NULL, JF_REQUEST_EXIT, JF_HTTP_GET, NULL)); 355 | } 356 | curl_easy_cleanup(s_handle); 357 | for (i = 0; i < JF_NET_ASYNC_THREADS; i++) { 358 | assert(pthread_join(s_async_threads[i], NULL) == 0); 359 | } 360 | curl_share_cleanup(s_curl_sh); 361 | curl_slist_free_all(s_headers_POST); 362 | curl_global_cleanup(); 363 | 364 | assert(pthread_mutex_unlock(&s_mut) == 0); 365 | } 366 | ////////////////////////////////// 367 | 368 | 369 | ////////// NETWORKING ////////// 370 | static CURL *jf_net_handle_init(void) 371 | { 372 | CURL *handle; 373 | 374 | assert((handle = curl_easy_init()) != NULL); 375 | 376 | // report errors 377 | s_curl_errorbuffer[0] = '\0'; 378 | s_curl_errorbuffer[sizeof(s_curl_errorbuffer) - 1] = '\0'; 379 | JF_CURL_ASSERT(curl_easy_setopt(handle, CURLOPT_ERRORBUFFER, s_curl_errorbuffer)); 380 | 381 | // be a good neighbour 382 | JF_CURL_ASSERT(curl_easy_setopt(handle, CURLOPT_SHARE, s_curl_sh)); 383 | JF_CURL_ASSERT(curl_easy_setopt(handle, CURLOPT_NOSIGNAL, 1L)); 384 | 385 | // ask for all supported kinds of compression 386 | JF_CURL_ASSERT(curl_easy_setopt(handle, CURLOPT_ACCEPT_ENCODING, "")); 387 | 388 | // follow redirects and keep POST method if using it 389 | JF_CURL_ASSERT(curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, 1)); 390 | JF_CURL_ASSERT(curl_easy_setopt(handle, CURLOPT_POSTREDIR, CURL_REDIR_POST_ALL)); 391 | 392 | return handle; 393 | } 394 | 395 | 396 | static void jf_net_handle_before_perform(CURL *handle, 397 | const char *resource, 398 | const jf_request_type request_type, 399 | const jf_http_method method, 400 | const char *payload, 401 | const jf_reply *reply) 402 | { 403 | char *url; 404 | 405 | JF_DEBUG_PRINTF("jf_net_handle_before_perform: resource=%s request_type=%d method=%u payload=%s\n", 406 | resource, 407 | request_type, 408 | method, 409 | payload); 410 | 411 | // url 412 | if (request_type == JF_REQUEST_CHECK_UPDATE) { 413 | JF_CURL_ASSERT(curl_easy_setopt(handle, 414 | CURLOPT_URL, 415 | "https://github.com/Aanok/jftui/releases/latest")); 416 | } else { 417 | url = jf_concat(2, g_options.server, resource); 418 | JF_CURL_ASSERT(curl_easy_setopt(handle, CURLOPT_URL, url)); 419 | free(url); 420 | } 421 | 422 | // HTTP method and headers 423 | switch (method) { 424 | case JF_HTTP_GET: 425 | JF_CURL_ASSERT(curl_easy_setopt(handle, CURLOPT_HTTPGET, 1)); 426 | JF_CURL_ASSERT(curl_easy_setopt(handle, CURLOPT_HTTPHEADER, s_headers)); 427 | break; 428 | case JF_HTTP_POST: 429 | // for ASYNC_DETACH we must assume the caller unwilling or unable 430 | // to keep the payload live till completion 431 | JF_CURL_ASSERT(curl_easy_setopt(handle, 432 | (request_type == JF_REQUEST_ASYNC_DETACH ? CURLOPT_COPYPOSTFIELDS : CURLOPT_POSTFIELDS), 433 | payload != NULL ? payload : "")); 434 | JF_CURL_ASSERT(curl_easy_setopt(handle, CURLOPT_HTTPHEADER, s_headers_POST)); 435 | break; 436 | case JF_HTTP_DELETE: 437 | JF_CURL_ASSERT(curl_easy_setopt(handle, CURLOPT_CUSTOMREQUEST, "DELETE")); 438 | JF_CURL_ASSERT(curl_easy_setopt(handle, CURLOPT_HTTPHEADER, s_headers)); 439 | break; 440 | } 441 | 442 | // request 443 | switch (request_type) { 444 | case JF_REQUEST_IN_MEMORY: 445 | case JF_REQUEST_ASYNC_IN_MEMORY: 446 | JF_CURL_ASSERT(curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, jf_reply_callback)); 447 | break; 448 | case JF_REQUEST_SAX_PROMISCUOUS: 449 | s_tb.promiscuous_context = true; 450 | JF_CURL_ASSERT(curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, jf_thread_buffer_callback)); 451 | break; 452 | case JF_REQUEST_SAX: 453 | s_tb.promiscuous_context = false; 454 | JF_CURL_ASSERT(curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, jf_thread_buffer_callback)); 455 | break; 456 | case JF_REQUEST_CHECK_UPDATE: 457 | // we don't care for the redirect 458 | JF_CURL_ASSERT(curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, 0)); 459 | JF_CURL_ASSERT(curl_easy_setopt(handle, CURLOPT_HEADERFUNCTION, jf_check_update_header_callback)); 460 | JF_CURL_ASSERT(curl_easy_setopt(handle, CURLOPT_HEADERDATA, (void *)reply)); 461 | // no break 462 | case JF_REQUEST_ASYNC_DETACH: 463 | JF_CURL_ASSERT(curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, jf_detach_callback)); 464 | break; 465 | case JF_REQUEST_EXIT: 466 | // just to silence the warning, this is never reached 467 | // (and should be kept that way!) 468 | return; 469 | } 470 | JF_CURL_ASSERT(curl_easy_setopt(handle, CURLOPT_WRITEDATA, (void *)reply)); 471 | } 472 | 473 | 474 | static void jf_net_handle_after_perform(CURL *handle, 475 | const CURLcode result, 476 | const jf_request_type request_type, 477 | jf_reply *reply) 478 | { 479 | long status_code; 480 | 481 | if (request_type == JF_REQUEST_ASYNC_DETACH || reply == NULL) { 482 | jf_reply_free(reply); 483 | return; 484 | } 485 | 486 | if (request_type == JF_REQUEST_CHECK_UPDATE) { 487 | // reset handle to sane defaults 488 | JF_CURL_ASSERT(curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, 1)); 489 | JF_CURL_ASSERT(curl_easy_setopt(handle, CURLOPT_HEADERFUNCTION, NULL)); 490 | JF_CURL_ASSERT(curl_easy_setopt(handle, CURLOPT_HEADERDATA, NULL)); 491 | } 492 | 493 | // leave if we've already caught an error 494 | if (JF_REPLY_PTR_HAS_ERROR(reply)) return; 495 | 496 | // copy info text and leave if curl caught an error 497 | if (result != CURLE_OK) { 498 | free(reply->payload); 499 | reply->payload = (char *)curl_easy_strerror(result); 500 | reply->state = JF_REPLY_ERROR_NETWORK; 501 | return; 502 | } 503 | 504 | if (request_type == JF_REQUEST_SAX_PROMISCUOUS || request_type == JF_REQUEST_SAX) { 505 | jf_thread_buffer_wait_parsing_done(); 506 | } 507 | 508 | // check for http error 509 | JF_CURL_ASSERT(curl_easy_getinfo(handle, CURLINFO_RESPONSE_CODE, &status_code)); 510 | switch (status_code) { 511 | case 200: 512 | case 204: 513 | reply->state = JF_REPLY_SUCCESS; 514 | break; 515 | case 400: 516 | reply->state = JF_REPLY_ERROR_HTTP_400; 517 | break; 518 | case 401: 519 | reply->state = JF_REPLY_ERROR_HTTP_401; 520 | break; 521 | case 302: 522 | if (request_type == JF_REQUEST_CHECK_UPDATE) { 523 | reply->state = reply->payload == NULL ? 524 | JF_REPLY_ERROR_BAD_LOCATION 525 | : JF_REPLY_SUCCESS; 526 | break; 527 | } 528 | // no break on else 529 | default: 530 | free(reply->payload); 531 | assert((reply->payload = malloc(34)) != NULL); 532 | snprintf(reply->payload, 34, "http request returned status %ld", status_code); 533 | reply->state = JF_REPLY_ERROR_HTTP_NOT_OK; 534 | break; 535 | } 536 | } 537 | 538 | 539 | jf_reply *jf_net_request(const char *resource, 540 | const jf_request_type request_type, 541 | const jf_http_method method, 542 | const char *payload) 543 | { 544 | jf_reply *reply; 545 | jf_async_request *a_r; 546 | 547 | if (request_type == JF_REQUEST_EXIT) { 548 | reply = jf_reply_new(); 549 | reply->state = JF_REPLY_ERROR_EXIT_REQUEST; 550 | return reply; 551 | } 552 | 553 | if (s_handle == NULL) { 554 | jf_net_init(); 555 | } 556 | 557 | if (JF_REQUEST_TYPE_IS_ASYNC(request_type)) { 558 | a_r = jf_async_request_new(resource, 559 | request_type, 560 | method, 561 | payload); 562 | reply = a_r->reply; 563 | jf_synced_queue_enqueue(s_async_queue, a_r); 564 | } else { 565 | reply = jf_reply_new(); 566 | jf_net_handle_before_perform(s_handle, 567 | resource, 568 | request_type, 569 | method, 570 | payload, 571 | reply); 572 | jf_net_handle_after_perform(s_handle, 573 | curl_easy_perform(s_handle), 574 | request_type, 575 | reply); 576 | } 577 | 578 | return reply; 579 | } 580 | /////////////////////////////////// 581 | 582 | 583 | ////////// ASYNC NETWORKING ////////// 584 | static jf_async_request *jf_async_request_new(const char *resource, 585 | const jf_request_type request_type, 586 | const jf_http_method method, 587 | const char *payload) 588 | { 589 | jf_async_request *a_r; 590 | static pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER; 591 | static size_t id = 0; 592 | 593 | assert((a_r = malloc(sizeof(jf_async_request))) != NULL); 594 | a_r->reply = request_type == JF_REQUEST_ASYNC_DETACH ? NULL : jf_reply_new(); 595 | if (resource == NULL) { 596 | a_r->resource = NULL; 597 | } else { 598 | assert((a_r->resource = strdup(resource)) != NULL); 599 | } 600 | a_r->type = request_type; 601 | a_r->method = method; 602 | switch (method) { 603 | case JF_HTTP_GET: 604 | case JF_HTTP_DELETE: 605 | a_r->payload = NULL; 606 | break; 607 | case JF_HTTP_POST: 608 | if (payload == NULL) { 609 | a_r->payload = NULL; 610 | } else { 611 | assert((a_r->payload = strdup(payload)) != NULL); 612 | } 613 | break; 614 | } 615 | pthread_mutex_lock(&mut); 616 | a_r->id = id++; 617 | pthread_mutex_unlock(&mut); 618 | 619 | return a_r; 620 | } 621 | 622 | 623 | static void jf_async_request_free(jf_async_request *a_r) 624 | { 625 | if (a_r == NULL) return; 626 | free(a_r->resource); 627 | free(a_r->payload); 628 | free(a_r); 629 | } 630 | 631 | 632 | static size_t jf_detach_callback(__attribute__((unused)) char *payload, 633 | size_t size, 634 | size_t nmemb, 635 | __attribute__((unused)) void *userdata) 636 | { 637 | if (JF_STATE_IS_EXITING(g_state.state)) { 638 | return 0; 639 | } 640 | // discard everything 641 | return size * nmemb; 642 | } 643 | 644 | 645 | static void *jf_net_async_worker_thread(__attribute__((unused)) void *arg) 646 | { 647 | CURL *handle; 648 | jf_async_request *request; 649 | 650 | handle = jf_net_handle_init(); 651 | 652 | // block signals we handle in main thread 653 | { 654 | sigset_t ss; 655 | sigemptyset(&ss); 656 | sigaddset(&ss, SIGABRT); 657 | sigaddset(&ss, SIGINT); 658 | sigaddset(&ss, SIGPIPE); 659 | assert(pthread_sigmask(SIG_BLOCK, &ss, NULL) == 0); 660 | } 661 | 662 | while (true) { 663 | request = (jf_async_request *)jf_synced_queue_dequeue(s_async_queue); 664 | if (request->type == JF_REQUEST_EXIT) { 665 | jf_async_request_free(request); 666 | curl_easy_cleanup(handle); 667 | pthread_exit(NULL); 668 | } 669 | jf_net_handle_before_perform(handle, 670 | request->resource, 671 | request->type, 672 | request->method, 673 | request->payload, 674 | request->reply); 675 | jf_net_handle_after_perform(handle, 676 | curl_easy_perform(handle), 677 | request->type, 678 | request->reply); 679 | jf_async_request_free(request); 680 | assert(pthread_cond_broadcast(&s_async_cv) == 0); 681 | } 682 | } 683 | 684 | 685 | static inline pthread_rwlock_t * 686 | jf_net_get_lock_for_data(curl_lock_data data) 687 | { 688 | switch (data) { 689 | case CURL_LOCK_DATA_COOKIE: 690 | return &s_share_cookie_rw; 691 | case CURL_LOCK_DATA_DNS: 692 | return &s_share_dns_rw; 693 | case CURL_LOCK_DATA_SSL_SESSION: 694 | return &s_share_ssl_rw; 695 | case CURL_LOCK_DATA_CONNECT: 696 | return &s_share_connect_rw; 697 | #if JF_CURL_VERSION_GE(7,61) 698 | case CURL_LOCK_DATA_PSL: 699 | return &s_share_psl_rw; 700 | #endif 701 | default: 702 | // no-op, other types are for curl internals 703 | return NULL; 704 | } 705 | } 706 | 707 | 708 | static void jf_net_share_lock(__attribute__((unused)) CURL *handle, 709 | curl_lock_data data, 710 | curl_lock_access access, 711 | __attribute__((unused)) void *userptr) 712 | { 713 | pthread_rwlock_t *rw_lock = jf_net_get_lock_for_data(data); 714 | 715 | if (rw_lock == NULL) return; 716 | 717 | switch (access) { 718 | case CURL_LOCK_ACCESS_SHARED: 719 | assert(pthread_rwlock_rdlock(rw_lock) == 0); 720 | break; 721 | case CURL_LOCK_ACCESS_SINGLE: 722 | assert(pthread_rwlock_wrlock(rw_lock) == 0); 723 | break; 724 | default: 725 | // no-op, other types are for internals 726 | return; 727 | } 728 | } 729 | 730 | 731 | static void jf_net_share_unlock(__attribute__((unused)) CURL *handle, 732 | curl_lock_data data, 733 | __attribute__((unused)) void *userptr) 734 | { 735 | pthread_rwlock_t *rw_lock = jf_net_get_lock_for_data(data); 736 | 737 | if (rw_lock == NULL) return; 738 | 739 | assert(pthread_rwlock_unlock(rw_lock) == 0); 740 | } 741 | 742 | 743 | jf_reply *jf_net_await(jf_reply *reply) 744 | { 745 | assert(reply != NULL); 746 | pthread_mutex_lock(&s_async_mut); 747 | while (reply->state == JF_REPLY_PENDING) { 748 | pthread_cond_wait(&s_async_cv, &s_async_mut); 749 | } 750 | pthread_mutex_unlock(&s_async_mut); 751 | return reply; 752 | } 753 | ////////////////////////////////////// 754 | 755 | 756 | ////////// MISCELLANEOUS GARBAGE /////////// 757 | static size_t jf_check_update_header_callback(char *payload, 758 | size_t size, 759 | size_t nmemb, 760 | void *userdata) 761 | { 762 | size_t real_size = size * nmemb; 763 | jf_reply *reply; 764 | char *version_str; 765 | size_t version_len; 766 | 767 | if (JF_STATE_IS_EXITING(g_state.state)) { 768 | return 0; 769 | } 770 | 771 | if (strncasecmp(payload, 772 | "location", 773 | JF_STATIC_STRLEN("location") < real_size ? 774 | JF_STATIC_STRLEN("location") : real_size) == 0) { 775 | reply = (jf_reply *)userdata; 776 | if ((version_str = strrchr(payload, '/')) == NULL) goto bad_exit; 777 | // digest "/v" 778 | version_str += 2; 779 | // sanity check, knowing we will drop the trailing CRLF 780 | if (version_str >= payload + real_size - 2) goto bad_exit; 781 | version_len = (size_t)((payload + real_size - 3) - version_str) + 1; 782 | assert((reply->payload = strndup(version_str, version_len)) != NULL); 783 | reply->size = version_len; 784 | } 785 | return real_size; 786 | 787 | bad_exit: 788 | reply->state = JF_REPLY_ERROR_BAD_LOCATION; 789 | return real_size; 790 | } 791 | 792 | 793 | char *jf_net_urlencode(const char *url) 794 | { 795 | char *tmp, *retval; 796 | assert(s_handle != NULL); 797 | assert((tmp = curl_easy_escape(s_handle, url, 0)) != NULL); 798 | retval = strdup(tmp); 799 | curl_free(tmp); 800 | assert(retval != NULL); 801 | return retval; 802 | } 803 | 804 | 805 | bool jf_net_url_is_valid(const char *url) 806 | { 807 | #if JF_CURL_VERSION_GE(7,62) 808 | CURLU *curlu; 809 | 810 | if ((curlu = curl_url()) == NULL) { 811 | fprintf(stderr, "Error: curlu curl_url returned NULL.\n"); 812 | curl_url_cleanup(curlu); 813 | return false; 814 | } 815 | 816 | if (curl_url_set(curlu, CURLUPART_URL, url, 0) == CURLUE_OK) { 817 | curl_url_cleanup(curlu); 818 | return true; 819 | } else { 820 | curl_url_cleanup(curlu); 821 | return false; 822 | } 823 | #else 824 | fprintf(stderr, "Warning: the libcurl version jftui was compiled against will defer URL validation to the first network request.\n"); 825 | fprintf(stderr, "If the URL you have entered turns out to be invalid, repeat the login process by passing --login to try again.\n"); 826 | return true; 827 | #endif 828 | } 829 | 830 | 831 | bool jf_net_url_is_localhost(const char *url) 832 | { 833 | char *host; 834 | bool is_localhost; 835 | 836 | if (url == NULL) return false; 837 | 838 | #if JF_CURL_VERSION_GE(7,61) 839 | // use CURL stuff if available 840 | // please hope it's available 841 | CURLU *curlu; 842 | CURLUcode res; 843 | 844 | if ((curlu = curl_url()) == NULL) { 845 | fprintf(stderr, "Error: curlu curl_url returned NULL.\n"); 846 | curl_url_cleanup(curlu); 847 | return false; 848 | } 849 | 850 | if ((res = curl_url_set(curlu, 851 | CURLUPART_URL, 852 | url, 853 | CURLU_DEFAULT_SCHEME)) != CURLUE_OK) { 854 | fprintf(stderr, "Error: curlu curl_url_set failure.\n"); 855 | curl_url_cleanup(curlu); 856 | return false; 857 | } 858 | 859 | res = curl_url_get(curlu, CURLUPART_HOST, &host, 0); 860 | curl_url_cleanup(curlu); 861 | 862 | if (res != CURLUE_OK) { 863 | fprintf(stderr, "Error: curlu curl_url_get failure.\n"); 864 | // TODO error string 865 | return false; 866 | } 867 | #else 868 | // terrible horrible no good very bad fallback implementation otherwise 869 | if ((host = strstr(url, "@")) != NULL) { 870 | // there's an authority 871 | host++; 872 | } else if ((host = strstr(url, "//")) != NULL) { 873 | // there's a schema 874 | host += 2; 875 | } else { 876 | host = (char *)url; 877 | } 878 | #endif 879 | 880 | is_localhost = strncmp(host, "127.0.0.1", JF_STATIC_STRLEN("127.0.0.1")) == 0 881 | || strncasecmp(host, "localhost", JF_STATIC_STRLEN("localhost")) == 0 882 | || strncmp(host, "[::1]", JF_STATIC_STRLEN("[::1]")) == 0; 883 | 884 | #if JF_CURL_VERSION_GE(7,62) 885 | curl_free(host); 886 | #endif 887 | 888 | return is_localhost; 889 | } 890 | //////////////////////////////////////////// 891 | -------------------------------------------------------------------------------- /src/net.h: -------------------------------------------------------------------------------- 1 | #ifndef _JF_NET 2 | #define _JF_NET 3 | 4 | 5 | #include 6 | #include 7 | 8 | 9 | ////////// CODE MACROS ////////// 10 | #define JF_CURL_ASSERT(_s) \ 11 | do { \ 12 | CURLcode _c = _s; \ 13 | if (_c != CURLE_OK) { \ 14 | fprintf(stderr, "%s:%d: " #_s " failed.\n", __FILE__, __LINE__); \ 15 | fprintf(stderr, "FATAL: libcurl error: %s: %s.\n", \ 16 | curl_easy_strerror(_c), s_curl_errorbuffer); \ 17 | jf_exit(JF_EXIT_FAILURE); \ 18 | } \ 19 | } while (false) 20 | 21 | #define JF_CURL_SHARE_ASSERT(_s) \ 22 | do { \ 23 | CURLSHcode _c = _s; \ 24 | if (_c != CURLSHE_OK) { \ 25 | fprintf(stderr, "%s:%d: " #_s " failed.\n", __FILE__, __LINE__); \ 26 | fprintf(stderr, "FATAL: libcurl error: %s.\n", \ 27 | curl_share_strerror(_c)); \ 28 | pthread_mutex_unlock(&s_mut); \ 29 | jf_exit(JF_EXIT_FAILURE); \ 30 | } \ 31 | } while (false) 32 | 33 | ///////////////////////////////// 34 | 35 | 36 | ////////// CONSTANTS ///////// 37 | #define JF_NET_ASYNC_THREADS 3 38 | ////////////////////////////// 39 | 40 | ////////// JF_REPLY ////////// 41 | typedef enum __attribute__((__packed__)) jf_reply_state { 42 | // REMEMBER TO UPDATE THE MACROS BELOW WHEN CHANGING THESE! 43 | JF_REPLY_PENDING = 0, 44 | JF_REPLY_SUCCESS = 1, 45 | 46 | JF_REPLY_ERROR_STUB = -1, 47 | JF_REPLY_ERROR_HTTP_401 = -2, 48 | JF_REPLY_ERROR_MALLOC = -3, 49 | JF_REPLY_ERROR_CONCAT = -4, 50 | JF_REPLY_ERROR_X_EMBY_AUTH = -5, 51 | JF_REPLY_ERROR_BAD_LOCATION = -7, 52 | JF_REPLY_ERROR_EXIT_REQUEST = -8, 53 | JF_REPLY_ERROR_NETWORK = -9, 54 | 55 | JF_REPLY_ERROR_HTTP_400 = -32, 56 | JF_REPLY_ERROR_HTTP_NOT_OK = -33, 57 | JF_REPLY_ERROR_PARSER = -34, 58 | } jf_reply_state; 59 | 60 | 61 | typedef struct jf_reply { 62 | char *payload; 63 | size_t size; 64 | jf_reply_state state; 65 | } jf_reply; 66 | 67 | 68 | #define JF_REPLY_PTR_IS_PENDING(_p) ((_p)->state == 0) 69 | #define JF_REPLY_PTR_HAS_ERROR(_p) ((_p)->state < 0) 70 | #define JF_REPLY_PTR_SHOULD_FREE_PAYLOAD(_p) ((_p)->state == 1 || (_p)->state <= -32) 71 | 72 | 73 | jf_reply *jf_reply_new(void); 74 | void jf_reply_free(jf_reply *r); 75 | char *jf_reply_error_string(const jf_reply *r); 76 | ////////////////////////////// 77 | 78 | 79 | ////////// PARSER THREAD COMMUNICATION ////////// 80 | size_t jf_thread_buffer_item_count(void); 81 | void jf_thread_buffer_clear_error(void); 82 | ///////////////////////////////////////////////// 83 | 84 | 85 | ////////// NETWORK UNIT ////////// 86 | void jf_net_clear(void); 87 | ////////////////////////////////// 88 | 89 | 90 | ////////// NETWORKING ////////// 91 | typedef enum jf_request_type { 92 | JF_REQUEST_IN_MEMORY = 0, 93 | JF_REQUEST_SAX = 1, 94 | JF_REQUEST_SAX_PROMISCUOUS = 2, 95 | 96 | JF_REQUEST_ASYNC_IN_MEMORY = -1, 97 | JF_REQUEST_ASYNC_DETACH = -2, 98 | JF_REQUEST_CHECK_UPDATE = -3, 99 | 100 | JF_REQUEST_EXIT = -100 101 | } jf_request_type; 102 | 103 | #define JF_REQUEST_TYPE_IS_ASYNC(_t) ((_t) < 0) 104 | 105 | 106 | typedef enum jf_http_method { 107 | JF_HTTP_GET, 108 | JF_HTTP_POST, 109 | JF_HTTP_DELETE 110 | } jf_http_method; 111 | 112 | 113 | // Executes a network request to the Jellyfin server. The response may be 114 | // entirely put in a single jf_reply in memory or passed step by step to the 115 | // JSON parser thread with constant memory usage. In the latter case, the 116 | // function will wait for parsing to be complete before returning. 117 | // 118 | // Parameters: 119 | // resource: 120 | // For JF_REQUEST_CHECK_UPDATE and JF_REQUEST_EXIT, this is ignored. 121 | // Otherwise, it is treated as a suffix to append to the server's address 122 | // to compute the full URL. 123 | // request_type: 124 | // - JF_REQUEST_IN_MEMORY will cause the request to be evaded blockingly 125 | // and the response to be passed back in a jf_reply struct; 126 | // - JF_REQUEST_SAX will cause the response to be blockingly passed to the 127 | // JSON parser and digested as a non-promiscuous context; 128 | // - JF_REQUEST_SAX_PROMISCUOUS likewise but digested as a promiscuous 129 | // context; 130 | // (note: both SAX requests will wait for the parser to be done before 131 | // returning control to the caller) 132 | // - JF_REQUEST_ASYNC_IN_MEMORY will cause the request to be evaded 133 | // asynchronously: control will be passed back the caller immediately 134 | // while a separate thread takes care of network traffic. The response 135 | // will be passed back in a jf_reply struct. The caller may use 136 | // jf_net_async_await to wait until the request is fully evaded. 137 | // - JF_REQUEST_ASYNC_DETACH will likewise work asynchronously; however, 138 | // the function will immediately return NULL and all response data 139 | // will be discarded on arrival. Use for requests whose outcome you 140 | // don't care about, like watch state updates. 141 | // - JF_REQUEST_CHECK_UPDATE functions like JF_REQUEST_ASYNC_IN_MEMORY, 142 | // except the resource parameter is ignored and internally set to the 143 | // one required for the optional update check against github.com 144 | // - JF_REQUEST_EXIT should not be used here and will return a reply 145 | // containing an error code without performing any network activity. 146 | // method: 147 | // Can be JF_HTTP_GET, JF_HTTP_POST, JF_HTTP_DELETE, with the obvious 148 | // semantics. 149 | // payload: 150 | // Ignored for GET and DELETE requests. Constitutes the requests' body 151 | // for POST (may be NULL for an empty body). 152 | // 153 | // Returns: 154 | // A jf_reply which either: 155 | // - marks an error (authentication, network, parser's, internal), check with 156 | // the JF_REPLY_PTR_HAS_ERROR macro and get an error string with 157 | // jf_reply_error_string; 158 | // - contains the body of the response for a JF_REQUEST_[ASYNC_]IN_MEMORY and 159 | // JF_REQUEST_CHECK_UPDATE. 160 | // - contains an empty body for JF_REQUEST_SAX_*; 161 | // - is NULL for JF_REQUEST_ASYNC_DETACH. 162 | // CAN FATAL. 163 | jf_reply *jf_net_request(const char *resource, 164 | jf_request_type request_type, 165 | const jf_http_method method, 166 | const char *payload); 167 | //////////////////////////////// 168 | 169 | 170 | ////////// ASYNC NETWORKING ////////// 171 | typedef struct jf_async_request { 172 | jf_reply *reply; 173 | char *resource; 174 | jf_request_type type; 175 | jf_http_method method; 176 | char *payload; 177 | size_t id; 178 | } jf_async_request; 179 | 180 | 181 | jf_reply *jf_net_await(jf_reply *r); 182 | ////////////////////////////////////// 183 | 184 | 185 | ////////// MISCELLANEOUS GARBAGE /////////// 186 | char *jf_net_urlencode(const char *url); 187 | bool jf_net_url_is_valid(const char *url); 188 | 189 | 190 | // Checks if the provided URL refers to the local host machine. 191 | // REQUIRES: url is a valid URL. 192 | bool jf_net_url_is_localhost(const char *url); 193 | //////////////////////////////////////////// 194 | 195 | #endif 196 | -------------------------------------------------------------------------------- /src/playback.c: -------------------------------------------------------------------------------- 1 | #include "playback.h" 2 | #include "disk.h" 3 | #include "config.h" 4 | #include "json.h" 5 | #include "net.h" 6 | #include "menu.h" 7 | #include "shared.h" 8 | #include "mpv.h" 9 | 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | ////////// GLOBAL VARIABLES ////////// 17 | extern jf_global_state g_state; 18 | extern jf_options g_options; 19 | extern mpv_handle *g_mpv_ctx; 20 | ////////////////////////////////////// 21 | 22 | 23 | ////////// STATIC VARIABLES ////////// 24 | // element of the current playlist that was playing when we last printed the 25 | // playlist to terminal 26 | size_t s_last_playlist_print = 0; 27 | ////////////////////////////////////// 28 | 29 | 30 | ////////// STATIC FUNCTIONS /////////////// 31 | // playback_ticks refers to segment referred by id 32 | static void jf_post_session_update(const char *id, 33 | int64_t playback_ticks, 34 | const char *update_url); 35 | 36 | 37 | static void jf_post_session(const int64_t playback_ticks, 38 | const char *update_url); 39 | 40 | 41 | // Requests PlaybackPositionTicks for item's additionalparts (if any) and 42 | // populates the field for item's children. item's own playback_ticks is set to 43 | // 0. 44 | // 45 | // Parameters: 46 | // - item: the item to check (will be modified in place). 47 | // 48 | // Returns: 49 | // - true: on success; 50 | // - false: on failure, in which case playback_ticks may have been populated 51 | // for some of the children before encountering the failure. 52 | static inline bool jf_playback_populate_video_ticks(jf_menu_item *item); 53 | 54 | 55 | static void jf_playback_playlist_window(size_t window_size, size_t window[2]); 56 | /////////////////////////////////////////// 57 | 58 | 59 | ////////// PROGRESS SYNC ////////// 60 | static void jf_post_session_update(const char *id, 61 | int64_t playback_ticks, 62 | const char *update_url) 63 | { 64 | char *progress_post; 65 | 66 | progress_post = jf_json_generate_progress_post(id, playback_ticks); 67 | jf_net_request(update_url, 68 | JF_REQUEST_ASYNC_DETACH, 69 | JF_HTTP_POST, 70 | progress_post); 71 | free(progress_post); 72 | } 73 | 74 | 75 | static void jf_post_session(const int64_t playback_ticks, 76 | const char *update_url) 77 | { 78 | size_t current_part = (size_t)-1; 79 | size_t last_part = (size_t)-1; 80 | size_t i; 81 | int64_t accounted_ticks, current_tick_offset; 82 | 83 | // single-part items are blissfully simple and I lament my toil elsewise 84 | if (g_state.now_playing->children_count <= 1) { 85 | jf_post_session_update(g_state.now_playing->id, 86 | playback_ticks, 87 | update_url); 88 | g_state.now_playing->playback_ticks = playback_ticks; 89 | return; 90 | } 91 | 92 | // split-part: figure out part number of current pos and last update 93 | accounted_ticks = 0; 94 | current_tick_offset = 0; 95 | for (i = 0; i < g_state.now_playing->children_count; i++) { 96 | if (accounted_ticks <= playback_ticks) { 97 | if (playback_ticks < accounted_ticks + g_state.now_playing->children[i]->runtime_ticks) { 98 | current_part = i; 99 | } else { 100 | current_tick_offset += g_state.now_playing->children[i]->runtime_ticks; 101 | } 102 | } 103 | if (accounted_ticks <= g_state.now_playing->playback_ticks 104 | && g_state.now_playing->playback_ticks < accounted_ticks + g_state.now_playing->children[i]->runtime_ticks) { 105 | last_part = i; 106 | } 107 | accounted_ticks += g_state.now_playing->children[i]->runtime_ticks; 108 | } 109 | 110 | // check error 111 | if (current_part == (size_t)-1 || last_part == (size_t)-1) { 112 | fprintf(stderr, "Warning: could not figure out playback position within a split-file. Playback progress was not notified to the server.\n"); 113 | return; 114 | } 115 | 116 | // update progress of current part and record last update 117 | jf_post_session_update(g_state.now_playing->children[current_part]->id, 118 | playback_ticks - current_tick_offset, 119 | update_url); 120 | g_state.now_playing->playback_ticks = playback_ticks; 121 | 122 | // check if moved across parts and in case update 123 | if (last_part == current_part) return; 124 | for (i = 0; i < g_state.now_playing->children_count; i++) { 125 | if (i < current_part) { 126 | jf_menu_item_set_flag_detach(g_state.now_playing->children[i], 127 | JF_FLAG_TYPE_PLAYED, 128 | true); 129 | } else if (i > current_part) { 130 | jf_menu_item_set_flag_detach(g_state.now_playing->children[i], 131 | JF_FLAG_TYPE_PLAYED, 132 | false); 133 | } 134 | } 135 | } 136 | 137 | 138 | void jf_playback_update_playing(const int64_t playback_ticks) 139 | { 140 | jf_post_session(playback_ticks, "/sessions/playing"); 141 | } 142 | 143 | 144 | void jf_playback_update_progress(const int64_t playback_ticks) 145 | { 146 | jf_post_session(playback_ticks, "/sessions/playing/progress"); 147 | } 148 | 149 | 150 | void jf_playback_update_stopped(const int64_t playback_ticks) 151 | { 152 | jf_post_session(playback_ticks, "/sessions/playing/stopped"); 153 | } 154 | /////////////////////////////////// 155 | 156 | 157 | ////////// SUBTITLES ////////// 158 | void jf_playback_load_external_subtitles(void) 159 | { 160 | char subs_language[4]; 161 | size_t i, j; 162 | jf_menu_item *child; 163 | 164 | // external subtitles 165 | // note: they unfortunately require loadfile to already have been issued 166 | subs_language[3] = '\0'; 167 | for (i = 0; i < g_state.now_playing->children_count; i++) { 168 | for (j = 0; j < g_state.now_playing->children[i]->children_count; j++) { 169 | child = g_state.now_playing->children[i]->children[j]; 170 | if (child->type != JF_ITEM_TYPE_VIDEO_SUB) { 171 | fprintf(stderr, 172 | "Warning: unrecognized g_state.now_playing type (%s) for %s, part %zu, child %zu. This is a bug.\n", 173 | jf_item_type_get_name(child->type), 174 | g_state.now_playing->name, 175 | i, 176 | j); 177 | continue; 178 | } 179 | 180 | // tmp = jf_menu_item_get_request_url(child); 181 | strncpy(subs_language, child->id, 3); 182 | const char *command[] = { "sub-add", 183 | jf_menu_item_get_request_url(child), 184 | "auto", 185 | child->id + 3, 186 | subs_language, 187 | NULL }; 188 | if (mpv_command(g_mpv_ctx, command) < 0) { 189 | jf_reply *r = jf_net_request(child->name, 190 | JF_REQUEST_IN_MEMORY, 191 | JF_HTTP_GET, 192 | NULL); 193 | fprintf(stderr, 194 | "Warning: external subtitle %s could not be loaded.\n", 195 | child->id[3] != '\0' ? child->id + 3 : child->name); 196 | if (r->state == JF_REPLY_ERROR_HTTP_400) { 197 | fprintf(stderr, "Reason: %s.\n", r->payload); 198 | } 199 | jf_reply_free(r); 200 | } 201 | } 202 | } 203 | 204 | } 205 | 206 | 207 | void jf_playback_align_subtitle(const int64_t sid) 208 | { 209 | int64_t track_count, track_id, playback_ticks, sub_delay; 210 | size_t i; 211 | long long offset_ticks; 212 | int success, is_external; 213 | bool is_sub; 214 | char num[3]; 215 | char *track_type, *tmp; 216 | 217 | if (g_state.now_playing->children_count <= 1) return; 218 | 219 | // look for right track 220 | if (mpv_get_property(g_mpv_ctx, "track-list/count", MPV_FORMAT_INT64, &track_count) != 0) return; 221 | i = 0; // track-numbers are 0-based 222 | while (true) { 223 | if ((int64_t)i >= track_count) return; 224 | success = snprintf(num, 3, "%ld", i); 225 | if (success < 0 || success >= 3) { 226 | i++; 227 | continue; 228 | } 229 | tmp = jf_concat(3, "track-list/", num, "/id"); 230 | success = mpv_get_property(g_mpv_ctx, tmp, MPV_FORMAT_INT64, &track_id); 231 | free(tmp); 232 | if (success != 0) { 233 | i++; 234 | continue; 235 | } 236 | tmp = jf_concat(3, "track-list/", num, "/type"); 237 | success = mpv_get_property(g_mpv_ctx, tmp, MPV_FORMAT_STRING, &track_type); 238 | free(tmp); 239 | if (success != 0) { 240 | i++; 241 | continue; 242 | } 243 | is_sub = strcmp(track_type, "sub") == 0; 244 | mpv_free(track_type); 245 | if (track_id == sid && is_sub) break; 246 | i++; 247 | } 248 | 249 | // check if external 250 | tmp = jf_concat(3, "track-list/", num, "/external"); 251 | success = mpv_get_property(g_mpv_ctx, tmp, MPV_FORMAT_FLAG, &is_external); 252 | free(tmp); 253 | if (success != 0) { 254 | fprintf(stderr, 255 | "Warning: could not align subtitle track to split-file: mpv_get_property (external): %s.\n", 256 | mpv_error_string(success)); 257 | return; 258 | } 259 | if (is_external) { 260 | // compute offset 261 | success = mpv_get_property(g_mpv_ctx, "time-pos", MPV_FORMAT_INT64, &playback_ticks); 262 | if (success != 0) { 263 | fprintf(stderr, 264 | "Warning: could not align subtitle track to split-file: mpv_get_property (time-pos): %s.\n", 265 | mpv_error_string(success)); 266 | return; 267 | } 268 | playback_ticks = JF_SECS_TO_TICKS(playback_ticks); 269 | offset_ticks = 0; 270 | i = 0; 271 | while (i < g_state.now_playing->children_count 272 | && offset_ticks + g_state.now_playing->children[i]->runtime_ticks <= playback_ticks) { 273 | offset_ticks += g_state.now_playing->children[i]->runtime_ticks; 274 | i++; 275 | } 276 | sub_delay = JF_TICKS_TO_SECS(offset_ticks); 277 | 278 | // apply 279 | success = mpv_set_property(g_mpv_ctx, "sub-delay", MPV_FORMAT_INT64, &sub_delay); 280 | if (success != 0) { 281 | fprintf(stderr, 282 | "Warning: could not align subtitle track to split-file: mpv_set_property: %s.\n", 283 | mpv_error_string(success)); 284 | } 285 | } else { 286 | // internal are graciously aligned by EDL protocol: 0 offset 287 | sub_delay = 0; 288 | success = mpv_set_property(g_mpv_ctx, "sub-delay", MPV_FORMAT_INT64, &sub_delay); 289 | if (success != 0) { 290 | fprintf(stderr, 291 | "Warning: could not align subtitle track to split-file: mpv_set_property: %s.\n", 292 | mpv_error_string(success)); 293 | } 294 | } 295 | } 296 | /////////////////////////////// 297 | 298 | 299 | ////////// ITEM PLAYBACK ////////// 300 | void jf_playback_play_video(const jf_menu_item *item) 301 | { 302 | jf_growing_buffer filename; 303 | char *part_url; 304 | size_t i; 305 | jf_menu_item *child; 306 | 307 | // merge video files 308 | JF_MPV_ASSERT(mpv_set_property_string(g_mpv_ctx, "force-media-title", item->name)); 309 | JF_MPV_ASSERT(mpv_set_property_string(g_mpv_ctx, "title", item->name)); 310 | filename = jf_growing_buffer_new(128); 311 | jf_growing_buffer_append(filename, "edl://", JF_STATIC_STRLEN("edl://")); 312 | for (i = 0; i < item->children_count; i++) { 313 | child = item->children[i]; 314 | if (child->type != JF_ITEM_TYPE_VIDEO_SOURCE) { 315 | fprintf(stderr, 316 | "Warning: unrecognized item type (%s) for %s part %zu. This is a bug.\n", 317 | jf_item_type_get_name(child->type), item->name, i); 318 | continue; 319 | } 320 | part_url = jf_menu_item_get_request_url(child); 321 | jf_growing_buffer_sprintf(filename, 0, "%%%zu%%%s", strlen(part_url), part_url); 322 | jf_growing_buffer_append(filename, ";", 1); 323 | } 324 | jf_growing_buffer_append(filename, "", 1); 325 | const char *loadfile[] = { "loadfile", filename->buf, NULL }; 326 | JF_DEBUG_PRINTF("loadfile %s\n", filename->buf); 327 | JF_MPV_ASSERT(mpv_command(g_mpv_ctx, loadfile)); 328 | jf_growing_buffer_free(filename); 329 | 330 | // external subtitles will be loaded at MPV_EVENT_START_FILE 331 | // after loadfile has been evaded 332 | } 333 | 334 | 335 | bool jf_playback_play_item(jf_menu_item *item) 336 | { 337 | char *request_url; 338 | jf_reply *replies[2]; 339 | 340 | if (item == NULL) { 341 | return false; 342 | } 343 | 344 | if (JF_ITEM_TYPE_IS_FOLDER(item->type)) { 345 | fprintf(stderr, "Error: jf_menu_play_item invoked on folder item type. This is a bug.\n"); 346 | return false; 347 | } 348 | 349 | switch (item->type) { 350 | case JF_ITEM_TYPE_AUDIO: 351 | case JF_ITEM_TYPE_AUDIOBOOK: 352 | if ((request_url = jf_menu_item_get_request_url(item)) == NULL) { 353 | fprintf(stderr, "Error: jf_playback_play_item: jf_menu_item_get_request_url returned NULL. This is a bug.\n"); 354 | jf_playback_end(); 355 | return false; 356 | } 357 | if (jf_menu_ask_resume(item) == false) { 358 | jf_playback_end(); 359 | return false; 360 | } 361 | JF_MPV_ASSERT(mpv_set_property_string(g_mpv_ctx, "title", item->name)); 362 | const char *loadfile[] = { "loadfile", request_url, NULL }; // TODO debug print 363 | mpv_command(g_mpv_ctx, loadfile); 364 | jf_menu_item_free(g_state.now_playing); 365 | g_state.now_playing = item; 366 | break; 367 | case JF_ITEM_TYPE_EPISODE: 368 | case JF_ITEM_TYPE_MOVIE: 369 | case JF_ITEM_TYPE_MUSIC_VIDEO: 370 | // check if item was already evaded re: split file and versions 371 | if (item->children_count > 0) { 372 | if (jf_menu_ask_resume(item) == false) { 373 | jf_playback_end(); 374 | return false; 375 | } 376 | jf_playback_play_video(item); 377 | } else { 378 | request_url = jf_menu_item_get_request_url(item); 379 | replies[0] = jf_net_request(request_url, 380 | JF_REQUEST_ASYNC_IN_MEMORY, 381 | JF_HTTP_GET, 382 | NULL); 383 | request_url = jf_concat(3, "/videos/", item->id, "/additionalparts"); 384 | replies[1] = jf_net_request(request_url, 385 | JF_REQUEST_IN_MEMORY, 386 | JF_HTTP_GET, 387 | NULL); 388 | if (JF_REPLY_PTR_HAS_ERROR(replies[1])) { 389 | fprintf(stderr, 390 | "Error: network request for /additionalparts of item %s failed: %s.\n", 391 | item->name, 392 | jf_reply_error_string(replies[1])); 393 | jf_reply_free(replies[1]); 394 | jf_reply_free(jf_net_await(replies[0])); 395 | jf_playback_end(); 396 | return false; 397 | } 398 | if (JF_REPLY_PTR_HAS_ERROR(jf_net_await(replies[0]))) { 399 | fprintf(stderr, 400 | "Error: network request for item %s failed: %s.\n", 401 | item->name, 402 | jf_reply_error_string(replies[0])); 403 | jf_reply_free(replies[0]); 404 | jf_reply_free(replies[1]); 405 | jf_playback_end(); 406 | return false; 407 | } 408 | jf_json_parse_video(item, replies[0]->payload, replies[1]->payload); 409 | jf_reply_free(replies[0]); 410 | jf_reply_free(replies[1]); 411 | if (jf_playback_populate_video_ticks(item) == false 412 | || jf_menu_ask_resume(item) == false) { 413 | jf_playback_end(); 414 | return false; 415 | } 416 | jf_playback_play_video(item); 417 | } 418 | jf_disk_playlist_replace_item(g_state.playlist_position, item); 419 | jf_menu_item_free(g_state.now_playing); 420 | g_state.now_playing = item; 421 | break; 422 | default: 423 | fprintf(stderr, 424 | "Error: jf_menu_play_item unsupported type (%s). This is a bug.\n", 425 | jf_item_type_get_name(item->type)); 426 | return false; 427 | } 428 | 429 | return true; 430 | } 431 | 432 | 433 | static inline bool jf_playback_populate_video_ticks(jf_menu_item *item) 434 | { 435 | jf_reply **replies; 436 | jf_growing_buffer part_url = jf_growing_buffer_new(0); 437 | size_t i; 438 | 439 | if (item == NULL) return true; 440 | if (item->type != JF_ITEM_TYPE_EPISODE 441 | && item->type != JF_ITEM_TYPE_MOVIE) return true; 442 | 443 | // the Emby interface was designed by a drunk gibbon. to check for 444 | // a progress marker, we have to request the items corresponding to 445 | // the additionalparts and look at them individually 446 | // ...and each may have its own bookmark! 447 | 448 | // parent and first child refer the same ID, thus the same part 449 | item->children[0]->playback_ticks = item->playback_ticks; 450 | // but at this point it makes no sense for the parent item to have a PB 451 | // tick since there may be multiple markers 452 | item->playback_ticks = 0; 453 | 454 | // now go and get all markers for all parts 455 | assert((replies = malloc((item->children_count - 1) * sizeof(jf_reply *))) != NULL); 456 | for (i = 1; i < item->children_count; i++) { 457 | jf_growing_buffer_empty(part_url); 458 | jf_growing_buffer_sprintf(part_url, 0, 459 | "/users/%s/items/%s", 460 | g_options.userid, 461 | item->children[i]->id); 462 | replies[i - 1] = jf_net_request(part_url->buf, 463 | JF_REQUEST_ASYNC_IN_MEMORY, 464 | JF_HTTP_GET, 465 | NULL); 466 | } 467 | jf_growing_buffer_free(part_url); 468 | 469 | for (i = 1; i < item->children_count; i++) { 470 | jf_net_await(replies[i - 1]); 471 | if (JF_REPLY_PTR_HAS_ERROR(replies[i - 1])) { 472 | fprintf(stderr, 473 | "Error: could not fetch resume information for part %zu of item %s: %s.\n", 474 | i + 1, 475 | item->name, 476 | jf_reply_error_string(replies[i - 1])); 477 | for (i = 1; i < item->children_count; i++) { 478 | if (replies[i - 1] != NULL) { 479 | jf_net_await(replies[i - 1]); 480 | jf_reply_free(replies[i - 1]); 481 | } 482 | } 483 | free(replies); 484 | return false; 485 | } 486 | jf_json_parse_playback_ticks(item->children[i], replies[i - 1]->payload); 487 | jf_reply_free(replies[i - 1]); 488 | } 489 | free(replies); 490 | return true; 491 | } 492 | /////////////////////////////////// 493 | 494 | 495 | ////////// PLAYLIST CONTROLS ////////// 496 | bool jf_playback_next(void) 497 | { 498 | jf_menu_item *item; 499 | bool more_playback; 500 | 501 | if (g_state.playlist_position == jf_disk_playlist_item_count()) { 502 | if (g_state.playlist_loops == 1 || g_state.playlist_loops == 0) return false; 503 | g_state.playlist_position = 1; 504 | g_state.playlist_loops--; 505 | } else { 506 | if (g_state.playlist_loops > 1) { 507 | g_state.loop_state = JF_LOOP_STATE_OUT_OF_SYNC; 508 | } 509 | g_state.playlist_position++; 510 | } 511 | 512 | g_state.state = JF_STATE_PLAYLIST_SEEKING; 513 | 514 | item = jf_disk_playlist_get_item(g_state.playlist_position); 515 | JF_DEBUG_PRINTF("Skipping to item PRE-evasion:\n"); 516 | #ifdef JF_DEBUG 517 | jf_menu_item_print(item); 518 | #endif 519 | more_playback = jf_playback_play_item(item); 520 | 521 | JF_DEBUG_PRINTF("Skipping to item POST-evasion:\n"); 522 | #ifdef JF_DEBUG 523 | jf_menu_item_print(item); 524 | #endif 525 | 526 | return more_playback; 527 | } 528 | 529 | 530 | bool jf_playback_previous(void) 531 | { 532 | jf_menu_item *item; 533 | bool more_playback; 534 | 535 | if (g_state.playlist_position == 1) { 536 | if (g_state.playlist_loops == 1 || g_state.playlist_loops == 0) return false; 537 | g_state.playlist_position = jf_disk_playlist_item_count(); 538 | // don't decrement the playlist loop counter going backwards 539 | // since that's how mpv does it 540 | } else { 541 | // NB going backwards will not trigger options/playlist-loop decrement 542 | g_state.playlist_position--; 543 | } 544 | 545 | g_state.state = JF_STATE_PLAYLIST_SEEKING; 546 | 547 | item = jf_disk_playlist_get_item(g_state.playlist_position); 548 | JF_DEBUG_PRINTF("Skipping to item PRE-evasion:\n"); 549 | #ifdef JF_DEBUG 550 | jf_menu_item_print(item); 551 | #endif 552 | more_playback = jf_playback_play_item(item); 553 | 554 | JF_DEBUG_PRINTF("Skipping to item POST-evasion:\n"); 555 | #ifdef JF_DEBUG 556 | jf_menu_item_print(item); 557 | #endif 558 | 559 | return more_playback; 560 | } 561 | 562 | 563 | void jf_playback_end(void) 564 | { 565 | // kill playback core 566 | mpv_terminate_destroy(g_mpv_ctx); 567 | g_mpv_ctx = NULL; 568 | // enforce a clean state for the application 569 | jf_menu_item_free(g_state.now_playing); 570 | g_state.now_playing = NULL; 571 | g_state.playlist_position = 0; 572 | s_last_playlist_print = 0; 573 | // signal to enter UI mode 574 | g_state.state = JF_STATE_MENU_UI; 575 | } 576 | 577 | 578 | void jf_playback_shuffle_playlist(void) 579 | { 580 | size_t pos = g_state.playlist_position - 1; 581 | size_t item_count_no_curr = jf_disk_playlist_item_count() - 1; 582 | size_t i; 583 | size_t j; 584 | 585 | for (i = 0; i < item_count_no_curr - 1; i++) { 586 | if (i == pos) continue; 587 | if ((j = (size_t)random() % (item_count_no_curr - i - 1) + i + 1) >= pos) { 588 | j++; 589 | } 590 | jf_disk_playlist_swap_items(i + 1, j + 1); 591 | } 592 | } 593 | 594 | 595 | static void jf_playback_playlist_window(size_t window_size, size_t window[2]) 596 | { 597 | size_t pos = g_state.playlist_position; 598 | size_t item_count = jf_disk_playlist_item_count(); 599 | 600 | if (window_size == 0) { 601 | window_size = item_count; 602 | } 603 | 604 | // the window size is guaranteed (if there are enough items) 605 | // the window slides without ever going beyond boundaries 606 | if (pos <= window_size / 2) { 607 | window[0] = 1; 608 | window[1] = window_size > item_count ? item_count : window[0] + window_size - 1; 609 | } else if (pos + window_size / 2 > item_count) { 610 | window[1] = item_count; 611 | window[0] = window_size >= window[1] ? 1 : window[1] - window_size + 1; 612 | } else { 613 | window[0] = pos - window_size / 2; 614 | window[1] = window[0] + window_size - 1; 615 | } 616 | } 617 | 618 | 619 | void jf_playback_print_playlist(size_t window_size) 620 | { 621 | size_t i; 622 | size_t pos = g_state.playlist_position; 623 | size_t terminal[2]; 624 | int is_video; 625 | int64_t osd_h; 626 | int64_t osd_font_size; 627 | size_t osd[2]; 628 | jf_growing_buffer osd_msg; 629 | const char *osd_cmd[3] = { "show-text", NULL, NULL }; 630 | 631 | // print to terminal, but only if we didn't already do it for this item and 632 | // this playlist. we must print manually because `show-text` doesn't print 633 | // to terminal during video playback. but we don't want to flood the term 634 | // with repeat prints 635 | if (g_state.playlist_position != s_last_playlist_print) { 636 | jf_mpv_terminal(g_mpv_ctx, false); 637 | 638 | jf_term_clear_bottom(NULL); 639 | 640 | jf_playback_playlist_window(window_size, terminal); 641 | fprintf(stdout, "\n===== jftui playlist (%zu items) =====\n", jf_disk_playlist_item_count()); 642 | for (i = terminal[0]; i < pos; i++) { 643 | fprintf(stdout, "%zu: %s\n", i, jf_disk_playlist_get_item_name(i)); 644 | } 645 | fprintf(stdout, "\t>>> %zu: %s <<<\n", i, g_state.now_playing->name); 646 | for (i = pos + 1; i <= terminal[1]; i++) { 647 | fprintf(stdout, "%zu: %s\n", i, jf_disk_playlist_get_item_name(i)); 648 | } 649 | fprintf(stdout, "\n"); 650 | 651 | jf_mpv_terminal(g_mpv_ctx, true); 652 | 653 | s_last_playlist_print = g_state.playlist_position; 654 | } 655 | 656 | // if there is a video output, print to OSD there too 657 | JF_MPV_ASSERT(mpv_get_property(g_mpv_ctx, "vo-configured", MPV_FORMAT_FLAG, &is_video)); 658 | if (! is_video) return; 659 | 660 | // prepare OSD string 661 | JF_MPV_ASSERT(mpv_get_property(g_mpv_ctx, "osd-height", MPV_FORMAT_INT64, &osd_h)); 662 | JF_MPV_ASSERT(mpv_get_property(g_mpv_ctx, "osd-font-size", MPV_FORMAT_INT64, &osd_font_size)); 663 | // bad heuristic but better than nothing 664 | jf_playback_playlist_window((size_t)(osd_h/osd_font_size/2), osd); 665 | osd_msg = jf_growing_buffer_new(0); 666 | jf_growing_buffer_sprintf(osd_msg, 0, "===== jftui playlist (%zu items) =====", jf_disk_playlist_item_count()); 667 | for (i = osd[0]; i < pos; i++) { 668 | jf_growing_buffer_sprintf(osd_msg, 0, "\n%zu: %s", i, jf_disk_playlist_get_item_name(i)); 669 | } 670 | jf_growing_buffer_sprintf(osd_msg, 0, "\n\t>>> %zu: %s <<<", i, g_state.now_playing->name); 671 | for (i = pos + 1; i <= osd[1]; i++) { 672 | jf_growing_buffer_sprintf(osd_msg, 0, "\n%zu: %s", i, jf_disk_playlist_get_item_name(i)); 673 | } 674 | jf_growing_buffer_append(osd_msg, "", 1); 675 | 676 | // print to OSD 677 | osd_cmd[1] = osd_msg->buf; 678 | JF_MPV_ASSERT(mpv_command(g_mpv_ctx, osd_cmd)); 679 | jf_growing_buffer_free(osd_msg); 680 | } 681 | /////////////////////////////////////// 682 | -------------------------------------------------------------------------------- /src/playback.h: -------------------------------------------------------------------------------- 1 | #ifndef _JF_PLAYBACK 2 | #define _JF_PLAYBACK 3 | 4 | 5 | #include "shared.h" 6 | 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | 13 | // Update playback progress marker of the currently playing item on the server 14 | // (as of g_state.now_playing). 15 | // Detect if we moved across split-file parts since the last such update and 16 | // mark parts previous to current as played, next to current as unplayed (so 17 | // that the item only has one overall progress marker on the server). 18 | // 19 | // Parameters: 20 | // - playback_ticks: current position in Jellyfin ticks, referring to the 21 | // whole merged file in case of split-part. 22 | // 23 | // jf_playback_update_playing will POST to /sessions/playing/played 24 | // and should thus be called for playback that is starting. 25 | // 26 | // jf_playback_update_progress will POST to /sessions/playing/progress 27 | // and should thus be called for ongoing playback. 28 | // 29 | // jf_playback_update_stopped will POST to /sessions/playing/stopped 30 | // and should thus be called for playback that just ended. 31 | void jf_playback_update_playing(const int64_t playback_ticks); 32 | void jf_playback_update_progress(const int64_t playback_ticks); 33 | void jf_playback_update_stopped(const int64_t playback_ticks); 34 | 35 | 36 | void jf_playback_load_external_subtitles(void); 37 | void jf_playback_align_subtitle(const int64_t sid); 38 | 39 | 40 | bool jf_playback_play_item(jf_menu_item *item); 41 | void jf_playback_play_video(const jf_menu_item *item); 42 | bool jf_playback_next(void); 43 | bool jf_playback_previous(void); 44 | void jf_playback_end(void); 45 | // won't move item currently playing 46 | void jf_playback_shuffle_playlist(void); 47 | 48 | // Will print part or the entirety of the current jftui playback playlist to 49 | // stdout. 50 | // 51 | // - slice_height: number of items before AND after to try to print. 52 | // If 0 will print whole playlist. 53 | // 54 | // CAN'T FAIL. 55 | void jf_playback_print_playlist(size_t slice_height); 56 | #endif 57 | -------------------------------------------------------------------------------- /src/shared.c: -------------------------------------------------------------------------------- 1 | #include "shared.h" 2 | #include "config.h" 3 | 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | ////////// GLOBALS ////////// 13 | extern jf_global_state g_state; 14 | extern jf_options g_options; 15 | extern mpv_handle *g_mpv_ctx; 16 | ///////////////////////////// 17 | 18 | 19 | ////////// STATIC FUNCTIONS ////////// 20 | #ifdef JF_DEBUG 21 | static void jf_menu_item_print_indented(const jf_menu_item *item, const size_t level); 22 | #endif 23 | inline static void jf_growing_buffer_make_space(jf_growing_buffer buffer, 24 | size_t to_add); 25 | ////////////////////////////////////// 26 | 27 | 28 | ////////// JF_MENU_ITEM ////////// 29 | const char *jf_item_type_get_name(const jf_item_type type) 30 | { 31 | switch (type) { 32 | case JF_ITEM_TYPE_NONE: 33 | return "None"; 34 | case JF_ITEM_TYPE_AUDIO: 35 | return "Audio"; 36 | case JF_ITEM_TYPE_AUDIOBOOK: 37 | return "Audiobook"; 38 | case JF_ITEM_TYPE_EPISODE: 39 | return "Episode"; 40 | case JF_ITEM_TYPE_MOVIE: 41 | return "Movie"; 42 | case JF_ITEM_TYPE_VIDEO_SOURCE: 43 | return "Video_Source"; 44 | case JF_ITEM_TYPE_VIDEO_SUB: 45 | return "Video_Sub"; 46 | case JF_ITEM_TYPE_COLLECTION: 47 | return "Collection_Generic"; 48 | case JF_ITEM_TYPE_COLLECTION_MUSIC: 49 | return "Collection_Music"; 50 | case JF_ITEM_TYPE_COLLECTION_SERIES: 51 | return "Collection_Series"; 52 | case JF_ITEM_TYPE_COLLECTION_MOVIES: 53 | return "Collection_Movies"; 54 | case JF_ITEM_TYPE_COLLECTION_MUSIC_VIDEOS: 55 | return "Collection_Music_Videos"; 56 | case JF_ITEM_TYPE_USER_VIEW: 57 | return "User_View"; 58 | case JF_ITEM_TYPE_FOLDER: 59 | return "Folder"; 60 | case JF_ITEM_TYPE_PLAYLIST: 61 | return "Playlist"; 62 | case JF_ITEM_TYPE_ARTIST: 63 | return "Artist"; 64 | case JF_ITEM_TYPE_ALBUM: 65 | return "Album"; 66 | case JF_ITEM_TYPE_SEASON: 67 | return "Season"; 68 | case JF_ITEM_TYPE_SERIES: 69 | return "Series"; 70 | case JF_ITEM_TYPE_SEARCH_RESULT: 71 | return "Search_Result"; 72 | case JF_ITEM_TYPE_MENU_ROOT: 73 | case JF_ITEM_TYPE_MENU_FAVORITES: 74 | case JF_ITEM_TYPE_MENU_CONTINUE: 75 | case JF_ITEM_TYPE_MENU_NEXT_UP: 76 | case JF_ITEM_TYPE_MENU_LATEST_ADDED: 77 | case JF_ITEM_TYPE_MENU_LIBRARIES: 78 | return "Persistent_Folder"; 79 | default: 80 | return "Unrecognized"; 81 | } 82 | } 83 | 84 | 85 | jf_menu_item *jf_menu_item_new(jf_item_type type, 86 | jf_menu_item **children, 87 | const size_t children_count, 88 | const char *id, 89 | const char *name, 90 | const char *path, 91 | const long long runtime_ticks, 92 | const long long playback_ticks) 93 | { 94 | jf_menu_item *menu_item; 95 | size_t name_length = jf_strlen(name); 96 | size_t path_length = jf_strlen(path); 97 | 98 | assert((menu_item = malloc(sizeof(jf_menu_item) 99 | + name_length 100 | + path_length)) != NULL); 101 | 102 | menu_item->type = type; 103 | 104 | menu_item->children = children; 105 | menu_item->children_count = children_count; 106 | 107 | if (id == NULL) { 108 | menu_item->id[0] = '\0'; 109 | } else { 110 | memcpy(menu_item->id, id, JF_ID_LENGTH); 111 | menu_item->id[JF_ID_LENGTH] = '\0'; 112 | } 113 | 114 | if (name == NULL) { 115 | menu_item->name = NULL; 116 | } else { 117 | menu_item->name = (char *)menu_item + sizeof(jf_menu_item); 118 | memcpy(menu_item->name, name, name_length); 119 | } 120 | 121 | if (path == NULL) { 122 | menu_item->path = NULL; 123 | } else { 124 | menu_item->path = (char *)menu_item + sizeof(jf_menu_item) + name_length; 125 | memcpy(menu_item->path, path, path_length); 126 | } 127 | menu_item->runtime_ticks = runtime_ticks; 128 | menu_item->playback_ticks = playback_ticks; 129 | 130 | return menu_item; 131 | } 132 | 133 | 134 | void jf_menu_item_free(jf_menu_item *menu_item) 135 | { 136 | size_t i; 137 | 138 | if (menu_item == NULL) { 139 | return; 140 | } 141 | 142 | if (! (JF_ITEM_TYPE_IS_PERSISTENT(menu_item->type))) { 143 | for (i = 0; i < menu_item->children_count; i++) { 144 | jf_menu_item_free(menu_item->children[i]); 145 | } 146 | free(menu_item->children); 147 | free(menu_item); 148 | } 149 | } 150 | 151 | 152 | #ifdef JF_DEBUG 153 | static void jf_menu_item_print_indented(const jf_menu_item *item, const size_t level) 154 | { 155 | size_t i; 156 | 157 | if (item == NULL) return; 158 | 159 | JF_PRINTF_INDENT("Name: %s\n", item->name); 160 | JF_PRINTF_INDENT("Path: %s\n", item->path); 161 | JF_PRINTF_INDENT("Type: %s\n", jf_item_type_get_name(item->type)); 162 | if (item->type == JF_ITEM_TYPE_VIDEO_SUB) { 163 | JF_PRINTF_INDENT("Id: %s|%s\n", item->id, item->id + 3); 164 | } else { 165 | JF_PRINTF_INDENT("Id: %s\n", item->id); 166 | } 167 | JF_PRINTF_INDENT("PB ticks: %lld, RT ticks: %lld\n", item->playback_ticks, item->runtime_ticks); 168 | if (item->children_count > 0) { 169 | JF_PRINTF_INDENT("Children:\n"); 170 | for (i = 0; i < item->children_count; i++) { 171 | jf_menu_item_print_indented(item->children[i], level + 1); 172 | } 173 | } 174 | } 175 | 176 | 177 | void jf_menu_item_print(const jf_menu_item *item) 178 | { 179 | if (item == NULL) { 180 | printf("Null item\n"); 181 | return; 182 | } 183 | 184 | jf_menu_item_print_indented(item, 0); 185 | } 186 | #endif 187 | ////////////////////////////////// 188 | 189 | 190 | ////////// THREAD BUFFER ////////// 191 | void jf_thread_buffer_init(jf_thread_buffer *tb) 192 | { 193 | tb->used = 0; 194 | tb->promiscuous_context = false; 195 | tb->state = JF_THREAD_BUFFER_STATE_CLEAR; 196 | tb->item_count = 0; 197 | assert(pthread_mutex_init(&tb->mut, NULL) == 0); 198 | assert(pthread_cond_init(&tb->cv_no_data, NULL) == 0); 199 | assert(pthread_cond_init(&tb->cv_has_data, NULL) == 0); 200 | } 201 | /////////////////////////////////// 202 | 203 | 204 | ////////// GROWING BUFFER ////////// 205 | jf_growing_buffer jf_growing_buffer_new(const size_t size) 206 | { 207 | jf_growing_buffer buffer; 208 | assert((buffer = malloc(sizeof(struct _jf_growing_buffer))) != NULL); 209 | assert((buffer->buf = malloc(size > 0 ? size : 1024)) != NULL); 210 | buffer->size = size > 0 ? size : 1024; 211 | buffer->used = 0; 212 | return buffer; 213 | } 214 | 215 | 216 | inline static void jf_growing_buffer_make_space(jf_growing_buffer buffer, 217 | const size_t required) 218 | { 219 | size_t estimate; 220 | 221 | if (buffer == NULL) return; 222 | 223 | if (buffer->used + required > buffer->size) { 224 | estimate = (buffer->used + required ) / 2 * 3; 225 | buffer->size = estimate >= buffer->size * 2 ? estimate : buffer->size * 2; 226 | assert((buffer->buf = realloc(buffer->buf, buffer->size)) != NULL); 227 | } 228 | } 229 | 230 | 231 | void jf_growing_buffer_append(jf_growing_buffer buffer, 232 | const void *data, 233 | size_t length) 234 | { 235 | if (buffer == NULL) return; 236 | 237 | if (length == 0) { 238 | length = strlen(data); 239 | } 240 | 241 | jf_growing_buffer_make_space(buffer, length); 242 | memcpy(buffer->buf + buffer->used, data, length); 243 | buffer->used += length; 244 | } 245 | 246 | 247 | void jf_growing_buffer_sprintf(jf_growing_buffer buffer, 248 | size_t offset, 249 | const char *format, 250 | ...) 251 | { 252 | int sprintf_len; 253 | va_list ap; 254 | 255 | if (buffer == NULL) return; 256 | 257 | if (offset == 0) { 258 | offset = buffer->used; 259 | } 260 | 261 | va_start(ap, format); 262 | // count terminating NULL too or output loses last character 263 | assert((sprintf_len = vsnprintf(NULL, 0, format, ap) + 1) != -1); 264 | va_end(ap); 265 | 266 | jf_growing_buffer_make_space(buffer, offset + (size_t)sprintf_len - buffer->used); 267 | 268 | va_start(ap, format); 269 | // so this DOES write the terminating NULL as well 270 | assert(vsnprintf(buffer->buf + offset, (size_t)sprintf_len, format, ap) 271 | == sprintf_len - 1); 272 | va_end(ap); 273 | 274 | // but we ignore that it's there 275 | buffer->used += (size_t)sprintf_len - 1; 276 | } 277 | 278 | 279 | void jf_growing_buffer_empty(jf_growing_buffer buffer) 280 | { 281 | if (buffer == NULL) return; 282 | 283 | buffer->used = 0; 284 | } 285 | 286 | 287 | void jf_growing_buffer_free(jf_growing_buffer buffer) 288 | { 289 | if (buffer == NULL) return; 290 | 291 | free(buffer->buf); 292 | free(buffer); 293 | } 294 | //////////////////////////////////// 295 | 296 | 297 | ////////// SYNCED QUEUE ////////// 298 | jf_synced_queue *jf_synced_queue_new(const size_t slots) 299 | { 300 | jf_synced_queue *q; 301 | 302 | assert((q = malloc(sizeof(jf_synced_queue))) != NULL); 303 | assert((q->slots = calloc(slots, sizeof(void *))) != NULL); 304 | q->slot_count = slots; 305 | q->current = 0; 306 | q->next = 0; 307 | assert(pthread_mutex_init(&q->mut, NULL) == 0); 308 | assert(pthread_cond_init(&q->cv_is_empty, NULL) == 0); 309 | assert(pthread_cond_init(&q->cv_is_full, NULL) == 0); 310 | return q; 311 | } 312 | 313 | 314 | void jf_synced_queue_free(jf_synced_queue *q) 315 | { 316 | free(q); 317 | } 318 | 319 | 320 | void jf_synced_queue_enqueue(jf_synced_queue *q, const void *payload) 321 | { 322 | if (payload == NULL) return; 323 | 324 | pthread_mutex_lock(&q->mut); 325 | while (q->slots[q->next] != NULL) { 326 | pthread_cond_wait(&q->cv_is_full, &q->mut); 327 | } 328 | q->slots[q->next] = payload; 329 | q->next = (q->next + 1) % q->slot_count; 330 | pthread_mutex_unlock(&q->mut); 331 | pthread_cond_signal(&q->cv_is_empty); 332 | } 333 | 334 | 335 | void *jf_synced_queue_dequeue(jf_synced_queue *q) 336 | { 337 | void *payload; 338 | 339 | pthread_mutex_lock(&q->mut); 340 | while (q->slots[q->current] == NULL) { 341 | pthread_cond_wait(&q->cv_is_empty, &q->mut); 342 | } 343 | payload = (void *)q->slots[q->current]; 344 | q->slots[q->current] = NULL; 345 | q->current = (q->current + 1) % q->slot_count; 346 | pthread_mutex_unlock(&q->mut); 347 | pthread_cond_signal(&q->cv_is_full); 348 | 349 | return payload; 350 | } 351 | ////////////////////////////////// 352 | 353 | 354 | ////////// MISCELLANEOUS GARBAGE ////////// 355 | #define STRNCASECMP_LITERAL(_str, _lit, _len) strncasecmp(_str, _lit, _len > JF_STATIC_STRLEN(_lit) ? JF_STATIC_STRLEN(_lit) : _len) 356 | 357 | 358 | bool jf_strong_bool_parse(const char *str, 359 | const size_t len, 360 | jf_strong_bool *out) 361 | { 362 | size_t l; 363 | 364 | if (str == NULL) return false; 365 | 366 | l = len > 0 ? len : strlen(str); 367 | 368 | if (STRNCASECMP_LITERAL(str, "no", l) == 0) { 369 | *out = JF_STRONG_BOOL_NO; 370 | return true; 371 | } 372 | if (STRNCASECMP_LITERAL(str, "yes", l) == 0) { 373 | *out = JF_STRONG_BOOL_YES; 374 | return true; 375 | } 376 | if (STRNCASECMP_LITERAL(str, "force", l) == 0) { 377 | *out = JF_STRONG_BOOL_FORCE; 378 | return true; 379 | } 380 | 381 | return false; 382 | } 383 | 384 | 385 | const char *jf_strong_bool_to_str(jf_strong_bool strong_bool) 386 | { 387 | switch (strong_bool) { 388 | case JF_STRONG_BOOL_NO: 389 | return "no"; 390 | case JF_STRONG_BOOL_YES: 391 | return "yes"; 392 | case JF_STRONG_BOOL_FORCE: 393 | return "force"; 394 | } 395 | 396 | return NULL; 397 | } 398 | 399 | 400 | char *jf_concat(size_t n, ...) 401 | { 402 | char *buf; 403 | char *tmp; 404 | size_t len = 0; 405 | size_t i; 406 | va_list ap; 407 | 408 | va_start(ap, n); 409 | for (i = 0; i < n; i++) { 410 | len += strlen(va_arg(ap, const char*)); 411 | } 412 | va_end(ap); 413 | 414 | assert((buf = malloc(len + 1)) != NULL); 415 | tmp = buf; 416 | va_start(ap, n); 417 | for (i = 0; i < n; i++) { 418 | tmp = memccpy(tmp, va_arg(ap, const char*), '\0', (size_t)(buf + len - tmp + 1)); 419 | // you should check tmp != NULL here but that happens only if there's no \0 in src 420 | // and we've already run strlen's on the input (#YOLO) 421 | tmp--; 422 | } 423 | buf[len] = '\0'; 424 | va_end(ap); 425 | 426 | return buf; 427 | } 428 | 429 | 430 | void jf_print_zu(size_t n) 431 | { 432 | static char str[20]; 433 | unsigned char i = 0; 434 | do { 435 | str[i++] = n % 10 + '0'; 436 | } while ((n /= 10) > 0); 437 | while (i-- != 0) { 438 | fwrite(str + i, 1, 1, stdout); 439 | } 440 | } 441 | 442 | 443 | char *jf_generate_random_id(size_t len) 444 | { 445 | char *rand_id; 446 | 447 | // default length 448 | len = len > 0 ? len : 10; 449 | 450 | assert((rand_id = malloc(len + 1)) != NULL); 451 | rand_id[len] = '\0'; 452 | for (; len > 0; len--) { 453 | rand_id[len - 1] = '0' + random() % 10; 454 | } 455 | return rand_id; 456 | } 457 | 458 | 459 | char *jf_make_timestamp(const long long ticks) 460 | { 461 | char *str; 462 | unsigned char seconds, minutes, hours; 463 | seconds = (unsigned char)((ticks / 10000000) % 60); 464 | minutes = (unsigned char)((ticks / 10000000 / 60) % 60); 465 | hours = (unsigned char)(ticks / 10000000 / 60 / 60); 466 | 467 | // allocate with overestimate. we shan't cry 468 | assert((str = malloc(sizeof("xxx:xx:xx"))) != NULL); 469 | snprintf(str, sizeof("xxx:xx:xx"), "%02u:%02u:%02u", hours, minutes, seconds); 470 | return str; 471 | } 472 | 473 | 474 | inline size_t jf_clamp_zu(const size_t zu, const size_t min, 475 | const size_t max) 476 | { 477 | return zu < min ? min : zu > max ? max : zu; 478 | } 479 | 480 | 481 | inline void jf_clear_stdin(void) 482 | { 483 | fcntl(0, F_SETFL, fcntl(0, F_GETFL)|O_NONBLOCK); 484 | while (getchar() != EOF) ; 485 | fcntl(0, F_SETFL, fcntl(0, F_GETFL)& ~O_NONBLOCK); 486 | } 487 | 488 | 489 | void jf_term_clear_bottom(FILE *stream) 490 | { 491 | struct winsize ws; 492 | size_t i; 493 | 494 | if (stream == NULL) { 495 | stream = stdout; 496 | } 497 | 498 | if (ioctl(fileno(stream), TIOCGWINSZ, &ws) < 0 || ws.ws_col == 0) return; 499 | 500 | putc('\r', stream); 501 | for (i = 0; i < ws.ws_col; i++) { 502 | putc(' ', stream); 503 | } 504 | putc('\r', stream); 505 | } 506 | 507 | 508 | size_t jf_strlen(const char *str) 509 | { 510 | return str == NULL ? 0 : strlen(str) + 1; 511 | } 512 | 513 | 514 | char *jf_make_date_one_year_ago(void) 515 | { 516 | static char date[32]; 517 | time_t now = time(NULL); 518 | struct tm my_date; 519 | 520 | gmtime_r(&now, &my_date); 521 | my_date.tm_year--; 522 | strftime(date, sizeof(date), "%F", &my_date); 523 | 524 | return date; 525 | } 526 | /////////////////////////////////////////// 527 | -------------------------------------------------------------------------------- /src/shared.h: -------------------------------------------------------------------------------- 1 | #ifndef _JF_SHARED 2 | #define _JF_SHARED 3 | 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | 14 | ////////// CODE MACROS ////////// 15 | // for hardcoded strings 16 | #define JF_STATIC_STRLEN(str) (sizeof(str) - 1) 17 | 18 | // for progress 19 | #define JF_TICKS_TO_SECS(t) (t) / 10000000 20 | #define JF_SECS_TO_TICKS(s) (s) * 10000000 21 | 22 | #define JF_MPV_ASSERT(_s) \ 23 | do { \ 24 | int _status = _s; \ 25 | if (_status < 0) { \ 26 | fprintf(stderr, "%s:%d: " #_s " failed.\n", __FILE__, __LINE__); \ 27 | fprintf(stderr, "FATAL: mpv API error: %s.\n", \ 28 | mpv_error_string(_status)); \ 29 | jf_exit(JF_EXIT_FAILURE); \ 30 | } \ 31 | } while (false) 32 | 33 | #ifdef JF_DEBUG 34 | #define JF_PRINTF_INDENT(...) \ 35 | do { \ 36 | for (i = 0; i < level; i++) { \ 37 | putchar('\t'); \ 38 | } \ 39 | printf(__VA_ARGS__); \ 40 | } while (false) 41 | #endif 42 | 43 | 44 | #ifdef JF_DEBUG 45 | #define JF_DEBUG_PRINTF(...) fprintf(stderr, "DEBUG: " __VA_ARGS__) 46 | #else 47 | #define JF_DEBUG_PRINTF(...) 48 | #endif 49 | 50 | 51 | // server version 52 | // lower 16 bits reserved for potential devel releases 53 | #define JF_SERVER_VERSION_MAKE(_major,_minor,_patch) \ 54 | (((uint64_t)(_major) << 48) | ((uint64_t)(_minor) << 32) | ((uint64_t)(_patch) << 16)) 55 | #define JF_SERVER_VERSION_GET_MAJOR(_version) ((_version) >> 48) 56 | #define JF_SERVER_VERSION_GET_MINOR(_version) (((_version) >> 32) & 0xFF) 57 | #define JF_SERVER_VERSION_GET_PATCH(_version) (((_version) >> 16) & 0xFF) 58 | 59 | 60 | #define JF_CURL_VERSION_GE(_major,_minor) \ 61 | (LIBCURL_VERSION_MAJOR == _major && LIBCURL_VERSION_MINOR >= _minor) || LIBCURL_VERSION_MAJOR >= _major 62 | ///////////////////////////////// 63 | 64 | 65 | ////////// CONSTANTS ////////// 66 | #define JF_VERSION "0.7.4" 67 | #define JF_THREAD_BUFFER_DATA_SIZE (CURL_MAX_WRITE_SIZE +1) 68 | #define JF_ID_LENGTH 32 69 | /////////////////////////////// 70 | 71 | 72 | // make sure all custom exit codes are not positive to avoid collisions 73 | // with UNIX signal identifiers 74 | #define JF_EXIT_SUCCESS 0 75 | #define JF_EXIT_FAILURE -1 76 | 77 | 78 | ////////// PROGRAM TERMINATION ////////// 79 | // Note: the code for this function is defined in the main.c TU. 80 | // Exits the program after making an attempt to perform required cleanup. 81 | // Meant as a catch-all, including normal termination and signal handling. 82 | // 83 | // Parameters: 84 | // - sig: can be a UNIX signal identifier or either JF_EXIT_FAILURE or 85 | // JF_EXIT_SUCCESS. In the latter two cases, the process wil return the 86 | // corresponding stdlib exit codes. 87 | // CAN (unsurprisingly) FATAL. 88 | void jf_exit(int sig); 89 | ///////////////////////////////////////// 90 | 91 | 92 | ////////// GENERIC JELLYFIN ITEM REPRESENTATION ////////// 93 | // Information about persistency is used to make part of the menu interface 94 | // tree not get deallocated when navigating upwards 95 | typedef enum __attribute__((__packed__)) jf_item_type { 96 | // Atoms 97 | JF_ITEM_TYPE_NONE = 0, 98 | JF_ITEM_TYPE_AUDIO = 1, 99 | JF_ITEM_TYPE_AUDIOBOOK = 2, 100 | JF_ITEM_TYPE_EPISODE = 3, 101 | JF_ITEM_TYPE_MOVIE = 4, 102 | JF_ITEM_TYPE_MUSIC_VIDEO = 5, 103 | JF_ITEM_TYPE_VIDEO_SOURCE = 6, 104 | // Subs break the usual format: 105 | // name: suffix URL for the stream. This is better computed at parse time 106 | // and cached for later use instead of computed on the fly by 107 | // as usual, since it requires more information (id, stream number, 108 | // codec) than normal. 109 | // id: given the above it would be redundant, so we use it for additional 110 | // information in the format "xxxDisplayTitle": the first three 111 | // characters mark an ISO language code (id[0] == '\0' if not 112 | // available) while the remaining 29 characters contain as much of 113 | // the JF DisplayTitle as possible. 114 | JF_ITEM_TYPE_VIDEO_SUB = 7, 115 | 116 | // Folders 117 | JF_ITEM_TYPE_COLLECTION = 20, 118 | JF_ITEM_TYPE_COLLECTION_MUSIC = 21, 119 | JF_ITEM_TYPE_COLLECTION_SERIES = 22, 120 | JF_ITEM_TYPE_COLLECTION_MOVIES = 23, 121 | JF_ITEM_TYPE_COLLECTION_MUSIC_VIDEOS = 24, 122 | JF_ITEM_TYPE_USER_VIEW = 25, 123 | JF_ITEM_TYPE_FOLDER = 26, 124 | JF_ITEM_TYPE_PLAYLIST = 27, 125 | JF_ITEM_TYPE_ARTIST = 28, 126 | JF_ITEM_TYPE_ALBUM = 29, 127 | JF_ITEM_TYPE_SEASON = 30, 128 | JF_ITEM_TYPE_SERIES = 31, 129 | 130 | // Special folder 131 | JF_ITEM_TYPE_SEARCH_RESULT = 100, 132 | 133 | // Persistent folders 134 | JF_ITEM_TYPE_MENU_ROOT = -1, 135 | JF_ITEM_TYPE_MENU_FAVORITES = -2, 136 | JF_ITEM_TYPE_MENU_CONTINUE = -3, 137 | JF_ITEM_TYPE_MENU_NEXT_UP = -4, 138 | JF_ITEM_TYPE_MENU_LATEST_ADDED = -5, 139 | JF_ITEM_TYPE_MENU_LIBRARIES = -6 140 | } jf_item_type; 141 | 142 | // Category macros. They're all expressions 143 | // UPDATE THESE if you add item_type's or change the item_type representation! 144 | #define JF_ITEM_TYPE_IS_PERSISTENT(t) ((t) < 0) 145 | #define JF_ITEM_TYPE_IS_FOLDER(t) ((t) < 0 || (t) >= 20) 146 | #define JF_ITEM_TYPE_HAS_DYNAMIC_CHILDREN(t) ((t) < -1 || (t) >= 20) 147 | 148 | 149 | const char *jf_item_type_get_name(const jf_item_type type); 150 | 151 | 152 | typedef struct jf_menu_item { 153 | jf_item_type type; 154 | struct jf_menu_item **children; 155 | size_t children_count; 156 | char id[JF_ID_LENGTH +1]; 157 | char *name; 158 | char *path; 159 | long long playback_ticks; 160 | long long runtime_ticks; 161 | } jf_menu_item; 162 | 163 | 164 | // Allocates a jf_menu_item struct in dynamic memory. 165 | // 166 | // Parameters: 167 | // - type: the jf_item_type of the menu item being represented. 168 | // - children: an array of pointers to jf_menu_item's that 169 | // descend from the current one in the UI/library hierarchy. 170 | // IT IS NOT COPIED BUT ASSIGNED (MOVE). 171 | // - children_count: the lenght of the `children` array 172 | // - id: the string marking the id of the item. It will be copied to an 173 | // internal buffer and must have JF_ID_LENGTH size but does not need to be 174 | // \0-terminated. May be NULL for persistent menu items, in which case the 175 | // internal buffer will contain a \0-terminated empty string. 176 | // - name: the string marking the display name of the item. It must be 177 | // \0-terminated. It will be copied by means of strdup. May be NULL, in 178 | // which case the corresponding field of the jf_menu_item will be NULL. 179 | // - runtime_ticks: length of underlying media item measured in JF ticks. 180 | // - playback_ticks: progress marker for partially viewed items measured in JF ticks. 181 | // 182 | // Returns: 183 | // A pointer to the newly allocated struct. 184 | // CAN FATAL. 185 | jf_menu_item *jf_menu_item_new(jf_item_type type, 186 | jf_menu_item **children, 187 | const size_t children_count, 188 | const char *id, 189 | const char *name, 190 | const char *path, 191 | const long long runtime_ticks, 192 | const long long playback_ticks); 193 | 194 | // Deallocates a jf_menu_item and all its descendants recursively, unless they 195 | // are marked as persistent (as per JF_ITEM_TYPE_IS_PERSISTENT). 196 | // 197 | // Parameters: 198 | // - menu_item: a pointer to the struct to deallocate. It may be NULL, in which 199 | // case the function will no-op. 200 | void jf_menu_item_free(jf_menu_item *menu_item); 201 | 202 | 203 | #ifdef JF_DEBUG 204 | void jf_menu_item_print(const jf_menu_item *item); 205 | #endif 206 | ////////////////////////////////////////////////////////// 207 | 208 | 209 | ////////// GROWING BUFFER ////////// 210 | typedef struct _jf_growing_buffer { 211 | char *buf; 212 | size_t size; 213 | size_t used; 214 | } *jf_growing_buffer; 215 | 216 | 217 | jf_growing_buffer jf_growing_buffer_new(const size_t size); 218 | void jf_growing_buffer_append(jf_growing_buffer buffer, 219 | const void *data, 220 | const size_t length); 221 | void jf_growing_buffer_sprintf(jf_growing_buffer buffer, 222 | size_t offset, 223 | const char *format, ...); 224 | void jf_growing_buffer_empty(jf_growing_buffer buffer); 225 | void jf_growing_buffer_free(jf_growing_buffer buffer); 226 | //////////////////////////////////// 227 | 228 | 229 | ////////// THREAD_BUFFER ////////// 230 | typedef enum jf_thread_buffer_state { 231 | JF_THREAD_BUFFER_STATE_CLEAR = 0, 232 | JF_THREAD_BUFFER_STATE_AWAITING_DATA = 1, 233 | JF_THREAD_BUFFER_STATE_PENDING_DATA = 2, 234 | JF_THREAD_BUFFER_STATE_PARSER_ERROR = 3, 235 | JF_THREAD_BUFFER_STATE_PARSER_DEAD = 4 236 | } jf_thread_buffer_state; 237 | 238 | 239 | typedef struct jf_thread_buffer { 240 | char data[JF_THREAD_BUFFER_DATA_SIZE]; 241 | size_t used; 242 | bool promiscuous_context; 243 | jf_thread_buffer_state state; 244 | size_t item_count; 245 | pthread_mutex_t mut; 246 | pthread_cond_t cv_no_data; 247 | pthread_cond_t cv_has_data; 248 | } jf_thread_buffer; 249 | 250 | 251 | void jf_thread_buffer_init(jf_thread_buffer *tb); 252 | /////////////////////////////////// 253 | 254 | 255 | ////////// GLOBAL APPLICATION STATE ////////// 256 | typedef enum jf_jftui_state { 257 | JF_STATE_STARTING = 0, 258 | JF_STATE_STARTING_FULL_CONFIG = 1, 259 | JF_STATE_STARTING_LOGIN = 2, 260 | JF_STATE_MENU_UI = 3, 261 | JF_STATE_PLAYBACK = 4, 262 | JF_STATE_PLAYBACK_INIT = 5, 263 | JF_STATE_PLAYBACK_START_MARK = 6, 264 | JF_STATE_PLAYLIST_SEEKING = 7, 265 | JF_STATE_PLAYBACK_STOPPING = 8, 266 | 267 | JF_STATE_USER_QUIT = -1, 268 | JF_STATE_FAIL = -2 269 | } jf_jftui_state; 270 | 271 | #define JF_STATE_IS_EXITING(_s) ((_s) < 0) 272 | 273 | 274 | typedef enum jf_loop_state { 275 | JF_LOOP_STATE_IN_SYNC = 0, 276 | JF_LOOP_STATE_RESYNCING = 1, 277 | JF_LOOP_STATE_OUT_OF_SYNC = 2 278 | } jf_loop_state; 279 | 280 | 281 | typedef struct jf_global_state { 282 | char *config_dir; 283 | char *session_id; 284 | char *server_name; 285 | uint64_t server_version; 286 | jf_jftui_state state; 287 | jf_menu_item *now_playing; 288 | // 1-indexed 289 | size_t playlist_position; 290 | // counter for playlist loops to do 291 | // -1 for infinite loops 292 | int64_t playlist_loops; 293 | jf_loop_state loop_state; 294 | #if MPV_CLIENT_API_VERSION >= MPV_MAKE_VERSION(2,1) 295 | char *mpv_cache_dir; 296 | #endif 297 | } jf_global_state; 298 | ////////////////////////////////////////////// 299 | 300 | 301 | ////////// SYNCED QUEUE ////////// 302 | typedef struct jf_synced_queue { 303 | const void **slots; 304 | size_t slot_count; 305 | size_t current; 306 | size_t next; 307 | pthread_mutex_t mut; 308 | pthread_cond_t cv_is_empty; 309 | pthread_cond_t cv_is_full; 310 | } jf_synced_queue; 311 | 312 | jf_synced_queue *jf_synced_queue_new(const size_t slot_count); 313 | 314 | // Deallocates the queue but NOT its contents. 315 | // 316 | // Parameters: 317 | // - q: pointer to the jf_synced_queue to deallocate (if NULL, no-op). 318 | // CAN'T FAIL. 319 | void jf_synced_queue_free(jf_synced_queue *q); 320 | 321 | void jf_synced_queue_enqueue(jf_synced_queue *q, const void *payload); 322 | 323 | void *jf_synced_queue_dequeue(jf_synced_queue *q); 324 | ////////////////////////////////// 325 | 326 | 327 | ////////// MISCELLANEOUS GARBAGE ////////// 328 | typedef enum jf_strong_bool { 329 | JF_STRONG_BOOL_NO = 0, 330 | JF_STRONG_BOOL_YES = 1, 331 | JF_STRONG_BOOL_FORCE = 2 332 | } jf_strong_bool; 333 | 334 | 335 | // Parses a string into a jf_strong_bool. 336 | // The mappings are case insensitive, as follows: 337 | // - "no" to JF_STRONG_BOOL_NO; 338 | // - "yes" to JF_STRONG_BOOL_YES; 339 | // - "force" to JF_STRONG_BOOL_FORCE. 340 | // 341 | // Returns: 342 | // - true on successful parsing; 343 | // - false on NULL or unrecognized input string. 344 | bool jf_strong_bool_parse(const char *str, 345 | const size_t len, 346 | jf_strong_bool *out); 347 | 348 | // Returns the obvious string representing a jf_strong_bool. 349 | // The mapping is as per the jf_strong_bool_parse function. 350 | const char *jf_strong_bool_to_str(jf_strong_bool strong_bool); 351 | 352 | 353 | // Concatenates any amount of NULL-terminated strings. The result will be 354 | // dynamically allocated and will need to be free'd. 355 | // 356 | // Parameters: 357 | // - n: the number of following arguments, i.e. strings to concatenate. 358 | // - varargs: a variadic sequence of (const char *) pointing to NULL-terminated 359 | // strings to be concatenated. 360 | // 361 | // Returns: 362 | // char * pointing to the malloc'd result. 363 | // CAN FATAL. 364 | char *jf_concat(const size_t n, ...); 365 | 366 | 367 | // Prints an unsigned, base-10 number to stdout. The function is NEITHER 368 | // reentrant NOR thread-safe. 369 | // IT WILL CAUSE UNDEFINED BEHAVIOUR if the base-10 representation of the 370 | // argument is longer than 20 digits, which means the binary representation of 371 | // the number takes more than 64 bits. 372 | // 373 | // Parameters: 374 | // - n: The number to print. It is always treated as unsigned and base-10. 375 | // Regardless of the system's implementation of size_t, it must fit into 376 | // 64 bits for the internal buffer not to overflow. 377 | void jf_print_zu(size_t n); 378 | 379 | 380 | // Generates a malloc'd string of arbitrary length of random digits. 381 | // 382 | // Parameters: 383 | // - len: length of the random string, excluding the terminating null btye. If 384 | // 0, a default of 10 will be applied. 385 | // 386 | // Returns: 387 | // Pointer to the string. It will need be free'd. 388 | // CAN FATAL. 389 | char *jf_generate_random_id(size_t length); 390 | 391 | 392 | char *jf_make_timestamp(const long long ticks); 393 | size_t jf_clamp_zu(const size_t zu, const size_t min, const size_t max); 394 | void jf_clear_stdin(void); 395 | 396 | 397 | // Tries to replace the entire bottom line of the terminal buffer with empty 398 | // space, by priting a line of whitespace. 399 | // 400 | // Parameters: 401 | // - stream: the actual stream that will be printed to. 402 | // Can be NULL to default to stdout. 403 | // 404 | // CAN'T FAIL. 405 | void jf_term_clear_bottom(FILE *stream); 406 | 407 | 408 | // Computes length of string, including terminating '\0' byte. 409 | // 410 | // Parameters: 411 | // - str: the string whose length to compute. 412 | // 413 | // Returns: 414 | // - 0 if str == NULL; 415 | // - strlen(str) + 1 if str != NULL. 416 | // 417 | // CAN'T FAIL. 418 | size_t jf_strlen(const char *str); 419 | 420 | 421 | // Returns a YYYY-mm-dd representation of today's date, shifted to exactly one 422 | // year ago. 423 | // 424 | // Returns: 425 | // - A pointer to a statically allocated buffer containing the null-terminated 426 | // string with the date. 427 | // 428 | // CAN'T FAIL. 429 | char *jf_make_date_one_year_ago(void); 430 | /////////////////////////////////////////// 431 | #endif 432 | --------------------------------------------------------------------------------