sksonic.c 81 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439
  1. #include <stdlib.h>
  2. #include <string.h>
  3. #include <stdio.h>
  4. #include <time.h>
  5. #include <locale.h>
  6. #include <signal.h>
  7. #include <unistd.h>
  8. #include <pthread.h>
  9. #include <curl/curl.h>
  10. #include "cJSON.c"
  11. #include <ncurses.h>
  12. #include "config.h"
  13. #define HASH_TABLE_SIZE 1024
  14. #define NOTIFICATION_LENGTH 1024
  15. #define MAX_QUERY_LENGTH 256
  16. typedef enum {
  17. PANEL_ARTISTS,
  18. PANEL_ALBUMS,
  19. PANEL_SONGS,
  20. NUM_PANELS
  21. } PanelType;
  22. typedef enum {
  23. WINDOW_INFO,
  24. WINDOW_PLAYLIST,
  25. WINDOW_PLAYBACK,
  26. NUM_WINDOWS
  27. } WindowType;
  28. typedef enum {
  29. MOVE_TOP,
  30. MOVE_BOTTOM,
  31. NUM_MOVEMENTS
  32. } SpecialMovement;
  33. typedef enum {
  34. STOPPED,
  35. PLAYING,
  36. PAUSED,
  37. NUM_STATUS
  38. } StatusType;
  39. typedef enum {
  40. NONE,
  41. SHUFFLE,
  42. REPEAT,
  43. } ShuffleRepeatStatus;
  44. typedef enum {
  45. VIEW_INFO,
  46. VIEW_PLAYLIST,
  47. NUM_VIEWS
  48. } ViewType;
  49. typedef struct {
  50. char *url;
  51. char *command;
  52. int playlist_pid;
  53. } PlaybackThreadArgs;
  54. typedef struct Connection {
  55. char *url;
  56. int port;
  57. char *user;
  58. char *password;
  59. char *version;
  60. char *app;
  61. } Connection;
  62. enum Operation {
  63. PING,
  64. ARTISTS,
  65. ALBUMS,
  66. SONGS,
  67. PLAY
  68. };
  69. typedef struct Song {
  70. char *id;
  71. char *name;
  72. int duration;
  73. } Song;
  74. typedef struct Album {
  75. char *id;
  76. char *name;
  77. int number_songs;
  78. Song *songs;
  79. } Album;
  80. typedef struct Artist {
  81. char *name;
  82. char *id;
  83. int number_albums;
  84. Album *albums;
  85. } Artist;
  86. typedef struct Database {
  87. Artist *artists;
  88. int number_artists;
  89. } Database;
  90. typedef struct Playlist {
  91. Song **songs;
  92. int size;
  93. int capacity;
  94. int current_playing;
  95. time_t start_time;
  96. int play_time;
  97. int status;
  98. int repeat_shuffle;
  99. int selected_song_idx;
  100. pid_t pid;
  101. ShuffleRepeatStatus shuffle_repeat_status;
  102. } Playlist;
  103. typedef struct SongInfo {
  104. const char *artist;
  105. const char *album;
  106. } SongInfo;
  107. typedef struct Playback_Program {
  108. char *executable;
  109. char *flags;
  110. } Playback_Program;
  111. typedef struct AppState {
  112. Playlist *playlist;
  113. Database *db;
  114. Artist *artist;
  115. Album *album;
  116. ViewType current_view;
  117. PanelType current_panel;
  118. const Connection *const connection;
  119. const Playback_Program program;
  120. int selected_artist_idx;
  121. int selected_album_idx;
  122. int selected_song_idx;
  123. WINDOW **windows[NUM_WINDOWS];
  124. } AppState;
  125. struct url_data {
  126. int size;
  127. char *data;
  128. };
  129. /* Functions */
  130. int write_url_data(void *const, const int, const int, struct url_data *const);
  131. void pause_resume(const AppState *const);
  132. void stop_playback(const AppState *const);
  133. void generate_subsonic_url(const Connection *, enum Operation, const char *,
  134. char **);
  135. void add_song(const Song *, Playlist *);
  136. void delete_song(const AppState *const);
  137. void get_artists(const Connection *, Database *);
  138. void get_albums(const Connection *const, const Database *const,
  139. const char *const);
  140. void get_songs(const Connection *const, const Database *const, const char *, const char *);
  141. void notify(const AppState *);
  142. void request_albums(const Connection *const, Artist *const);
  143. void request_songs(const Connection *, Album *);
  144. void print_window_data(const AppState *const, PanelType, WINDOW *const *const);
  145. void change_playback_status(const pid_t, const int);
  146. int add_to_playlist(AppState *);
  147. char *fetch_url_data(const char *const);
  148. Database init_db(void);
  149. AppState init_appstate(void);
  150. Playlist init_playlist(void);
  151. WINDOW **create_windows(const int, const int, const WindowType);
  152. void play_song(const AppState *const, const int);
  153. void search_idx(AppState *);
  154. static const Connection connection = {
  155. .url = URL,
  156. .port = PORT,
  157. .user = USER,
  158. .password = PWD,
  159. .version = VERSION,
  160. .app = APP
  161. };
  162. /**
  163. * Function to initialize the app state.
  164. *
  165. * @return The initialized app state.
  166. */
  167. AppState init_appstate(void)
  168. {
  169. static const Playback_Program program = { executable, flags };
  170. AppState state = {
  171. .selected_artist_idx = 0,
  172. .selected_album_idx = 0,
  173. .selected_song_idx = 0,
  174. .current_view = VIEW_INFO,
  175. .current_panel = PANEL_ARTISTS,
  176. .playlist = NULL,
  177. .connection = &connection,
  178. .program = program,
  179. .db = NULL,
  180. .artist = NULL,
  181. .album = NULL,
  182. .windows = { NULL },
  183. };
  184. return state;
  185. }
  186. /**
  187. * Function to delete an array of ncurses windows.
  188. *
  189. * @param windows Pointer to an array of ncurses windows.
  190. * @param num_windows Number of windows in the array.
  191. */
  192. static inline void delete_windows(WINDOW *windows[], const int num_windows)
  193. {
  194. for (int i = 0; i < num_windows; i++) {
  195. delwin(windows[i]);
  196. windows[i] = NULL;
  197. }
  198. }
  199. /**
  200. * Function that runs in a separate thread to play the song using ffplay.
  201. *
  202. * @param arg Pointer to a PlaybackThreadArgs struct containing data needed to run the playback.
  203. */
  204. void *playback_thread(void *arg)
  205. {
  206. // Get arguments and free memory associated with them.
  207. PlaybackThreadArgs *const args = (PlaybackThreadArgs *) arg;
  208. char *const url = args->url;
  209. char *const command = args->command;
  210. free(args);
  211. // Open a pipe for reading from the ffplay process.
  212. FILE *const fp = popen(command, "r");
  213. if (fp == NULL) {
  214. fprintf(stderr, "Failed to open stream.\n");
  215. free(url);
  216. return NULL;
  217. }
  218. // Clean up allocated memory and close the file pointer.
  219. pclose(fp);
  220. free(url);
  221. return NULL;
  222. }
  223. /**
  224. * Function to set up ncurses for the program's user interface.
  225. * Initializes the ncurses library and sets various options, as well as defining color pairs.
  226. */
  227. void setup_ncurses(void)
  228. {
  229. initscr();
  230. clear();
  231. noecho();
  232. curs_set(0);
  233. cbreak();
  234. halfdelay(5);
  235. keypad(stdscr, TRUE);
  236. start_color();
  237. // Define color pairs with colours defined in config.h
  238. init_pair(1, colors[INACTIVE][0], colors[INACTIVE][1]);
  239. init_pair(2, colors[ACTIVE][0], colors[ACTIVE][1]);
  240. // Refresh the screen to update changes.
  241. refresh();
  242. }
  243. /**
  244. * Function to create an array of ncurses windows.
  245. *
  246. * @param n Number of windows to create.
  247. * @param bottom_space Height of the space at the bottom of the screen.
  248. * @param window_type The type of windows to create.
  249. * @return An array of ncurses windows.
  250. */
  251. WINDOW **create_windows(const int number_windows, const int bottom_space,
  252. const WindowType window_type)
  253. {
  254. // Get screen dimensions
  255. const int screen_h = LINES;
  256. const int screen_w = COLS;
  257. // Compute window dimensions
  258. const int win_w = screen_w / number_windows;
  259. const int win_h =
  260. window_type == WINDOW_PLAYBACK ? bottom_space : screen_h - bottom_space;
  261. // Create windows
  262. WINDOW **const windows = calloc(number_windows, sizeof(WINDOW *));
  263. const int remaining_w = screen_w % number_windows;
  264. int x = 0;
  265. // Create windows with equal width and add extra column to some of them based on the remaining width
  266. for (int i = 0; i < number_windows; ++i) {
  267. // Determine the width of the current window
  268. int w = win_w + (i < remaining_w);
  269. // Create a new window with the calculated width
  270. windows[i] =
  271. newwin(win_h, w,
  272. window_type ==
  273. WINDOW_PLAYBACK ? screen_h - bottom_space : 0, x);
  274. // Increment x-coordinate by the width of the current window
  275. x += w;
  276. if (window_type != WINDOW_PLAYBACK) {
  277. box(windows[i], 0, 0);
  278. }
  279. }
  280. return windows;
  281. }
  282. /**
  283. * Returns the number of characters (or glyphs) in a string
  284. *
  285. * @param str A null-terminated string
  286. * @return The number of characters (or glyphs) in the string
  287. */
  288. static int get_char_count(const char *str)
  289. {
  290. int count = 0;
  291. while (*str != '\0') {
  292. // Check if the current byte is the first byte of a multi-byte sequence
  293. if ((*str & 0xC0) != 0x80) {
  294. count++;
  295. }
  296. // Move to the next byte
  297. str++;
  298. }
  299. return count;
  300. }
  301. /**
  302. * Returns the number of bytes in a string that correspond to characters.
  303. *
  304. * @param text A null-terminated string
  305. * @param max_width The maximum width of the formatted text in characters
  306. * @return The number of bytes in the formatted text that correspond to characters
  307. */
  308. static int get_character_byte_count(const char *text, const int max_width)
  309. {
  310. const char *p = text;
  311. size_t formatted_len = 0;
  312. size_t byte_count = 0;
  313. // Advance glyph by glyph, computing the correct size
  314. while ((*p != '\0') && (formatted_len < max_width)) {
  315. formatted_len++;
  316. if ((*p & 0x80) == 0) {
  317. // single-byte character (ASCII)
  318. byte_count += 1;
  319. p += 1;
  320. } else if ((*p & 0xE0) == 0xC0) {
  321. // two-byte character
  322. byte_count += 2;
  323. p += 2;
  324. } else if ((*p & 0xF0) == 0xE0) {
  325. // three-byte character
  326. byte_count += 3;
  327. p += 3;
  328. } else if ((*p & 0xF8) == 0xF0) {
  329. // four-byte character
  330. byte_count += 4;
  331. p += 4;
  332. } else {
  333. // Invalid character
  334. fprintf(stderr, "Invalid character.\n");
  335. exit(1);
  336. }
  337. }
  338. return byte_count;
  339. }
  340. /**
  341. * Formats a string to fit within a certain width, adding a prefix and ellipsis if necessary.
  342. * Returns a newly allocated formatted string that must be freed by the caller or NULL on failure.
  343. *
  344. * @param text The input text string to format
  345. * @param max_width The maximum width of the output string
  346. * @param prefix The prefix to prepend to the output string
  347. * @return A newly allocated formatted string or NULL on failure
  348. */
  349. char *format_text(const char *const text, const int max_width,
  350. const char *const prefix)
  351. {
  352. if (text == NULL || prefix == NULL || max_width < 0) {
  353. return NULL;
  354. }
  355. // Lenghts measured in characters
  356. const int prefix_len = get_char_count(prefix);
  357. const int text_len = get_char_count(text);
  358. const int total_len = prefix_len + text_len;
  359. // Lengths measured in bytes
  360. const size_t prefix_size = strlen(prefix);
  361. const size_t text_size = strlen(text);
  362. if (total_len <= max_width) {
  363. // Determine number of padding spaces
  364. const int padding_len = max_width - total_len;
  365. // Allocate enough space for formatted string
  366. const size_t bytes_formatted =
  367. prefix_size + text_size + padding_len + 1;
  368. char *const formatted = calloc(bytes_formatted, sizeof(char));
  369. if (formatted == NULL) {
  370. return NULL;
  371. }
  372. // Build formatted string with padding spaces
  373. memcpy(formatted, prefix, prefix_size);
  374. memcpy(formatted + prefix_size, text, text_size);
  375. memset(formatted + prefix_size + text_size, ' ', padding_len);
  376. formatted[bytes_formatted - 1] = '\0';
  377. return formatted;
  378. } else {
  379. // Lengths measured in bytes
  380. const int bytes_ellipsis = sizeof("\xE2\x80\xA6");
  381. const size_t bytes_prefix = strlen(prefix);
  382. const size_t bytes_text = get_character_byte_count(text, max_width - 1);
  383. const size_t bytes_formatted =
  384. bytes_prefix + bytes_text + bytes_ellipsis + 1;
  385. // Allocate enough space to hold the entire string with prefix and ellipsis
  386. char *const formatted = calloc(bytes_formatted, sizeof(char));
  387. if (formatted == NULL) {
  388. return NULL;
  389. }
  390. // Build the formatted string with ellipses
  391. memcpy(formatted, prefix, bytes_prefix);
  392. memcpy(formatted + bytes_prefix, text, bytes_text);
  393. memcpy(formatted + bytes_prefix + bytes_text, "\xE2\x80\xA6",
  394. bytes_ellipsis);
  395. formatted[bytes_formatted - 1] = '\0';
  396. return formatted;
  397. }
  398. }
  399. /**
  400. * Prints the playlist associated with the given app state to the specified window.
  401. * The playlist is printed starting at the currently selected song, and if necessary,
  402. * is scrolled to ensure that the selected song is visible.
  403. *
  404. * @param app_state a pointer to the AppState struct containing the playlist to print.
  405. * @param window a pointer to the ncurses window in which to print the playlist.
  406. */
  407. void print_playlist_data(const AppState *const app_state,
  408. WINDOW *const *const windows)
  409. {
  410. WINDOW *const window = windows[0];
  411. const Playlist *const playlist = app_state->playlist;
  412. const int max_row = getmaxy(window) - 2;
  413. const int max_col = getmaxx(window) - 2;
  414. const int current_index = playlist->selected_song_idx;
  415. const int current_playing = playlist->current_playing;
  416. const int number_items = playlist->size;
  417. // Define the row decoration, based on whether the current panel is active or not
  418. const int first_item = (number_items <= max_row) ? 0 :
  419. (current_index <= max_row / 3) ? 0 :
  420. (current_index <
  421. number_items - max_row * 2 / 3) ? current_index -
  422. max_row / 3 : number_items - max_row;
  423. const int last_item = MIN(first_item + max_row, number_items);
  424. wclear(window);
  425. box(window, 0, 0);
  426. // Loop through each item to display, formatting the text as necessary and applying row decoration
  427. for (int i = first_item; i < last_item; i++) {
  428. char *text = format_text(playlist->songs[i]->name, max_col,
  429. (i ==
  430. current_playing) ?
  431. appearance[ind_playing] : "");
  432. // Decorate the row, based on whether the current row is selected or not
  433. wattron(window,
  434. (i ==
  435. current_index) ? COLOR_PAIR(ACTIVE +
  436. 1) : COLOR_PAIR(INACTIVE +
  437. 1) | A_REVERSE);
  438. mvwprintw(window, i + 1 - first_item, 1, "%s", text);
  439. wattroff(window,
  440. (i ==
  441. current_index) ? COLOR_PAIR(ACTIVE +
  442. 1) : COLOR_PAIR(INACTIVE +
  443. 1) | A_REVERSE);
  444. free(text);
  445. }
  446. wrefresh(window);
  447. }
  448. /**
  449. * Prints the data associated with a given panel type to the specified window. The data displayed
  450. * depends on the panel type provided, and is based on the given app state. The currently selected item
  451. * is highlighted, and if necessary, the data is scrolled to ensure that the selected item is visible.
  452. *
  453. * @param app_state A constant pointer to a struct representing the current state of the application.
  454. * @param panel An enum value indicating which panel to print the data onto.
  455. * @param windows A constant pointer to an array of WINDOW pointers, representing the different panels of the application.
  456. *
  457. * @return void
  458. */
  459. void print_window_data(const AppState *const app_state, const PanelType panel,
  460. WINDOW *const *const windows)
  461. {
  462. WINDOW *const window = windows[panel];
  463. const int max_row = getmaxy(window) - 2;
  464. const int max_col = getmaxx(window) - 2;
  465. int current_index = 0;
  466. int number_items = 0;
  467. wclear(window);
  468. box(window, 0, 0);
  469. // Determine which data to display in the given panel, based on the app state and panel type
  470. void *ptr = NULL;
  471. switch (panel) {
  472. case PANEL_ARTISTS:
  473. current_index = app_state->selected_artist_idx;
  474. number_items = app_state->db->number_artists;
  475. // Assign the address of the beginning of the array of artists in the database struct
  476. // pointed to by app_state->db to the void pointer ptr
  477. ptr = (void *) app_state->db->artists;
  478. break;
  479. case PANEL_ALBUMS:
  480. current_index = app_state->selected_album_idx;
  481. number_items = app_state->artist->number_albums;
  482. // Assign the address of the beginning of the array of albums in the artist struct
  483. // pointed to by app_state->artist to the void pointer ptr
  484. ptr = (void *) app_state->artist->albums;
  485. break;
  486. case PANEL_SONGS:
  487. current_index = app_state->selected_song_idx;
  488. number_items = app_state->album->number_songs;
  489. // Assign the address of the beginning of the array of songs in the artist struct
  490. // pointed to by app_state->album to the void pointer ptr
  491. ptr = (void *) app_state->album->songs;
  492. break;
  493. default:
  494. current_index = 0;
  495. number_items = 0;
  496. break;
  497. }
  498. // Determine which items to display in the window, based on the current index and the number of items
  499. const int first_item = (number_items <= max_row) ? 0 :
  500. (current_index <= max_row / 3) ? 0 :
  501. (current_index <
  502. number_items - max_row * 2 / 3) ? current_index -
  503. max_row / 3 : number_items - max_row;
  504. const int last_item = MIN(first_item + max_row, number_items);
  505. // Define the row decoration, based on whether the current panel is active or not
  506. const int row_decoration[][2] = {
  507. // Active panel Inactive panel
  508. { COLOR_PAIR(ACTIVE + 1), COLOR_PAIR(INACTIVE + 1) }, // Selected row
  509. { COLOR_PAIR(INACTIVE + 1) | A_REVERSE, COLOR_PAIR(INACTIVE + 1) | A_REVERSE }, // Not selected row
  510. };
  511. const int is_active_panel = app_state->current_panel == panel ? 0 : 1;
  512. // Loop through each item to display, formatting the text as necessary and applying row decoration
  513. for (int i = first_item; i < last_item; i++) {
  514. char *text = NULL;
  515. if (ptr) {
  516. if (panel == PANEL_ARTISTS) {
  517. text = ((Artist *) ptr)[i].name;
  518. } else if (panel == PANEL_ALBUMS) {
  519. text = ((Album *) ptr)[i].name;
  520. } else if (panel == PANEL_SONGS) {
  521. text = ((Song *) ptr)[i].name;
  522. }
  523. }
  524. text = format_text(text, max_col, "");
  525. wstandend(window);
  526. const int is_selected_item = (i == current_index) ? 0 : 1;
  527. wattron(window, row_decoration[is_selected_item][is_active_panel]);
  528. mvwprintw(window, i + 1 - first_item, 1, "%s", text);
  529. wattron(window, COLOR_PAIR(INACTIVE + 1) | A_REVERSE);
  530. free(text);
  531. }
  532. wrefresh(window);
  533. }
  534. /**
  535. * Returns the action associated with the given key press, looked up from a local list
  536. *
  537. * @param keypress The key press to look up
  538. * @param keys_local An array of key-value pairs to search through
  539. * @param len The length of the keys_local array
  540. *
  541. * @return The corresponding action or -1 if not found
  542. */
  543. int get_action(const int keypress)
  544. {
  545. static int initialized = 0;
  546. static int hash_table[512] = { 0 }; // We use the maximum number as per ncurses definitions
  547. if (initialized == 0) {
  548. for (int i = 0; i < len_keys; ++i) {
  549. hash_table[keys[i][0]] = keys[i][1];
  550. }
  551. initialized = 1;
  552. }
  553. if (keypress == -1) {
  554. return -1;
  555. }
  556. const int action = hash_table[keypress];
  557. if (action >= 0) {
  558. return action;
  559. }
  560. return -1;
  561. }
  562. /**
  563. * Updates the selected indexes of the panels based on the given movement and current state of the app.
  564. * If the current view is VIEW_PLAYLIST, only updates the selected song index.
  565. * If the movement is MOVE_TOP or MOVE_BOTTOM, sets the panel destination to the corresponding offset based on the current panel.
  566. * If the movement is up or down and the current view is VIEW_INFO, updates the selected index of the current panel if possible and also updates the selected indexes of other panels depending on the current panel.
  567. * If the movement is left or right and the current view is VIEW_INFO, updates the current panel index.
  568. * @param action The action to perform
  569. * @param app_state Pointer to the AppState struct
  570. * @param special_movement The type of special movement (MOVE_TOP or MOVE_BOTTOM) to perform, -1 if not applicable
  571. */
  572. void movement(const int action, AppState *const app_state,
  573. const SpecialMovement special_movement)
  574. {
  575. if (action == -1) {
  576. return;
  577. }
  578. if (app_state->current_view == VIEW_PLAYLIST) {
  579. // Declare an array of pointers to integer variables that hold the selected index of each panel.
  580. int *const panel_destinations[1] = {
  581. &app_state->playlist->selected_song_idx,
  582. };
  583. const int current_panel = 0;
  584. // Define panel offsets for each panel type, which are used to determine
  585. // the first and last index of each panel when navigating with MOVE_TOP
  586. // and MOVE_BOTTOM movements.
  587. const int panel_offset[][2] = {
  588. //MOVE_TOP MOVE_BOTTOM
  589. { 0, app_state->playlist->size - 1 }, // Playlist
  590. };
  591. // If the movement is valid, set the panel destination to the corresponding
  592. // offset based on the current panel and the movement type.
  593. if (special_movement < NUM_MOVEMENTS) {
  594. *panel_destinations[current_panel] =
  595. panel_offset[current_panel][special_movement];
  596. }
  597. // Check if it is possible to go up
  598. else if (action == up && app_state->playlist->selected_song_idx > 0) {
  599. --app_state->playlist->selected_song_idx;
  600. }
  601. // Check if it is possible to go down
  602. else if (action == down
  603. && app_state->playlist->selected_song_idx <
  604. panel_offset[current_panel][1]) {
  605. ++app_state->playlist->selected_song_idx;
  606. }
  607. return;
  608. }
  609. // Declare an array of pointers to integer variables that hold the selected index of each panel.
  610. int *const panel_destinations[3] = {
  611. &app_state->selected_artist_idx,
  612. &app_state->selected_album_idx,
  613. &app_state->selected_song_idx,
  614. };
  615. const PanelType current_panel = app_state->current_panel;
  616. // Define panel offsets for each panel type, which are used to determine
  617. // the first and last index of each panel when navigating with MOVE_TOP
  618. // and MOVE_BOTTOM movements.
  619. const int panel_offset[][2] = {
  620. // MOVE_TOP MOVE_BOTTOM
  621. { 0, app_state->db->number_artists - 1 }, // ARTISTS_PANEL
  622. { 0, app_state->artist->number_albums - 1 }, // ALBUMS_PANEL
  623. { 0, app_state->album->number_songs - 1 }, // SONGS_PANEL
  624. };
  625. // If the movement is valid, set the panel destination to the corresponding
  626. // offset based on the current panel and the movement type.
  627. if (special_movement < NUM_MOVEMENTS) {
  628. *panel_destinations[current_panel] =
  629. panel_offset[current_panel][special_movement];
  630. } else if (app_state->current_view == VIEW_INFO) {
  631. switch (action) {
  632. case up:
  633. case down:
  634. {
  635. const int offset = action == down ? 1 : -1;
  636. const int new_idx =
  637. *panel_destinations[current_panel] + offset;
  638. if ((new_idx >= 0)
  639. && (new_idx <= panel_offset[current_panel][1])) {
  640. *panel_destinations[current_panel] = new_idx;
  641. }
  642. if (current_panel == PANEL_ARTISTS) {
  643. app_state->selected_album_idx = 0;
  644. }
  645. if (current_panel == PANEL_ALBUMS) {
  646. app_state->selected_song_idx = 0;
  647. }
  648. break;
  649. }
  650. case left:
  651. if (app_state->current_panel > 0) {
  652. --app_state->current_panel;
  653. }
  654. break;
  655. case right:
  656. if (app_state->current_panel < NUM_PANELS - 1) {
  657. ++app_state->current_panel;
  658. }
  659. break;
  660. default:
  661. break;
  662. }
  663. }
  664. Artist *const artist =
  665. &(app_state->db->artists[app_state->selected_artist_idx]);
  666. app_state->artist = artist;
  667. get_albums(app_state->connection, app_state->db, artist->id);
  668. Album *const album = &(artist->albums[app_state->selected_album_idx]);
  669. app_state->album = album;
  670. get_songs(app_state->connection, app_state->db, artist->id, album->id);
  671. return;
  672. }
  673. /**
  674. * Updates the contents of multiple windows based on the current view of the application state.
  675. *
  676. * @param app_state A pointer to the current state of the application
  677. * @param windows An array of pointers to the windows to be updated
  678. * @param number_windows The number of windows in the array
  679. */
  680. void refresh_windows(const AppState *const app_state,
  681. WINDOW *const *const windows, const int number_windows)
  682. {
  683. switch (app_state->current_view) {
  684. case VIEW_INFO:
  685. for (int i = 0; i < number_windows; ++i) {
  686. print_window_data(app_state, i, windows);
  687. }
  688. break;
  689. case VIEW_PLAYLIST:
  690. for (int i = 0; i < number_windows; ++i) {
  691. print_playlist_data(app_state, windows);
  692. }
  693. break;
  694. default:
  695. break;
  696. }
  697. }
  698. /**
  699. * Initializes a new empty database.
  700. *
  701. * @return A new empty database struct.
  702. */
  703. Database init_db(void)
  704. {
  705. // Initialize an empty database struct with NULL artists pointer and 0 number of artists.
  706. return (Database) {
  707. .artists = NULL,
  708. .number_artists = 0,
  709. };
  710. }
  711. /**
  712. * Initializes a new playlist by allocating memory for 10 songs and setting
  713. * all other fields to their default values.
  714. *
  715. * @return A new playlist struct.
  716. */
  717. Playlist init_playlist(void)
  718. {
  719. // Allocate memory for 10 Song pointers using malloc.
  720. Song **const songs = (Song **) malloc(10 * sizeof(Song *));
  721. // If malloc fails to allocate memory, clean up and exit program.
  722. if (songs == NULL) {
  723. fprintf(stderr, "Error: Failed to allocate memory for playlist.\n");
  724. exit(EXIT_FAILURE);
  725. }
  726. // Initialize and return a new Playlist struct with dynamically allocated memory for songs.
  727. return (Playlist) {
  728. .songs = &songs[0],
  729. .size = 0,
  730. .capacity = 10,
  731. .current_playing = 0,
  732. .start_time = (time_t) NULL,
  733. .play_time = 0,
  734. .status = STOPPED,
  735. .repeat_shuffle = 0,
  736. .selected_song_idx = -1,
  737. .pid = -1,
  738. .shuffle_repeat_status = NONE,
  739. };
  740. }
  741. /**
  742. * Adds a song to the end of the playlist. If the playlist is full, its capacity is doubled.
  743. *
  744. * @param song A pointer to the Song struct to be added.
  745. * @param playlist A pointer to the Playlist struct where the song will be added.
  746. */
  747. void add_song(const Song *const song, Playlist *const playlist)
  748. {
  749. // If the playlist is full, double its capacity by reallocating its memory block.
  750. if (playlist->size == playlist->capacity) {
  751. // Double the playlist's capacity using bit shifting for faster multiplication.
  752. playlist->capacity <<= 1;
  753. // Reallocate the memory block with the new capacity.
  754. void *const p = realloc(playlist->songs,
  755. playlist->capacity * sizeof(playlist->songs));
  756. // If realloc fails, print an error message and exit the program.
  757. if (p == NULL) {
  758. fprintf(stderr, "Error: Failed to allocate memory for playlist.\n");
  759. exit(EXIT_FAILURE);
  760. }
  761. // Update the playlist's memory block and song array pointers
  762. playlist->songs = p;
  763. }
  764. // Add the song to the end of the playlist and update its size
  765. playlist->songs[playlist->size++] = (Song *) song;
  766. // If the selected song index was not previously set, set it to the first song in the playlist.
  767. if (playlist->selected_song_idx == -1) {
  768. playlist->selected_song_idx = 0;
  769. }
  770. }
  771. /**
  772. * Update the selected index based on the query and action.
  773. *
  774. * @param possible_matches The possible matches to search through.
  775. * @param n_matches The number of possible matches.
  776. * @param query The query to search for.
  777. * @param action The action to perform.
  778. * @param current_found The current found index.
  779. * @param idx_to_update The index to update.
  780. */
  781. void update_selected_index(const char *const possible_matches[], const int n_matches, const char *const query, const int action, int *current_found, int *idx_to_update)
  782. {
  783. if (query == NULL) {
  784. return;
  785. }
  786. int start, end, step;
  787. switch (action) {
  788. case search_previous:
  789. start = *current_found - 1;
  790. end = 0;
  791. step = -1;
  792. break;
  793. case search_next:
  794. start = *current_found + 1;
  795. end = n_matches;
  796. step = 1;
  797. break;
  798. default:
  799. start = 0;
  800. end = n_matches;
  801. step = 1;
  802. break;
  803. }
  804. for (int i = start; i != end; i += step) {
  805. if (strstr(possible_matches[i], query) != NULL) {
  806. *idx_to_update = i;
  807. *current_found = i;
  808. return; // Found a match, exit the loop
  809. }
  810. }
  811. }
  812. /**
  813. * Search for a query in the current view and update the selected index.
  814. *
  815. * @param app_state The application state.
  816. */
  817. void search_idx(AppState *app_state)
  818. {
  819. WINDOW *playback_window = *app_state->windows[WINDOW_PLAYBACK];
  820. ViewType current_view = app_state->current_view;
  821. PanelType current_panel = app_state->current_panel;
  822. const int n_matches = current_view == WINDOW_PLAYLIST ? app_state->playlist->size :
  823. current_panel == PANEL_ARTISTS ? app_state->db->number_artists :
  824. current_panel == PANEL_ALBUMS ? app_state->artist->number_albums :
  825. current_panel == PANEL_SONGS ? app_state->album->number_songs : 0;
  826. if (n_matches == 0) {
  827. return;
  828. }
  829. const char *possible_matches[n_matches];
  830. int *idx_to_update = NULL;
  831. if (current_view == WINDOW_PLAYLIST) {
  832. for (int i = 0; i < n_matches; i++) {
  833. possible_matches[i] = app_state->playlist->songs[i]->name;
  834. }
  835. idx_to_update = &app_state->playlist->selected_song_idx;
  836. } else {
  837. if (current_panel == PANEL_ARTISTS) {
  838. for (int i = 0; i < n_matches; i++) {
  839. possible_matches[i] = app_state->db->artists[i].name;
  840. }
  841. idx_to_update = &app_state->selected_artist_idx;
  842. }
  843. if (current_panel == PANEL_ALBUMS) {
  844. for (int i = 0; i < n_matches; i++) {
  845. possible_matches[i] = app_state->artist->albums[i].name;
  846. }
  847. idx_to_update = &app_state->selected_album_idx;
  848. }
  849. if (current_panel == PANEL_SONGS) {
  850. for (int i = 0; i < n_matches; i++) {
  851. possible_matches[i] = app_state->album->songs[i].name;
  852. }
  853. idx_to_update = &app_state->selected_song_idx;
  854. }
  855. }
  856. wclear(playback_window);
  857. wprintw(playback_window, "Search: ");
  858. wrefresh(playback_window);
  859. char query[MAX_QUERY_LENGTH] = { 0 };
  860. int c;
  861. int i = 0;
  862. int current_found = 0;
  863. while ((c = getch()) != '\n') {
  864. switch (c) {
  865. case -1:
  866. break;
  867. case 27:
  868. return;
  869. break;
  870. case '\n':
  871. query[i] = '\0';
  872. break;
  873. case 127:
  874. case KEY_BACKSPACE:
  875. case KEY_DC:
  876. if (i > 0) {
  877. query[--i] = '\0';
  878. mvwaddch(playback_window, 0, strlen("Search: ") + i, ' ');
  879. }
  880. break;
  881. default:
  882. if (i < MAX_QUERY_LENGTH - 1) {
  883. mvwaddch(playback_window, 0, strlen("Search: ") + i, c);
  884. query[i++] = c;
  885. }
  886. break;
  887. }
  888. // Print any new characters entered
  889. wrefresh(playback_window);
  890. // Update the artist and album based on the query
  891. update_selected_index(possible_matches, n_matches, query, 0, &current_found, idx_to_update);
  892. Artist *const artist =
  893. &(app_state->db->artists[app_state->selected_artist_idx]);
  894. app_state->artist = artist;
  895. get_albums(app_state->connection, app_state->db, artist->id);
  896. Album *const album = &(artist->albums[app_state->selected_album_idx]);
  897. app_state->album = album;
  898. get_songs(app_state->connection, app_state->db, artist->id, album->id);
  899. // Refresh
  900. if (current_view == VIEW_PLAYLIST) {
  901. print_playlist_data(app_state,app_state->windows[WINDOW_PLAYLIST]);
  902. } else {
  903. // Refresh all panels (we could also refresh the child panel)
  904. for (int i = 0; i < NUM_PANELS; i++) {
  905. print_window_data(app_state, i, app_state->windows[WINDOW_INFO]);
  906. }
  907. }
  908. }
  909. int action = -1;
  910. while (1) {
  911. c = getch();
  912. action = get_action(c);
  913. switch (action) {
  914. case add_and_play:
  915. if (app_state->current_view == VIEW_INFO) {
  916. const int first_song_to_play = add_to_playlist(app_state);
  917. play_song(app_state, first_song_to_play);
  918. }
  919. if (app_state->current_view == VIEW_PLAYLIST) {
  920. play_song(app_state, app_state->playlist->selected_song_idx);
  921. refresh_windows(app_state, app_state->windows[WINDOW_PLAYLIST], 1);
  922. }
  923. return;
  924. break;
  925. case add:
  926. add_to_playlist(app_state);
  927. return;
  928. break;
  929. case search_next:
  930. case search_previous:
  931. update_selected_index(possible_matches, n_matches, query, action, &current_found, idx_to_update);
  932. // Update the artist and album as we request next or previous
  933. Artist *const artist =
  934. &(app_state->db->artists[app_state->selected_artist_idx]);
  935. app_state->artist = artist;
  936. get_albums(app_state->connection, app_state->db, artist->id);
  937. Album *const album = &(artist->albums[app_state->selected_album_idx]);
  938. app_state->album = album;
  939. get_songs(app_state->connection, app_state->db, artist->id, album->id);
  940. // Refresh
  941. if (current_view == VIEW_PLAYLIST) {
  942. print_playlist_data(app_state,app_state->windows[WINDOW_PLAYLIST]);
  943. } else {
  944. // Refresh all panels (we could also refresh the child panel)
  945. for (int i = 0; i < NUM_PANELS; i++) {
  946. print_window_data(app_state, i, app_state->windows[WINDOW_INFO]);
  947. }
  948. }
  949. break;
  950. default:
  951. break;
  952. }
  953. if (action != search_next && action != search_previous && c != -1) {
  954. break;
  955. }
  956. }
  957. }
  958. /**
  959. * Deletes the currently selected song from the playlist and adjusts the playlist size and capacity if necessary.
  960. *
  961. * @param app_state A pointer to the AppState struct containing the current program state.
  962. */
  963. void delete_song(const AppState *const app_state)
  964. {
  965. Playlist *const playlist = app_state->playlist;
  966. const int index = playlist->selected_song_idx;
  967. const int playlist_size = playlist->size;
  968. // If the selected song index is out of range, return without deleting any song.
  969. if (index >= playlist_size || index < 0) {
  970. return;
  971. }
  972. // Stop playback when deleting the song that is currently being played
  973. if (index == playlist->current_playing) {
  974. stop_playback(app_state);
  975. }
  976. // Use memmove to shift all elements after the deleted song one position to the left.
  977. memmove(&playlist->songs[index], &playlist->songs[index + 1],
  978. (playlist_size - index - 1) * sizeof(Song));
  979. // Decrement the playlist size after deleting the song.
  980. playlist->size--;
  981. // If the playlist size is less than a quarter of its capacity and the capacity is greater than 10,
  982. // reduce the capacity to half its current value using bit shifting instead of division.
  983. if (playlist->capacity > 10 && playlist_size < playlist->capacity / 4) {
  984. playlist->capacity >>= 1; // Bit shift right by 1 to divide by 2.
  985. playlist->songs =
  986. realloc(playlist->songs,
  987. playlist->capacity * sizeof(Song));
  988. // If realloc fails to allocate memory, print an error message and exit the program.
  989. if (playlist->songs == NULL) {
  990. fprintf(stderr,
  991. "Error: Failed to reallocate memory for playlist.\n");
  992. exit(EXIT_FAILURE);
  993. }
  994. }
  995. // If there are no more songs in the playlist, set the selected song index to -1.
  996. if (playlist_size == 0) {
  997. playlist->selected_song_idx = -1;
  998. }
  999. // If the deleted song was the last one in the playlist, select the new last song.
  1000. else if (index + 1 == playlist_size) {
  1001. --(playlist->selected_song_idx);
  1002. }
  1003. }
  1004. /**
  1005. * Gets the process ID (PID) of the running instance of the specified program.
  1006. *
  1007. * This function executes the "pidof" command to obtain the PID of the specified executable. It assumes that the
  1008. * "pidof" command is available on the system and does not perform any error checking or validation.
  1009. *
  1010. * @param program A Playback_Program struct containing the name of the program executable to look up.
  1011. * @return The PID of the running instance of the program, or -1 if no such instance is found.
  1012. */
  1013. pid_t get_pid(const Playback_Program program)
  1014. {
  1015. char command[100];
  1016. // Add cut because pthreads spawns a `sh` command which, in turn, starts program.executable
  1017. snprintf(command, sizeof(command), "pidof %s | cut -d \" \" -f 2",
  1018. program.executable);
  1019. FILE *const fp = popen(command, "r");
  1020. if (fp == NULL) {
  1021. perror("Error executing command");
  1022. return (pid_t) - 1;
  1023. }
  1024. // Read the output of the pidof command into a fixed-size buffer.
  1025. char pid_str[16];
  1026. if (fgets(pid_str, sizeof(pid_str), fp) == NULL) {
  1027. pclose(fp);
  1028. return (pid_t) - 1;
  1029. }
  1030. // Convert the PID string to an integer
  1031. pclose(fp);
  1032. return (pid_t) atoi(pid_str);
  1033. }
  1034. /**
  1035. * Changes the playback status of a running process by sending it a signal.
  1036. *
  1037. * This function sends the specified signal to the process with the given PID, using the kill() system call.
  1038. *
  1039. * @param pid The process ID of the running process.
  1040. * @param signal The signal to send to the process.
  1041. */
  1042. void change_playback_status(const pid_t pid, const int signal)
  1043. {
  1044. // If the PID is invalid or not set, there's nothing to do.
  1045. if (pid <= 0) {
  1046. return;
  1047. }
  1048. if (kill(pid, signal) == -1) {
  1049. // If the kill() call fails, log an error message.
  1050. perror("Error sending signal to process");
  1051. }
  1052. }
  1053. /**
  1054. * Dumps the current state of the application to a file.
  1055. *
  1056. * This function writes the current state of the application to a file. It includes
  1057. * information about the currently playing song, its artist and album, and the time
  1058. * at which the dump was created.
  1059. *
  1060. * @param app_state Pointer to the AppState object containing the current state
  1061. * of the application.
  1062. */
  1063. void dump(const AppState *const app_state)
  1064. {
  1065. const time_t now = time(NULL);
  1066. const Playlist *const playlist = app_state->playlist;
  1067. const Song *song = playlist->songs[playlist->current_playing];
  1068. FILE *const fp = fopen(state_dump, "w");
  1069. if (fp == NULL) {
  1070. printf("Error opening file!\n");
  1071. exit(1);
  1072. }
  1073. if (playlist->status != STOPPED) {
  1074. fprintf(fp, "{\"status\" : \"%s\",\
  1075. \"artist\" : \"%s\",\
  1076. \"album\" : \"%s\",\
  1077. \"song\" : \"%s\",\
  1078. \"length\" : %d,\
  1079. \"playtime\" : %d,\
  1080. \"time\" : %ld}\n", playlist->status == PLAYING ? "playing" : "paused", app_state->artist->name, app_state->album->name, song->name, song->duration, playlist->play_time, now);
  1081. } else {
  1082. fprintf(fp, "\n");
  1083. }
  1084. fclose(fp); // close the file after writing
  1085. }
  1086. /**
  1087. * Notifies the user about the currently playing song.
  1088. *
  1089. * This function takes in a pointer to an AppState struct and generates a notification message with information about the currently playing song.
  1090. * It then executes the notification using the system command.
  1091. *
  1092. * @param app_state A pointer to an AppState struct containing information about the application's state.
  1093. */
  1094. void notify(const AppState *const app_state)
  1095. {
  1096. char *const notification = calloc(NOTIFICATION_LENGTH, sizeof(char));
  1097. const Playlist *const playlist = app_state->playlist;
  1098. const Song *const song = playlist->songs[playlist->current_playing];
  1099. if (notify_cmd != NULL) {
  1100. snprintf(notification, NOTIFICATION_LENGTH,
  1101. "%s \"%s\" \"%s - %s - %s\"\n", notify_cmd,
  1102. "Now playing", app_state->artist->name,
  1103. app_state->album->name, song->name);
  1104. }
  1105. system(notification);
  1106. free(notification);
  1107. }
  1108. /**
  1109. * Plays a song from the current playlist.
  1110. * If defined, sends a notification and/or dumps the AppState into a file
  1111. *
  1112. * This function stops any currently-playing songs and starts playing the specified song. It generates a URL to stream the song,
  1113. * executes the specified player program with appropriate flags, and updates the playlist state accordingly.
  1114. *
  1115. * @param app_state A pointer to the current application state.
  1116. * @param index The index of the song in the playlist to play.
  1117. */
  1118. void play_song(const AppState *const app_state, const int index)
  1119. {
  1120. // Get references to the relevant data structures.
  1121. Playlist *const playlist = app_state->playlist;
  1122. const Playback_Program program = app_state->program;
  1123. // Validate the song index.
  1124. if (index >= playlist->size) {
  1125. fprintf(stderr, "Invalid song index.\n");
  1126. return;
  1127. }
  1128. // Send SIGTERM to the playlist process to stop any other playing processes.
  1129. change_playback_status(playlist->pid, SIGTERM);
  1130. // Set up playlist state for the new song.
  1131. const Song *const song = playlist->songs[index];
  1132. playlist->start_time = time(NULL);
  1133. playlist->current_playing = index;
  1134. playlist->status = PLAYING;
  1135. playlist->play_time = 0;
  1136. // Generate URL to stream the song.
  1137. char *url = NULL;
  1138. generate_subsonic_url(app_state->connection, PLAY, song->id, &url);
  1139. // Construct the command string to run the playback program.
  1140. const size_t command_len =
  1141. snprintf(NULL, 0, "%s %s \"%s\" > /dev/null 2>&1",
  1142. program.executable,
  1143. program.flags, url) + 1;
  1144. char command[command_len];
  1145. snprintf(command, command_len, "%s %s \"%s\" > /dev/null 2>&1",
  1146. program.executable, program.flags, url);
  1147. // Set up arguments to pass to the playback thread.
  1148. PlaybackThreadArgs *const args = malloc(sizeof(PlaybackThreadArgs));
  1149. args->url = url;
  1150. args->command = command;
  1151. args->playlist_pid = playlist->pid;
  1152. // Start the playback thread and detach it so that it runs independently.
  1153. pthread_t thread_id;
  1154. pthread_create(&thread_id, NULL, &playback_thread, (void *) args);
  1155. pthread_detach(thread_id);
  1156. // Store the new process ID in the playlist state.
  1157. playlist->pid = get_pid(program);
  1158. if (notify_cmd != NULL) {
  1159. notify(app_state);
  1160. }
  1161. if (state_dump != NULL) {
  1162. dump(app_state);
  1163. }
  1164. }
  1165. /**
  1166. * Stops any currently-playing song in the playlist.
  1167. *
  1168. * This function sends a SIGTERM signal to the process ID associated with the current playlist, if one exists, and updates the
  1169. * playlist state to indicate that playback has stopped.
  1170. *
  1171. * @param app_state A pointer to the current application state.
  1172. */
  1173. void stop_playback(const AppState *const app_state)
  1174. {
  1175. // Get a reference to the current playlist.
  1176. Playlist *const playlist = app_state->playlist;
  1177. // Check if there is a current song playing.
  1178. if (playlist->status != STOPPED) {
  1179. // Stop the playback process and update playlist state.
  1180. change_playback_status(app_state->playlist->pid, SIGTERM);
  1181. app_state->playlist->status = STOPPED;
  1182. app_state->playlist->start_time = (time_t) NULL;
  1183. app_state->playlist->play_time = -1;
  1184. }
  1185. if (state_dump != NULL) {
  1186. dump(app_state);
  1187. }
  1188. }
  1189. /**
  1190. * Pauses or resumes playback of a song in the playlist.
  1191. *
  1192. * If there is a current song playing, this function sends a SIGSTOP signal to pause playback, or a SIGCONT signal to resume playback,
  1193. * depending on the current state of the playlist. It then updates the playlist state to reflect the new status.
  1194. *
  1195. * @param app_state A pointer to the current application state.
  1196. */
  1197. void pause_resume(const AppState *const app_state)
  1198. {
  1199. Playlist *const playlist = app_state->playlist;
  1200. // Check if there is a current song playing.
  1201. switch (playlist->status) {
  1202. case STOPPED:
  1203. // Do nothing if no song is playing.
  1204. break;
  1205. case PLAYING:
  1206. // Pause playback and update playlist state.
  1207. change_playback_status(playlist->pid, SIGSTOP);
  1208. playlist->status = PAUSED;
  1209. break;
  1210. case PAUSED:
  1211. // Resume playback and update playlist state.
  1212. const time_t now = time(NULL);
  1213. playlist->start_time = now;
  1214. change_playback_status(playlist->pid, SIGCONT);
  1215. playlist->status = PLAYING;
  1216. break;
  1217. }
  1218. if (state_dump != NULL) {
  1219. dump(app_state);
  1220. }
  1221. return;
  1222. }
  1223. /**
  1224. * Callback function used to receive data from a libcurl request and write it to a buffer.
  1225. *
  1226. * This function is called by libcurl whenever new data is received in response to a URL request. It appends the new data to the existing
  1227. * buffer pointed to by the `data` parameter, reallocating memory as necessary, and updates the size of the buffer accordingly.
  1228. *
  1229. * @param ptr A pointer to the received data.
  1230. * @param size The size of each data element.
  1231. * @param nmemb The number of elements received.
  1232. * @param data A pointer to a url_data struct containing information about the buffer being written to.
  1233. * @return The total number of bytes written to the buffer.
  1234. */
  1235. int write_url_data(void *const ptr, const int size, const int nmemb,
  1236. struct url_data *const data)
  1237. {
  1238. // Get the starting index and number of bytes to write.
  1239. const int start_index = data->size;
  1240. const int bytes_written = (size * nmemb);
  1241. // Resize the data buffer to accommodate the new data.
  1242. char *const new_data_ptr = realloc(data->data, data->size + bytes_written + 1); // +1 for '\0'
  1243. if (new_data_ptr == NULL) {
  1244. fprintf(stderr, "Error: Failed to allocate memory for URL data.\n");
  1245. if (data->data) {
  1246. free(data->data);
  1247. data->data = NULL;
  1248. data->size = 0;
  1249. }
  1250. return 0;
  1251. }
  1252. data->data = new_data_ptr;
  1253. memcpy((data->data + start_index), ptr, bytes_written);
  1254. data->size += bytes_written;
  1255. data->data[data->size] = '\0';
  1256. return bytes_written;
  1257. }
  1258. /**
  1259. * Generates a Subsonic API URL for a given operation and data.
  1260. *
  1261. * @param conn The connection settings to use for generating the URL.
  1262. * @param operation The operation to perform.
  1263. * @param data The data to include in the URL (optional).
  1264. * @param url A pointer to a char pointer to store the generated URL.
  1265. *
  1266. * @return void
  1267. *
  1268. * @note The memory for the generated URL is allocated dynamically and must be freed by the caller.
  1269. */
  1270. void generate_subsonic_url(const Connection *const conn, enum Operation operation,
  1271. const char *data, char **url)
  1272. {
  1273. const char *path;
  1274. // Set the path based on 'operation'
  1275. switch (operation) {
  1276. case PING:
  1277. path = "rest/ping.view";
  1278. break;
  1279. case ARTISTS:
  1280. path = "rest/getArtists";
  1281. break;
  1282. case ALBUMS:
  1283. path = "rest/getArtist";
  1284. break;
  1285. case SONGS:
  1286. path = "rest/getAlbum";
  1287. break;
  1288. case PLAY:
  1289. path = "rest/stream";
  1290. break;
  1291. default:
  1292. fprintf(stderr, "Invalid operation.\n");
  1293. return;
  1294. }
  1295. const size_t len_url =
  1296. snprintf(NULL, 0, "%s:%d/%s?f=json&u=%s&p=%s&v=%s&c=%s%s%s",
  1297. conn->url, conn->port, path, conn->user, conn->password,
  1298. conn->version, conn->app, data ? "&id=" : "",
  1299. data ? data : "");
  1300. *url = malloc(sizeof(char) * (len_url + 1));
  1301. if (NULL == (*url)) {
  1302. fprintf(stderr, "Failed to allocate memory for the URL.\n");
  1303. return;
  1304. }
  1305. snprintf(*url, len_url + 1, "%s:%d/%s?f=json&u=%s&p=%s&v=%s&c=%s%s%s",
  1306. conn->url, conn->port, path, conn->user, conn->password,
  1307. conn->version, conn->app, data ? "&id=" : "", data ? data : "");
  1308. }
  1309. /**
  1310. * Fetches data from a given URL using libcurl and returns it as a string.
  1311. *
  1312. * @param url The URL to fetch data from.
  1313. *
  1314. * @return A pointer to the string containing the fetched data. This memory should be freed by the caller.
  1315. * If an error occurs, NULL is returned.
  1316. */
  1317. char *fetch_url_data(const char *const url)
  1318. {
  1319. CURL *const curl_handle = curl_easy_init();
  1320. const int initial_size = 4096;
  1321. struct url_data url_data = {
  1322. .size = 0,
  1323. .data = malloc(initial_size)
  1324. };
  1325. if (url_data.data == NULL) {
  1326. fprintf(stderr, "Error: Failed to allocate memory.\n");
  1327. exit(EXIT_FAILURE);
  1328. }
  1329. url_data.data[0] = '\0';
  1330. // If curl_handle failed, exit.
  1331. if (curl_handle == NULL) {
  1332. fprintf(stderr, "Error: Failed to initialize curl handle.\n");
  1333. exit(EXIT_FAILURE);
  1334. }
  1335. // Set curl options and perform request.
  1336. curl_easy_setopt(curl_handle, CURLOPT_URL, url);
  1337. curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, write_url_data);
  1338. curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, &url_data);
  1339. curl_easy_setopt(curl_handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2);
  1340. const CURLcode curl_result = curl_easy_perform(curl_handle);
  1341. if (curl_result != CURLE_OK) {
  1342. fprintf(stderr, "curl_easy_perform() failed: %s\n",
  1343. curl_easy_strerror(curl_result));
  1344. free(url_data.data);
  1345. return NULL;
  1346. }
  1347. // Return fetched data and clean up.
  1348. curl_easy_cleanup(curl_handle);
  1349. return url_data.data;
  1350. }
  1351. /**
  1352. * Fetches a list of artists from a Subsonic server using the provided connection information, populates a database structure
  1353. * with the artist information, and stores the result in memory.
  1354. *
  1355. * @param connection A pointer to a Connection struct that contains information about the Subsonic server.
  1356. * @param db A pointer to a Database struct that will be populated with the list of artists.
  1357. *
  1358. * @return None.
  1359. *
  1360. * @remarks The caller is responsible for freeing the memory allocated for the Database struct.
  1361. */
  1362. void get_artists(const Connection *const connection, Database *const db)
  1363. {
  1364. // Generate the URL to fetch the artists data
  1365. char *url = NULL;
  1366. generate_subsonic_url(connection, ARTISTS, NULL, &url);
  1367. char *const response = fetch_url_data(url);
  1368. cJSON *const response_root = cJSON_Parse(response);
  1369. cJSON const *subsonic_response =
  1370. cJSON_GetObjectItemCaseSensitive(response_root,
  1371. "subsonic-response");
  1372. if (strcmp
  1373. ("ok",
  1374. cJSON_GetObjectItemCaseSensitive(subsonic_response,
  1375. "status")->valuestring) != 0) {
  1376. fprintf(stderr,
  1377. "Error: Failed to retrieve artists from Subsonic server/\n");
  1378. exit(EXIT_FAILURE);
  1379. }
  1380. int number_artists = 0;
  1381. const cJSON *const index =
  1382. cJSON_GetObjectItemCaseSensitive(cJSON_GetObjectItemCaseSensitive
  1383. (subsonic_response, "artists"),
  1384. "index");
  1385. const cJSON *alphabetical_index;
  1386. // Count the number of artists in the response
  1387. cJSON_ArrayForEach(alphabetical_index, index) {
  1388. const cJSON *const artists_list =
  1389. cJSON_GetObjectItemCaseSensitive(alphabetical_index,
  1390. "artist");
  1391. const cJSON *artist;
  1392. cJSON_ArrayForEach(artist, artists_list) {
  1393. number_artists++;
  1394. }
  1395. }
  1396. // Allocate memory for artists in the database
  1397. db->number_artists = number_artists;
  1398. db->artists = malloc(sizeof(Artist) * number_artists);
  1399. // Loop through the artists in the response and store them in the database
  1400. int i = 0;
  1401. cJSON_ArrayForEach(alphabetical_index, index) {
  1402. const cJSON *const artists_list =
  1403. cJSON_GetObjectItemCaseSensitive(alphabetical_index,
  1404. "artist");
  1405. const cJSON *artist;
  1406. cJSON_ArrayForEach(artist, artists_list) {
  1407. Artist *const a = &(db->artists[i]);
  1408. a->id =
  1409. strdup(cJSON_GetObjectItemCaseSensitive(artist, "id")->
  1410. valuestring);
  1411. a->name =
  1412. strdup(cJSON_GetObjectItemCaseSensitive(artist, "name")->
  1413. valuestring);
  1414. a->number_albums = 0;
  1415. a->albums = NULL;
  1416. i++;
  1417. }
  1418. }
  1419. // Clean up
  1420. cJSON_Delete(response_root);
  1421. free(url);
  1422. free(response);
  1423. }
  1424. /**
  1425. * Finds an artist in the database based on the artist ID.
  1426. *
  1427. * @param db The database to search.
  1428. * @param artist_id The ID of the artist to find.
  1429. *
  1430. * @return The index of the artist in the database, or -1 if not found.
  1431. */
  1432. int find_artist(const Database *const db, const char *const artist_id)
  1433. {
  1434. // Check if the artist is already in the database
  1435. for (int i = 0; i < db->number_artists; i++) {
  1436. const Artist *const artist = &db->artists[i];
  1437. if (strcmp(artist->id, artist_id) == 0) {
  1438. // Artist is already in the database, return the album list
  1439. return i;
  1440. }
  1441. }
  1442. return -1;
  1443. }
  1444. /**
  1445. * Finds an album in the given artist's album list with the given ID.
  1446. *
  1447. * @param artist The artist to search for the album in.
  1448. * @param album_id The ID of the album to search for.
  1449. *
  1450. * @return The index of the album in the artist's album list, or -1 if the album was not found.
  1451. */
  1452. int find_album(const Artist *const artist, const char *const album_id)
  1453. {
  1454. // Check if the artist is already in the database
  1455. for (int i = 0; i < artist->number_albums; i++) {
  1456. const Album *const album = &artist->albums[i];
  1457. if (strcmp(album->id, album_id) == 0) {
  1458. // Album is already in the database, return its index
  1459. return i;
  1460. }
  1461. }
  1462. return -1;
  1463. }
  1464. /**
  1465. * Retrieves albums for an artist from a Subsonic server and stores them in the provided Artist struct.
  1466. *
  1467. * @param conn Connection struct containing information about the Subsonic server
  1468. * @param artist Artist struct to store the retrieved albums in
  1469. */
  1470. void request_albums(const Connection *const conn, Artist *const artist)
  1471. {
  1472. if (artist->albums != NULL) {
  1473. // If album information for this artist has already been retrieved, return
  1474. return;
  1475. }
  1476. char *url = NULL;
  1477. generate_subsonic_url(conn, ALBUMS, artist->id, &url);
  1478. char *const response = fetch_url_data(url);
  1479. cJSON *const response_root = cJSON_Parse(response);
  1480. cJSON *const subsonic_response =
  1481. cJSON_GetObjectItemCaseSensitive(response_root,
  1482. "subsonic-response");
  1483. if (strcmp
  1484. ("ok",
  1485. cJSON_GetObjectItemCaseSensitive(subsonic_response,
  1486. "status")->valuestring) != 0) {
  1487. fprintf(stderr,
  1488. "Error: Failed to retrieve artists from Subsonic server/\n");
  1489. exit(EXIT_FAILURE);
  1490. }
  1491. const cJSON *const artist_json =
  1492. cJSON_GetObjectItemCaseSensitive(subsonic_response, "artist");
  1493. const int number_albums = cJSON_GetObjectItemCaseSensitive(artist_json,
  1494. "albumCount")->valueint;
  1495. if (number_albums == 0) {
  1496. return; // No albums found for this artist
  1497. }
  1498. artist->albums = malloc(sizeof(Album) * number_albums);
  1499. artist->number_albums = number_albums;
  1500. const cJSON *const albums =
  1501. cJSON_GetObjectItemCaseSensitive(artist_json, "album");
  1502. const cJSON *album;
  1503. int i = 0;
  1504. cJSON_ArrayForEach(album, albums) {
  1505. Album *const a = &(artist->albums[i]);
  1506. a->id =
  1507. strdup(cJSON_GetObjectItemCaseSensitive(album, "id")->valuestring);
  1508. a->name =
  1509. strdup(cJSON_GetObjectItemCaseSensitive(album, "name")->
  1510. valuestring);
  1511. a->number_songs = 0;
  1512. a->songs = NULL;
  1513. i++;
  1514. }
  1515. cJSON_Delete(response_root);
  1516. free(url);
  1517. free(response);
  1518. }
  1519. /**
  1520. * Retrieves album information for a given artist from the server.
  1521. *
  1522. * This function retrieves album information for a given artist from the server
  1523. * by sending a request over the provided connection. If the artist already has
  1524. * album information in the database, the function does nothing.
  1525. *
  1526. * @param conn Pointer to the Connection object used to send requests to the server.
  1527. * @param db Pointer to the Database object containing the artist and album information.
  1528. * @param artist_id The ID of the artist for which to retrieve album information.
  1529. */
  1530. void get_albums(const Connection *const conn, const Database *const db,
  1531. const char *const artist_id)
  1532. {
  1533. // Retrieve the position occupied by the artist in the database
  1534. const int artist_idx = find_artist(db, artist_id);
  1535. Artist *const artist = &(db->artists[artist_idx]);
  1536. // Check if the artist already has album information
  1537. if (artist->number_albums == 0) {
  1538. request_albums(conn, artist);
  1539. }
  1540. }
  1541. /**
  1542. * Retrieves song information for a given album by an artist from the server.
  1543. *
  1544. * This function retrieves song information for a given album by an artist from the
  1545. * server by sending a request over the provided connection. If the album already has
  1546. * song information in the database, the function does nothing.
  1547. *
  1548. * @param conn Pointer to the Connection object used to send requests to the server.
  1549. * @param db Pointer to the Database object containing the artist and album information.
  1550. * @param artist_id The ID of the artist for which to retrieve album information.
  1551. * @param album_id The ID of the album for which to retrieve song information.
  1552. */
  1553. void get_songs(const Connection *const conn, const Database *const db,
  1554. const char *const artist_id, const char *const album_id)
  1555. {
  1556. // Retrieve the position occupied by the artist in the database
  1557. const int artist_idx = find_artist(db, artist_id);
  1558. const Artist *const artist = &(db->artists[artist_idx]);
  1559. // Retrieve the position occupied by the album in the Artist struct
  1560. const int album_idx = find_album(artist, album_id);
  1561. Album *const album = &(artist->albums[album_idx]);
  1562. // Check if the album already has song information
  1563. if (album->number_songs == 0) {
  1564. request_songs(conn, album);
  1565. }
  1566. }
  1567. /**
  1568. * Calculates the approximate duration of a song based on its size and bit rate.
  1569. *
  1570. * This function calculates the approximate duration of a song based on its size and
  1571. * bit rate. It uses the formula: size*8/rate/1000 to compute the duration in seconds.
  1572. *
  1573. * @param song A JSON object representing the song to compute the duration for.
  1574. * The object must have "size" and "bitRate" fields containing the size
  1575. * of the song in bytes and its bit rate in kilobits per second (kbps),
  1576. * respectively.
  1577. *
  1578. * @return The approximate duration of the song in seconds.
  1579. */
  1580. int approximate_duration(const cJSON *const song)
  1581. {
  1582. const int size = cJSON_GetObjectItemCaseSensitive(song, "size")->valueint;
  1583. const int rate =
  1584. cJSON_GetObjectItemCaseSensitive(song, "bitRate")->valueint;
  1585. return size * 8 / rate / 1000;
  1586. }
  1587. /**
  1588. * Retrieves song information for a given album from the Subsonic server.
  1589. *
  1590. * This function retrieves song information for a given album by sending a request to
  1591. * the Subsonic server over the provided connection. The retrieved information is stored
  1592. * in the provided Album object. If the album already has song information, the function
  1593. * does nothing.
  1594. *
  1595. * @param conn Pointer to the Connection object used to send requests to the server.
  1596. * @param album Pointer to the Album object to store the retrieved song information.
  1597. */
  1598. void request_songs(const Connection *const conn, Album *const album)
  1599. {
  1600. if (album->songs != NULL) {
  1601. // If album information for this artist has already been retrieved, return
  1602. return;
  1603. }
  1604. char *url = NULL;
  1605. generate_subsonic_url(conn, SONGS, album->id, &url);
  1606. char *const response = fetch_url_data(url);
  1607. cJSON *const response_root = cJSON_Parse(response);
  1608. cJSON *const subsonic_response =
  1609. cJSON_GetObjectItemCaseSensitive(response_root, "subsonic-response");
  1610. if (strcmp("ok", cJSON_GetObjectItemCaseSensitive(subsonic_response,
  1611. "status")->valuestring) != 0) {
  1612. fprintf(stderr,
  1613. "Error: Failed to retrieve artists from Subsonic server/\n");
  1614. exit(EXIT_FAILURE);
  1615. }
  1616. const cJSON *const album_json =
  1617. cJSON_GetObjectItemCaseSensitive(subsonic_response, "album");
  1618. const int number_songs =
  1619. cJSON_GetObjectItemCaseSensitive(album_json, "songCount")->valueint;
  1620. album->songs = malloc(sizeof(Song) * number_songs);
  1621. album->number_songs = number_songs;
  1622. const cJSON *const songs = cJSON_GetObjectItemCaseSensitive(album_json, "song");
  1623. const cJSON *song;
  1624. int i = 0;
  1625. cJSON_ArrayForEach(song, songs) {
  1626. Song *const s = &(album->songs[i]);
  1627. s->id =
  1628. strdup(cJSON_GetObjectItemCaseSensitive(song, "id")->valuestring);
  1629. s->name =
  1630. strdup(cJSON_GetObjectItemCaseSensitive(song, "title")->
  1631. valuestring);
  1632. s->duration = cJSON_HasObjectItem(song, "duration") ?
  1633. cJSON_GetObjectItemCaseSensitive(song, "duration")->valueint :
  1634. approximate_duration(song);
  1635. i++;
  1636. }
  1637. cJSON_Delete(response_root);
  1638. free(url);
  1639. free(response);
  1640. }
  1641. /**
  1642. * Plays the previous or next song in the playlist.
  1643. *
  1644. * This function plays the previous or next song in the playlist depending on the
  1645. * action provided. If the current song is the first/last one in the playlist and
  1646. * the user triggers a "previous"/"next" action, the function does nothing.
  1647. *
  1648. * @param app_state Pointer to the AppState object containing the current state
  1649. * of the application.
  1650. * @param action An integer constant representing the action to perform. It can take
  1651. * one of two values: `previous` (to play the previous song) or `next`
  1652. * (to play the next song).
  1653. */
  1654. void play_previous_next(AppState *const app_state, const int action)
  1655. {
  1656. Playlist *const playlist = app_state->playlist;
  1657. switch (action) {
  1658. case previous:
  1659. if (playlist->current_playing > 0) {
  1660. --(playlist->current_playing);
  1661. play_song(app_state, playlist->current_playing);
  1662. }
  1663. break;
  1664. case next:
  1665. if (playlist->current_playing < playlist->size - 1) {
  1666. ++(playlist->current_playing);
  1667. play_song(app_state, playlist->current_playing);
  1668. }
  1669. break;
  1670. default:
  1671. break;
  1672. }
  1673. }
  1674. /**
  1675. * Adds songs to the playlist based on the current panel of the application.
  1676. *
  1677. * This function adds songs to the playlist based on the current panel of the
  1678. * application. If the current panel is the "Artists" panel, it adds all songs from all
  1679. * albums of the currently selected artist. If the current panel is the "Albums" panel,
  1680. * it adds all songs from the currently selected album. If the current panel is the
  1681. * "Songs" panel, it adds the currently selected song to the playlist.
  1682. *
  1683. * @param app_state Pointer to the AppState object containing the current state of the
  1684. * application.
  1685. *
  1686. * @return The index of the first song that was added to the playlist (0-indexed), or 0 if
  1687. * no songs were added to the playlist.
  1688. */
  1689. int add_to_playlist(AppState *app_state)
  1690. {
  1691. Playlist *const playlist = app_state->playlist;
  1692. const Artist *const artist = app_state->artist;
  1693. const Album *album = app_state->album;
  1694. const Song *song = NULL;
  1695. const int first_song_to_play = playlist->size;
  1696. const int has_songs = playlist->size == 0 ? 0 : 1;
  1697. const PanelType current_panel = app_state->current_panel;
  1698. switch (current_panel) {
  1699. case PANEL_ARTISTS:
  1700. for (int i = 0; i < artist->number_albums; ++i) {
  1701. album = &artist->albums[i];
  1702. get_songs(app_state->connection, app_state->db, artist->id,
  1703. album->id);
  1704. for (int j = 0; j < album->number_songs; ++j) {
  1705. song = &album->songs[j];
  1706. add_song(song, playlist);
  1707. }
  1708. }
  1709. break;
  1710. case PANEL_ALBUMS:
  1711. get_songs(app_state->connection, app_state->db, artist->id,
  1712. album->id);
  1713. for (int i = 0; i < album->number_songs; i++) {
  1714. song = &album->songs[i];
  1715. add_song(song, playlist);
  1716. }
  1717. break;
  1718. case PANEL_SONGS:
  1719. song = &album->songs[app_state->selected_song_idx];
  1720. add_song(song, playlist);
  1721. break;
  1722. default:
  1723. break;
  1724. }
  1725. return has_songs ? first_song_to_play : 0;
  1726. }
  1727. /**
  1728. * Cleans up the application state and frees allocated memory.
  1729. *
  1730. * This function cleans up the application state by freeing all allocated memory. It
  1731. * specifically frees the playlist Song pointers, and deallocates memory used by the
  1732. * database (including Artists, Albums, and Songs). After this function is called, the
  1733. * AppState object should no longer be used.
  1734. *
  1735. * @param app_state Pointer to the AppState object containing the current state of the
  1736. * application.
  1737. */
  1738. void cleanup(AppState *app_state)
  1739. {
  1740. if (app_state == NULL) {
  1741. return;
  1742. }
  1743. // Make the playlist Song pointers point to NULL to avoid problems when we delete the underlying data
  1744. for (int i = 0; i < app_state->playlist->size; i++) {
  1745. app_state->playlist->songs[i] = NULL;
  1746. }
  1747. free(app_state->playlist->songs);
  1748. // Clean the database
  1749. for (int i = 0; i < app_state->db->number_artists; i++) {
  1750. Artist *artist = &(app_state->db->artists[i]);
  1751. for (int j = 0; j < artist->number_albums; j++) {
  1752. Album *album = &(artist->albums[j]);
  1753. for (int k = 0; k < album->number_songs; k++) {
  1754. Song *song = &(album->songs[k]);
  1755. free(song->id);
  1756. free(song->name);
  1757. }
  1758. free(album->id);
  1759. free(album->name);
  1760. free(album->songs);
  1761. }
  1762. free(artist->id);
  1763. free(artist->name);
  1764. free(artist->albums);
  1765. }
  1766. free(app_state->db->artists);
  1767. }
  1768. /**
  1769. * Retrieves the artist and album information for a given song.
  1770. *
  1771. * @param database Pointer to the database containing the artist, album, and song data.
  1772. * @param song Pointer to the song for which to retrieve the artist and album information.
  1773. * @return A SongInfo struct containing pointers to the artist and album of the given song.
  1774. * If the song is not found in any album, both artist and album pointers will be NULL.
  1775. */
  1776. SongInfo get_song_info(const Database *const database, const Song *const song) {
  1777. SongInfo song_info = { NULL, NULL };
  1778. for (int i = 0; i < database->number_artists; i++) {
  1779. const Artist *const artist = &database->artists[i];
  1780. for (int j = 0; j < artist->number_albums; j++) {
  1781. const Album *const album = &artist->albums[j];
  1782. for (int k = 0; k < album->number_songs; k++) {
  1783. if (strcmp(album->songs[k].id, song->id) == 0) {
  1784. song_info.artist = artist->name;
  1785. song_info.album = album->name;
  1786. return song_info;
  1787. }
  1788. }
  1789. }
  1790. }
  1791. return song_info; // Song not found in any album
  1792. }
  1793. /**
  1794. * Prints a progress bar indicating the current song's playback progress.
  1795. *
  1796. * This function prints a progress bar indicating the current song's playback progress.
  1797. * The progress bar is printed to the first window in the provided array of windows. If
  1798. * the playlist is stopped, the function returns without printing anything.
  1799. *
  1800. * @param windows An array of WINDOW pointers representing the windows to print to.
  1801. * The first window in the array is used to print the progress bar.
  1802. * @param app_state Pointer to the AppState object containing the current state of the
  1803. * application.
  1804. */
  1805. void print_progress_bar(WINDOW **windows, const AppState *const app_state)
  1806. {
  1807. // Get the window to print to
  1808. WINDOW *window = windows[0];
  1809. const Playlist *const playlist = app_state->playlist;
  1810. // Clear the window
  1811. werase(window);
  1812. if (playlist == NULL || playlist->status == STOPPED) {
  1813. wrefresh(window);
  1814. return;
  1815. }
  1816. // Retrieve the current song and its duration
  1817. const Song *const song = playlist->songs[playlist->current_playing];
  1818. const int duration = song->duration;
  1819. // Compute the progress bar width based on the window width
  1820. const int bar_width = getmaxx(window) - 2; // Subtracting 2 for the borders
  1821. // Compute the number of hashes to print in the progress bar
  1822. const int num_hashes =
  1823. (int) ((double) (playlist->play_time) / (double) (duration) *
  1824. (double) (bar_width));
  1825. // Retrieve the album and artist information
  1826. const SongInfo song_info = get_song_info(app_state->db, song);
  1827. const char *status_symbol;
  1828. switch (playlist->shuffle_repeat_status) {
  1829. case SHUFFLE:
  1830. status_symbol = strdup(appearance[ind_shuffle]);
  1831. break;
  1832. case REPEAT:
  1833. status_symbol = strdup(appearance[ind_repeat]);
  1834. break;
  1835. default:
  1836. status_symbol = " ";
  1837. break;
  1838. }
  1839. // Print the progress bar
  1840. wprintw(window, "\n %s - %s - %s [%s]\n", song_info.artist, song_info.album,
  1841. song->name, status_symbol);
  1842. wmove(window, 2, 1);
  1843. for (int i = 0; i < bar_width; ++i) {
  1844. if (i < num_hashes) {
  1845. waddstr(window, appearance[ind_played]);
  1846. } else {
  1847. waddstr(window, appearance[ind_unplayed]);
  1848. }
  1849. }
  1850. // Refresh the window
  1851. wrefresh(window);
  1852. }
  1853. /**
  1854. * Updates the state of the current playlist and starts playing the next song.
  1855. *
  1856. * This function updates the state of the current playlist based on the shuffle/repeat settings, and starts playing the next song in the updated playlist. If the current view is VIEW_PLAYLIST, it also refreshes the playlist window to display the updated state of the playlist.
  1857. *
  1858. * @param[in] app_state A pointer to the AppState object containing the current state of the application.
  1859. *
  1860. * @return void
  1861. */
  1862. void update_playlist_state(AppState *app_state)
  1863. {
  1864. Playlist *const playlist = app_state->playlist;
  1865. if (playlist == NULL) {
  1866. return;
  1867. }
  1868. int last_song = 0;
  1869. const int playlist_size = playlist->size;
  1870. // Increment index to play next song
  1871. switch (playlist->shuffle_repeat_status) {
  1872. case NONE:
  1873. if (playlist_size - 1 > playlist->current_playing) {
  1874. playlist->current_playing++;
  1875. } else {
  1876. last_song = 1;
  1877. playlist->status = STOPPED;
  1878. stop_playback(app_state);
  1879. }
  1880. break;
  1881. case SHUFFLE:
  1882. playlist->current_playing = rand() % playlist_size;
  1883. break;
  1884. case REPEAT:
  1885. break;
  1886. }
  1887. // Start playing the next song
  1888. if (last_song == 0) {
  1889. playlist->play_time = 0;
  1890. playlist->start_time = time(NULL);
  1891. play_song(app_state, playlist->current_playing);
  1892. }
  1893. if (app_state->current_view == VIEW_PLAYLIST) {
  1894. refresh_windows(app_state, app_state->windows[WINDOW_PLAYLIST], 1);
  1895. }
  1896. }
  1897. /*
  1898. * Updates the shuffle and repeat status of the playlist based on the given action.
  1899. *
  1900. * @param playlist Pointer to the playlist whose shuffle and repeat status should be updated.
  1901. * @param action Character representing the action to perform (either 'shuffle' or 'repeat').
  1902. */
  1903. void change_shuffle_repeat(Playlist *const playlist, const char action)
  1904. {
  1905. if (playlist == NULL) {
  1906. return;
  1907. }
  1908. switch (action) {
  1909. case shuffle:
  1910. playlist->shuffle_repeat_status =
  1911. (playlist->shuffle_repeat_status == SHUFFLE) ? NONE : SHUFFLE;
  1912. break;
  1913. case repeat:
  1914. playlist->shuffle_repeat_status =
  1915. (playlist->shuffle_repeat_status == REPEAT) ? NONE : REPEAT;
  1916. break;
  1917. }
  1918. }
  1919. /**
  1920. * Returns the chord corresponding to a given keypress.
  1921. *
  1922. * @param keypress The keypress for which we want to find the chord.
  1923. * @param chords A 2D array of chords and their corresponding keypresses.
  1924. * @param len The length of the chords array.
  1925. *
  1926. * @return The chord corresponding to the given keypress, or -1 if no such chord exists.
  1927. */
  1928. int get_chord(const int keypress)
  1929. {
  1930. static int initialized = 0;
  1931. static int hash_table[512] = { 0 }; // We use the maximum number as per ncurses definitions
  1932. if (initialized == 0) {
  1933. for (int i = 0; i < len_chords; i++) {
  1934. hash_table[chords[i][0]] = chords[i][1];
  1935. }
  1936. initialized = 1;
  1937. }
  1938. if (keypress == -1) {
  1939. return -1;
  1940. }
  1941. const int chord = hash_table[keypress];
  1942. if (chord != 0) {
  1943. return chord;
  1944. }
  1945. return -1;
  1946. }
  1947. /**
  1948. * Handles a given action and updates the application state accordingly.
  1949. *
  1950. * @param action The action to handle.
  1951. * @param app_state A pointer to the current state of the application.
  1952. */
  1953. void handle_action(const int action, AppState *app_state)
  1954. {
  1955. if (action == -1) {
  1956. return;
  1957. }
  1958. Playlist *playlist = app_state->playlist;
  1959. WINDOW **info_windows = app_state->windows[WINDOW_INFO];
  1960. WINDOW **playlist_windows = app_state->windows[WINDOW_PLAYLIST];
  1961. switch (action) {
  1962. case up:
  1963. case down:
  1964. case left:
  1965. case right:
  1966. case bottom:
  1967. case top:
  1968. {
  1969. const SpecialMovement special_movement =
  1970. (action == bottom) ? MOVE_BOTTOM :
  1971. (action == top) ? MOVE_TOP : NUM_MOVEMENTS;
  1972. movement(action, app_state, special_movement);
  1973. if (app_state->current_view == VIEW_INFO) {
  1974. refresh_windows(app_state, info_windows, 3);
  1975. } else {
  1976. refresh_windows(app_state, playlist_windows, 1);
  1977. }
  1978. break;
  1979. }
  1980. case add_and_play:
  1981. if (app_state->current_view == VIEW_INFO) {
  1982. const int first_song_to_play = add_to_playlist(app_state);
  1983. play_song(app_state, first_song_to_play);
  1984. }
  1985. if (app_state->current_view == VIEW_PLAYLIST) {
  1986. play_song(app_state, playlist->selected_song_idx);
  1987. refresh_windows(app_state, playlist_windows, 1);
  1988. }
  1989. break;
  1990. case previous:
  1991. case next:
  1992. play_previous_next(app_state, action);
  1993. if (app_state->current_view == VIEW_INFO) {
  1994. refresh_windows(app_state, info_windows, NUM_WINDOWS);
  1995. }
  1996. if (app_state->current_view == VIEW_PLAYLIST) {
  1997. refresh_windows(app_state, playlist_windows, 1);
  1998. }
  1999. break;
  2000. case shuffle:
  2001. case repeat:
  2002. change_shuffle_repeat(playlist, action);
  2003. break;
  2004. case add:
  2005. add_to_playlist(app_state);
  2006. break;
  2007. case remove_one:
  2008. if (app_state->current_view == VIEW_PLAYLIST) {
  2009. delete_song(app_state);
  2010. refresh_windows(app_state, playlist_windows, 1);
  2011. }
  2012. break;
  2013. case remove_all:
  2014. if (app_state->current_view == VIEW_PLAYLIST) {
  2015. while (playlist->size > 0) {
  2016. delete_song(app_state);
  2017. }
  2018. refresh_windows(app_state, playlist_windows, 1);
  2019. }
  2020. break;
  2021. case play_pause:
  2022. pause_resume(app_state);
  2023. break;
  2024. case stop:
  2025. stop_playback(app_state);
  2026. break;
  2027. case main_view:
  2028. if (app_state->current_view == VIEW_PLAYLIST) {
  2029. endwin();
  2030. delete_windows(playlist_windows, 1);
  2031. playlist_windows[0] = NULL;
  2032. app_state->current_view = VIEW_INFO;
  2033. info_windows =
  2034. create_windows(NUM_PANELS, bottom_space, WINDOW_INFO);
  2035. for (int i = 0; i < NUM_PANELS; i++) {
  2036. print_window_data(app_state, i, info_windows);
  2037. }
  2038. app_state->windows[WINDOW_INFO] = info_windows;
  2039. }
  2040. break;
  2041. case playlist_view:
  2042. if (app_state->current_view == VIEW_INFO) {
  2043. endwin();
  2044. delete_windows(info_windows, NUM_PANELS);
  2045. if (playlist_windows[0] == NULL) {
  2046. playlist_windows =
  2047. create_windows(1, bottom_space, WINDOW_PLAYLIST);
  2048. } else {
  2049. playlist_windows =
  2050. create_windows(1, bottom_space, WINDOW_PLAYLIST);
  2051. }
  2052. app_state->current_view = VIEW_PLAYLIST;
  2053. print_playlist_data(app_state, playlist_windows);
  2054. app_state->windows[WINDOW_PLAYLIST] = playlist_windows;
  2055. refresh();
  2056. }
  2057. break;
  2058. case search:
  2059. search_idx(app_state);
  2060. case resize:
  2061. endwin();
  2062. refresh();
  2063. if (app_state->current_view == VIEW_INFO) {
  2064. delete_windows(info_windows, NUM_PANELS);
  2065. info_windows =
  2066. create_windows(NUM_PANELS, bottom_space, WINDOW_INFO);
  2067. for (int i = 0; i < NUM_PANELS; i++) {
  2068. print_window_data(app_state, i, info_windows);
  2069. }
  2070. app_state->windows[WINDOW_INFO] = info_windows;
  2071. }
  2072. if (app_state->current_view == VIEW_PLAYLIST) {
  2073. delete_windows(playlist_windows, 1);
  2074. playlist_windows =
  2075. create_windows(1, bottom_space, WINDOW_PLAYLIST);
  2076. print_playlist_data(app_state, playlist_windows);
  2077. app_state->windows[WINDOW_PLAYLIST] = playlist_windows;
  2078. }
  2079. break;
  2080. case quit:
  2081. if (app_state->playlist->status == PLAYING) {
  2082. kill(get_pid(app_state->program), SIGTERM);
  2083. }
  2084. endwin();
  2085. for (int i = 0; i < 3; i++) {
  2086. delwin(info_windows[i]);
  2087. }
  2088. cleanup(app_state);
  2089. exit(0);
  2090. break;
  2091. case chord:
  2092. {
  2093. const int c = getch();
  2094. const int chord_action = get_chord(c);
  2095. handle_action(chord_action, app_state);
  2096. break;
  2097. }
  2098. default:
  2099. break;
  2100. }
  2101. }
  2102. int main(void)
  2103. {
  2104. setlocale(LC_ALL, "");
  2105. setup_ncurses();
  2106. WINDOW **playlist_windows =
  2107. create_windows(1, bottom_space, WINDOW_PLAYLIST);
  2108. WINDOW **info_windows =
  2109. create_windows(NUM_PANELS, bottom_space, WINDOW_INFO);
  2110. WINDOW **playback_windows =
  2111. create_windows(1, bottom_space, WINDOW_PLAYBACK);
  2112. Database db = init_db();
  2113. Playlist playlist = init_playlist();
  2114. AppState app_state = init_appstate();
  2115. app_state.playlist = &playlist;
  2116. get_artists(app_state.connection, &db);
  2117. Artist *artist = &(db.artists[app_state.selected_artist_idx]);
  2118. app_state.artist = artist;
  2119. get_albums(app_state.connection, &db, artist->id);
  2120. Album *album = &(artist->albums[app_state.selected_album_idx]);
  2121. app_state.album = album;
  2122. get_songs(app_state.connection, &db, artist->id, album->id);
  2123. app_state.db = &db;
  2124. app_state.windows[WINDOW_INFO] = info_windows;
  2125. app_state.windows[WINDOW_PLAYLIST] = playlist_windows;
  2126. app_state.windows[WINDOW_PLAYBACK] = playback_windows;
  2127. print_window_data(&app_state, PANEL_ARTISTS, info_windows);
  2128. print_window_data(&app_state, PANEL_ALBUMS, info_windows);
  2129. print_window_data(&app_state, PANEL_SONGS, info_windows);
  2130. time_t now;
  2131. while (1) {
  2132. int action = -1;
  2133. int c = -1;
  2134. switch (playlist.status) {
  2135. case PLAYING:
  2136. now = (time_t) time(NULL);
  2137. playlist.play_time += difftime(now, playlist.start_time);
  2138. // Check if current song has finished playing
  2139. if (playlist.current_playing < playlist.size
  2140. && playlist.play_time >=
  2141. playlist.songs[playlist.current_playing]->duration) {
  2142. update_playlist_state(&app_state);
  2143. }
  2144. playlist.start_time = now;
  2145. print_progress_bar(playback_windows, &app_state);
  2146. break;
  2147. case PAUSED:
  2148. break;
  2149. case STOPPED:
  2150. print_progress_bar(playback_windows, &app_state);
  2151. break;
  2152. default:
  2153. break;
  2154. }
  2155. c = getch();
  2156. action = get_action(c);
  2157. handle_action(action, &app_state);
  2158. }
  2159. return 0;
  2160. }