├── sample.png ├── main.c ├── Makefile ├── .gitignore ├── config ├── net.h ├── nyaa.h ├── curses.h ├── LICENSE ├── README.md ├── core.h ├── net.c ├── core.c ├── nyaa.c └── curses.c /sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/z411/ncnyaa/HEAD/sample.png -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2016 z411, see LICENSE for details */ 2 | 3 | #include "core.h" 4 | #include "curses.h" 5 | 6 | int 7 | main() 8 | { 9 | if(load_config("config")) { 10 | init_curses(); 11 | cleanup(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SOURCES=core.c curses.c net.c nyaa.c main.c 2 | CFLAGS=`xml2-config --cflags` `curl-config --cflags` 3 | LDFLAGS=-lxml2 -lcurl -lncursesw 4 | 5 | OBJECTS=$(SOURCES:.c=.o) 6 | OUT=ncnyaa 7 | 8 | $(OUT): $(OBJECTS) 9 | $(CC) -o $@ $^ $(LDFLAGS) 10 | 11 | clean: 12 | rm *.o $(OUT) 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Object files 2 | *.o 3 | *.ko 4 | *.obj 5 | *.elf 6 | 7 | # Precompiled Headers 8 | *.gch 9 | *.pch 10 | 11 | # Libraries 12 | *.lib 13 | *.a 14 | *.la 15 | *.lo 16 | 17 | # Shared objects (inc. Windows DLLs) 18 | *.dll 19 | *.so 20 | *.so.* 21 | *.dylib 22 | 23 | # Executables 24 | *.exe 25 | *.out 26 | *.app 27 | *.i*86 28 | *.x86_64 29 | *.hex 30 | 31 | # Debug files 32 | *.dSYM/ 33 | *.su 34 | -------------------------------------------------------------------------------- /config: -------------------------------------------------------------------------------- 1 | # ncnyaa config file 2 | # 3 | # You can move this file to $XDG_CONFIG_HOME/ncnyaa/config 4 | # or $HOME/.config/ncnyaa/config 5 | 6 | # Download torrent file when selected (yes/no) 7 | download=no 8 | 9 | # Torrent download path 10 | #path=download 11 | 12 | # Run following command after selecting a torrent 13 | # $TITLE = torrent title 14 | # $URL = remote url 15 | # $FILE = local torrent (if any) 16 | #cmd=xdg-open $FILE 17 | cmd=transmission-remote -a $URL 18 | 19 | # Default category number 20 | category=3 21 | -------------------------------------------------------------------------------- /net.h: -------------------------------------------------------------------------------- 1 | #ifndef NET_H 2 | #define NET_H 3 | #include 4 | #include 5 | 6 | struct MemoryChunk 7 | { 8 | char * memory; 9 | size_t size; 10 | }; 11 | 12 | struct MemoryChunk * download_to_mem(const char * url); 13 | int download_to_file(const char * url, const char * out_filename); 14 | 15 | char * url_encode(const char * str); 16 | extern size_t mem_write_callback(void *contents, size_t size, size_t nmemb, void *userp); 17 | 18 | xmlDocPtr parse_rss(struct MemoryChunk * mem); 19 | 20 | #endif 21 | -------------------------------------------------------------------------------- /nyaa.h: -------------------------------------------------------------------------------- 1 | #ifndef NYAA_H 2 | #define NYAA_H 3 | #include "core.h" 4 | #include 5 | 6 | struct Category 7 | { 8 | char * id; 9 | int sukebe; 10 | char * name; 11 | }; 12 | 13 | struct Sort 14 | { 15 | int id; 16 | int order; 17 | char * name; 18 | }; 19 | 20 | extern struct Category nyaa_categories[]; 21 | extern int nyaa_categories_size; 22 | 23 | int parse_nyaa(struct TorrentList * torrent_list, struct Search * search); 24 | struct TorrentItem * parse_nyaa_item(xmlNodePtr node_ptr); 25 | 26 | int download_nyaa(struct TorrentItem * item); 27 | 28 | #endif 29 | -------------------------------------------------------------------------------- /curses.h: -------------------------------------------------------------------------------- 1 | #ifndef CURSES_H 2 | #define CURSES_H 3 | #define _XOPEN_SOURCE 4 | #define _XOPEN_SOURCE_EXTENDED 5 | #include 6 | #include 7 | 8 | int init_curses(); 9 | void key_event(int ch); 10 | 11 | struct TorrentItem * get_selected(); 12 | 13 | void write_item(int i); 14 | void write_page(); 15 | void set_status(const char * msg); 16 | void cycle_sort(); 17 | void cycle_order(); 18 | void ask_categories(); 19 | void ask_search(); 20 | void perform_search(); 21 | void download_selected(); 22 | void move_choice(int add); 23 | 24 | void refresh_windows(); 25 | void rewrite_title(); 26 | void rewrite_help(int all); 27 | void handle_resize(); 28 | void make_error(); 29 | 30 | int wcols(const wchar_t * str, int width); 31 | void wfillx(WINDOW * win, int size); 32 | 33 | #endif 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 z411 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ncnyaa 2 | ncnyaa is a ncurses nyaa browser and downloader for your terminal. 3 | It can search in different categories, download torrents and/or run a shell command 4 | for a downloaded item. 5 | 6 | ![Sample](/sample.png?raw=true "ncnyaa") 7 | 8 | # Requirements 9 | * ncurses (libncursesw5-dev) 10 | * libxml2 (libxml2-dev) 11 | * libcurl (libcurl-dev) 12 | 13 | # Compiling 14 | After you've satisfied the requirements, just simply clone this repo 15 | and run the Makefile: 16 | 17 | cd ncnyaa 18 | make 19 | 20 | # Using 21 | **Please check the configuration file (`config`) for customization.** 22 | 23 | Then run the program in a terminal: 24 | 25 | ./ncnyaa 26 | 27 | | Keybind | Action | 28 | | --- | --- | 29 | | j/k/Up/Down | Move up or down | 30 | | u/b/PgUp/PgDn | Jump up or down | 31 | | c | Change search category | 32 | | o | Change sort type (Date, Size, Seeders, etc.) | 33 | | O | Change sort order (Ascending/Descending) | 34 | | s | Perform search | 35 | | . | Perform last search | 36 | | d/Enter | Download and/or run command (depending on config) | 37 | | q | Quit | 38 | 39 | # Authors/License 40 | Copyright (c) 2016 z411 41 | Licensed under the MIT license; see LICENSE file for details. 42 | 43 | -------------------------------------------------------------------------------- /core.h: -------------------------------------------------------------------------------- 1 | #ifndef CORE_H 2 | #define CORE_H 3 | #define _VERSION "0.1" 4 | 5 | #include 6 | 7 | struct Config 8 | { 9 | int download; 10 | char * path; 11 | char * cmd; 12 | int category; 13 | }; 14 | 15 | enum TorrentType 16 | { 17 | TYPE_UNKNOWN, 18 | TYPE_NORMAL, 19 | TYPE_TRUSTED, 20 | TYPE_REMAKE, 21 | TYPE_A 22 | }; 23 | 24 | enum SortType 25 | { 26 | SORT_NONE, 27 | SORT_DATE, 28 | SORT_SEEDERS, 29 | SORT_LEECHERS, 30 | SORT_DOWNLOADS, 31 | SORT_SIZE, 32 | SORT_NAME, 33 | SORT_ALL 34 | }; 35 | 36 | enum SortOrder 37 | { 38 | ORDER_NONE, 39 | ORDER_DESC, 40 | ORDER_ASC 41 | }; 42 | 43 | struct Search 44 | { 45 | char term[30]; 46 | int category; 47 | enum SortType sort; 48 | enum SortOrder order; 49 | }; 50 | 51 | struct TorrentItem 52 | { 53 | short i; 54 | const char * title; 55 | wchar_t * title_w; 56 | const char * link; 57 | int seeders; 58 | int leechers; 59 | int downloads; 60 | double size; 61 | char size_u[5]; 62 | enum TorrentType type; 63 | }; 64 | 65 | struct TorrentList 66 | { 67 | size_t size; 68 | size_t capacity; 69 | struct TorrentItem * list[150]; 70 | }; 71 | 72 | extern struct Config config; 73 | 74 | void run_command(const char * cmd, struct TorrentItem * item); 75 | void apply_config_opt(char * key, char * val); 76 | int load_config(const char * name); 77 | int parse_config(const char * path); 78 | void cleanup(); 79 | 80 | void strip(char * str); 81 | 82 | #endif 83 | -------------------------------------------------------------------------------- /net.c: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2016 z411, see LICENSE for details */ 2 | 3 | #include "core.h" 4 | #include "net.h" 5 | 6 | #include 7 | #include 8 | 9 | struct MemoryChunk 10 | *download_to_mem(const char * url) 11 | { 12 | CURL *curl; 13 | CURLcode res; 14 | 15 | struct MemoryChunk * mem = malloc(sizeof(struct MemoryChunk)); 16 | mem->memory = NULL; 17 | mem->size = 0; 18 | 19 | curl = curl_easy_init(); 20 | if (curl) { 21 | curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); 22 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, mem_write_callback); 23 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)mem); 24 | curl_easy_setopt(curl, CURLOPT_URL, url); 25 | 26 | res = curl_easy_perform(curl); 27 | 28 | curl_easy_cleanup(curl); 29 | 30 | if(res != CURLE_OK) 31 | return NULL; 32 | } 33 | 34 | return mem; 35 | } 36 | 37 | int 38 | download_to_file(const char * url, const char * out_filename) 39 | { 40 | CURL *curl; 41 | CURLcode res; 42 | 43 | FILE *fp; 44 | 45 | curl = curl_easy_init(); 46 | if (curl) 47 | { 48 | if ((fp = fopen(out_filename, "wb"))) { 49 | curl_easy_setopt(curl, CURLOPT_URL, url); 50 | curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); 51 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, NULL); 52 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); 53 | 54 | res = curl_easy_perform(curl); 55 | 56 | fclose(fp); 57 | } 58 | 59 | curl_easy_cleanup(curl); 60 | 61 | if(res != CURLE_OK) 62 | return 0; 63 | } else { 64 | return 0; 65 | } 66 | 67 | return 1; 68 | } 69 | 70 | char 71 | *url_encode(const char * str) 72 | { 73 | return curl_escape(str, strlen(str)); 74 | } 75 | 76 | extern size_t 77 | mem_write_callback(void *contents, size_t size, size_t nmemb, void *userp) 78 | { 79 | /* 80 | Callback function for libcurl, which saves data into a 81 | MemoryChunk struct. 82 | */ 83 | 84 | size_t realsize = size * nmemb; 85 | struct MemoryChunk *mem = (struct MemoryChunk *)userp; 86 | 87 | mem->memory = realloc(mem->memory, mem->size + realsize + 1); 88 | if (mem->memory == NULL) { 89 | printf("not enough memory (realloc returned NULL)\n"); 90 | return 0; 91 | } 92 | 93 | memcpy(&(mem->memory[mem->size]), contents, realsize); 94 | mem->size += realsize; 95 | mem->memory[mem->size] = 0; 96 | 97 | return realsize; 98 | } 99 | 100 | xmlDocPtr 101 | parse_rss(struct MemoryChunk * mem) 102 | { 103 | /* Use the MemoryChunk and get it into a xmlDoc using libxml */ 104 | xmlDocPtr doc; 105 | 106 | if (mem->memory) { 107 | doc = xmlReadMemory(mem->memory, (int)mem->size, "noname", NULL, XML_PARSE_NOBLANKS); 108 | } else { 109 | return NULL; 110 | } 111 | 112 | return doc; 113 | } 114 | 115 | -------------------------------------------------------------------------------- /core.c: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2016 z411, see LICENSE for details */ 2 | 3 | #include "core.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #define MAXLINE 100 11 | 12 | struct Config config = { 1, NULL, NULL, 0 }; 13 | 14 | void 15 | run_command(const char * cmd, struct TorrentItem * item) 16 | { 17 | setenv("TITLE", item->title, 1); 18 | setenv("URL", item->link, 1); 19 | system(config.cmd); 20 | } 21 | 22 | void 23 | apply_config_opt(char * key, char * val) 24 | { 25 | if (!strcmp(key, "download")) { 26 | config.download = (!strcmp(val, "yes")) ? 1 : 0; 27 | } else if (!strcmp(key, "category")) { 28 | config.category = strtol(val, NULL, 10); 29 | } else if (!strcmp(key, "cmd")) { 30 | if (config.cmd == NULL) { 31 | config.cmd = malloc((strlen(val)+1)*sizeof(*config.cmd)); 32 | if (config.cmd != NULL) strcpy(config.cmd, val); 33 | } else 34 | printf("config: Duplicate option, ignoring: %s\n", key); 35 | } else if (!strcmp(key, "path")) { 36 | if (config.cmd == NULL) { 37 | config.path = malloc((strlen(val)+1)*sizeof(*config.path)); 38 | if (config.path != NULL) strcpy(config.path, val); 39 | } else 40 | printf("config: Duplicate option, ignoring: %s\n", key); 41 | } else { 42 | printf("config: Invalid option, ignoring: %s\n", key); 43 | } 44 | } 45 | 46 | int 47 | load_config(const char *name) 48 | { 49 | /* Try current directory */ 50 | if(access(name, R_OK) == 0) 51 | return parse_config(name); 52 | 53 | char *env; 54 | char *path; 55 | 56 | /* Try XDG */ 57 | env = getenv("XDG_CONFIG_HOME"); 58 | if(env) { 59 | size_t sz = strlen(env) + 1 + strlen("ncnyaa") + 1 + strlen(name) + 1; 60 | path = malloc(sz); 61 | 62 | if(path && snprintf(path, sz, "%s/ncnyaa/%s", env, name) && access(path, R_OK) == 0) 63 | return parse_config(path); 64 | } 65 | 66 | /* Try HOME */ 67 | env = getenv("HOME"); 68 | if(env) { 69 | size_t sz = strlen(env) + 1 + strlen(".config/ncnyaa") + 1 + strlen(name) + 1; 70 | path = malloc(sz); 71 | 72 | if(path && snprintf(path, sz, "%s/.config/ncnyaa/%s", env, name) && access(path, R_OK) == 0) 73 | return parse_config(path); 74 | } 75 | 76 | printf("No configuration file found.\n"); 77 | return 0; 78 | } 79 | 80 | int 81 | parse_config(const char * filename) 82 | { 83 | printf("reading %s\n", filename); 84 | 85 | FILE *fp; 86 | char line[MAXLINE]; 87 | 88 | if ((fp = fopen(filename, "r")) != NULL) { 89 | char * key; 90 | char * val; 91 | 92 | while (fgets(line, MAXLINE, fp)) { 93 | /* Ignore comment */ 94 | if(line[0] == '#') continue; 95 | 96 | /* Remove endline */ 97 | int ln = strlen(line) - 1; 98 | if(line[ln] == '\n') 99 | line[ln--] = 0; 100 | 101 | /* Remove CR */ 102 | if(line[ln] == '\r') 103 | line[ln--] = 0; 104 | 105 | /* Ignore empty lines */ 106 | if(ln<0) continue; 107 | 108 | /* Parse config line */ 109 | key = strtok(line, "="); 110 | val = strtok(NULL, "="); 111 | if (val != NULL) { 112 | strip(key); 113 | strip(val); 114 | apply_config_opt(key, val); 115 | } else { 116 | printf("config: Invalid line, ignoring: %s\n", line); 117 | } 118 | } 119 | 120 | fclose(fp); 121 | return 1; 122 | } else { 123 | perror("Error reading configuration file"); 124 | return 0; 125 | } 126 | } 127 | 128 | void 129 | cleanup() 130 | { 131 | free(config.cmd); 132 | free(config.path); 133 | } 134 | 135 | void 136 | strip(char * str) 137 | { 138 | if (*str) { 139 | char *p, *q; 140 | for (p = q = str; *p == ' '; p++); 141 | while (*p) *q++ = *p++; 142 | for (; *(q-1) == ' '; q--); 143 | *q = '\0'; 144 | } 145 | } 146 | 147 | -------------------------------------------------------------------------------- /nyaa.c: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2016 z411, see LICENSE for details */ 2 | 3 | #include "nyaa.h" 4 | 5 | #include "net.h" 6 | #include 7 | 8 | int nyaa_categories_size = 34; 9 | struct Category nyaa_categories[] = { 10 | { "0_0", 0, "All categories" }, 11 | { "1_0", 0, "Anime" }, 12 | { "1_1", 0, "Anime - AMV" }, 13 | { "1_2", 0, "Anime - English" }, 14 | { "1_3", 0, "Anime - Non-English" }, 15 | { "1_4", 0, "Anime - Raw" }, 16 | { "2_0", 0, "Audio" }, 17 | { "2_1", 0, "Audio - Lossless" }, 18 | { "2_2", 0, "Audio - Lossy" }, 19 | { "3_0", 0, "Literature" }, 20 | { "3_1", 0, "Literature - English" }, 21 | { "3_2", 0, "Literature - Non-English" }, 22 | { "3_3", 0, "Literature - Raw" }, 23 | { "4_0", 0, "Live" }, 24 | { "4_1", 0, "Live - English" }, 25 | { "4_2", 0, "Live - PVs" }, 26 | { "4_3", 0, "Live - Non-English" }, 27 | { "4_4", 0, "Live - Raw" }, 28 | { "5_0", 0, "Pictures" }, 29 | { "5_1", 0, "Pictures - Graphics" }, 30 | { "5_2", 0, "Pictures - Photos" }, 31 | { "6_0", 0, "Software" }, 32 | { "6_1", 0, "Software - Applications" }, 33 | { "6_2", 0, "Software - Games" }, 34 | { "0_0", 1, "All categories" }, 35 | { "1_0", 1, "Art" }, 36 | { "1_1", 1, "Art - Anime" }, 37 | { "1_2", 1, "Art - Doujinshi" }, 38 | { "1_3", 1, "Art - Games" }, 39 | { "1_4", 1, "Art - Manga" }, 40 | { "1_5", 1, "Art - Pictures" }, 41 | { "2_0", 1, "Real Life" }, 42 | { "2_1", 1, "Real Life - Photobooks" }, 43 | { "2_2", 1, "Real Life - Videos" } 44 | }; 45 | 46 | char nyaa_sorts[][10] = {"none", "id", "seeders", "leechers", "downloads", "size", "", ""}; 47 | char nyaa_orders[][5] = {"none", "desc", "asc"}; 48 | 49 | int 50 | parse_nyaa(struct TorrentList * torrent_list, struct Search * search) 51 | { 52 | /* Get a Nyaa.se RSS and fill the torrent_list with the available torrents */ 53 | char url[200]; 54 | 55 | if (search->category >= 0 && search->category < nyaa_categories_size) 56 | snprintf(url, sizeof(url), 57 | "https://%s.nyaa.si/?page=rss&c=%s&s=%s&o=%s&q=%s", 58 | (nyaa_categories[search->category].sukebe) ? "sukebei" : "www", 59 | nyaa_categories[search->category].id, 60 | nyaa_sorts[search->sort], 61 | nyaa_orders[search->order], 62 | url_encode(search->term)); 63 | else 64 | snprintf(url, sizeof(url), 65 | "https://www.nyaa.si/?page=rss&q=%s", 66 | url_encode(search->term)); 67 | 68 | struct MemoryChunk * mem = download_to_mem(url); 69 | if (mem == NULL) 70 | return 0; 71 | 72 | xmlDocPtr doc = parse_rss(mem); 73 | 74 | free(mem->memory); 75 | free(mem); 76 | 77 | //xmlDocPtr doc = xmlReadFile("test.rss", NULL, 0); 78 | 79 | xmlNodePtr cur = xmlDocGetRootElement(doc); 80 | if (cur == NULL) 81 | return 0; 82 | 83 | /* Only proceed if it's a valid RSS (root element is rss) */ 84 | if (xmlStrEqual(cur->name, (const xmlChar *)"rss")) { 85 | cur = cur->children->children; 86 | 87 | /* Fill the torrent_list with pointers to Nyaa items */ 88 | int i; 89 | for (i = 0; i < 150 && cur != NULL; cur = cur->next) { 90 | if (xmlStrEqual(cur->name, (const xmlChar *)"item")) { 91 | torrent_list->list[i++] = parse_nyaa_item(cur); 92 | } 93 | } 94 | torrent_list->size = i; 95 | } else { 96 | return 0; 97 | } 98 | 99 | xmlFreeDoc(doc); 100 | 101 | return 1; 102 | } 103 | 104 | struct TorrentItem 105 | *parse_nyaa_item(xmlNodePtr node_ptr) 106 | { 107 | /* Parses a single RSS item containing a Nyaa torrent */ 108 | xmlNodePtr cur; 109 | struct TorrentItem * item = malloc(sizeof(struct TorrentItem)); 110 | 111 | size_t size; 112 | for (cur = node_ptr->children; cur; cur = cur->next) { 113 | if (xmlStrEqual(cur->name, (const xmlChar *)"title")) { 114 | item->title = (const char*)xmlNodeGetContent(cur); 115 | size = mbstowcs(NULL, item->title, strlen(item->title)) + 1; 116 | 117 | item->title_w = malloc(size * sizeof(wchar_t)); 118 | mbstowcs(item->title_w, item->title, size); 119 | } else if (xmlStrEqual(cur->name, (const xmlChar *)"link")) { 120 | item->link = (const char*)xmlNodeGetContent(cur); 121 | } else if (xmlStrEqual(cur->name, (const xmlChar *)"seeders")) { 122 | sscanf((char*)xmlNodeGetContent(cur), "%d", &item->seeders); 123 | } else if (xmlStrEqual(cur->name, (const xmlChar *)"leechers")) { 124 | sscanf((char*)xmlNodeGetContent(cur), "%d", &item->leechers); 125 | } else if (xmlStrEqual(cur->name, (const xmlChar *)"downloads")) { 126 | sscanf((char*)xmlNodeGetContent(cur), "%d", &item->downloads); 127 | } else if (xmlStrEqual(cur->name, (const xmlChar *)"size")) { 128 | sscanf((char*)xmlNodeGetContent(cur), "%lf %s", &item->size, item->size_u); 129 | } 130 | } 131 | 132 | return item; 133 | } 134 | 135 | int 136 | download_nyaa(struct TorrentItem * item) 137 | { 138 | char fname[256]; 139 | 140 | if (config.path != NULL && config.path[0] != '\0') 141 | snprintf(fname, sizeof(fname), "%s/%s.torrent", config.path, item->title); 142 | else 143 | snprintf(fname, sizeof(fname), "%s.torrent", item->title); 144 | 145 | int res = download_to_file(item->link, fname); 146 | setenv("FILE", fname, 1); 147 | 148 | return res; 149 | } 150 | -------------------------------------------------------------------------------- /curses.c: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2016 z411, see LICENSE for details */ 2 | 3 | #include "curses.h" 4 | 5 | #include 6 | #include 7 | 8 | #include "core.h" 9 | #include "nyaa.h" 10 | 11 | #define LIST_PAGE_SIZE (LINES - 4) 12 | #define COL_SIZE 10 13 | #define COL_SEEDERS 4 14 | #define COL_LEECHERS 4 15 | #define COL_DOWNLOADS 6 16 | #define COL_TITLE (COLS-COL_SIZE-COL_SEEDERS-COL_LEECHERS-COL_DOWNLOADS) 17 | #define MIN_COLS 54 18 | #define MIN_LINES 10 19 | 20 | static WINDOW *win_title; 21 | static WINDOW *win_list; 22 | static WINDOW *win_help; 23 | static WINDOW *win_status; 24 | 25 | static struct TorrentList torrent_list; 26 | static int list_offset = 0; 27 | static int choice = 0; 28 | 29 | static struct Search search = { "", 0, SORT_DATE, ORDER_DESC }; 30 | 31 | int 32 | init_curses() 33 | { 34 | setlocale(LC_ALL, ""); 35 | torrent_list.size = 0; 36 | 37 | signal(SIGWINCH, handle_resize); 38 | 39 | initscr(); 40 | 41 | if (COLS < MIN_COLS || LINES < MIN_LINES) { 42 | printf("Terminal too small.\n"); 43 | endwin(); 44 | return -1; 45 | } 46 | 47 | /* Set default category if defined in config file */ 48 | if (config.category >= 0 && config.category <= nyaa_categories_size) 49 | search.category = config.category; 50 | 51 | cbreak(); 52 | noecho(); 53 | curs_set(0); 54 | 55 | start_color(); 56 | use_default_colors(); 57 | 58 | init_pair(1, COLOR_YELLOW, COLOR_BLUE); 59 | init_pair(2, COLOR_GREEN, -1); // Trusted torrent 60 | init_pair(3, COLOR_YELLOW, -1); // Remake torrent 61 | init_pair(4, COLOR_CYAN, -1); // A+ torrent 62 | init_pair(5, COLOR_RED, -1); // sukebei category 63 | 64 | refresh(); 65 | 66 | win_title = newwin(1, COLS, 0, 0); 67 | win_list = newwin(LINES - 3, COLS, 1, 0); 68 | win_help = newwin(1, COLS, LINES-2, 0); 69 | win_status = newwin(1, COLS, LINES-1, 0); 70 | wbkgd(win_title, COLOR_PAIR(1)); 71 | wbkgd(win_help, COLOR_PAIR(1)); 72 | refresh_windows(); 73 | 74 | /* Event loop */ 75 | int ch; 76 | keypad(win_list, TRUE); 77 | while((ch = wgetch(win_list)) != 'q') 78 | key_event(ch); 79 | 80 | /* Free memory and quit */ 81 | delwin(win_title); 82 | delwin(win_list); 83 | delwin(win_status); 84 | endwin(); 85 | 86 | return 0; 87 | } 88 | 89 | void 90 | key_event(int ch) 91 | { 92 | switch(ch) 93 | { 94 | case KEY_UP: 95 | case 'k': 96 | move_choice(-1); break; 97 | case KEY_DOWN: 98 | case 'j': 99 | move_choice(1); break; 100 | case KEY_PPAGE: 101 | case 'u': 102 | move_choice(-10); break; 103 | case KEY_NPAGE: 104 | case 'b': 105 | move_choice(10); break; 106 | case 'o': 107 | cycle_sort(); break; 108 | case 'O': 109 | cycle_order(); break; 110 | case 'c': 111 | ask_categories(); break; 112 | case 's': 113 | ask_search(); perform_search(); break; 114 | case '.': 115 | perform_search(); break; 116 | case 'd': 117 | case 10: 118 | download_selected(); break; 119 | } 120 | } 121 | 122 | struct TorrentItem 123 | *get_selected() 124 | { 125 | return torrent_list.list[list_offset+choice]; 126 | } 127 | 128 | void 129 | write_item(int i) 130 | { 131 | if(list_offset+i >= torrent_list.size) { 132 | mvwaddstr(win_list, i+1, 0, "None"); 133 | return; 134 | } 135 | 136 | struct TorrentItem * item = torrent_list.list[list_offset+i]; 137 | 138 | if (item->type == TYPE_TRUSTED) wattron(win_list, COLOR_PAIR(2)); 139 | else if (item->type == TYPE_REMAKE) wattron(win_list, COLOR_PAIR(3)); 140 | else if (item->type == TYPE_A) wattron(win_list, COLOR_PAIR(4)); 141 | 142 | wmove(win_list, i+1, 0); 143 | 144 | waddnwstr(win_list, item->title_w, wcols(item->title_w, COL_TITLE - 1)); 145 | wfillx(win_list, COL_TITLE); 146 | 147 | wprintw(win_list, "%.1lf %s", item->size, item->size_u); 148 | wfillx(win_list, COL_TITLE+COL_SIZE); 149 | 150 | wprintw(win_list, "%d", item->seeders); 151 | wfillx(win_list, COL_TITLE+COL_SIZE+COL_SEEDERS); 152 | 153 | wprintw(win_list, "%d", item->leechers); 154 | wfillx(win_list, COL_TITLE+COL_SIZE+COL_SEEDERS+COL_LEECHERS); 155 | 156 | wprintw(win_list, "%d", item->downloads); 157 | wfillx(win_list, COLS-1); 158 | 159 | if (item->type == TYPE_TRUSTED) wattroff(win_list, COLOR_PAIR(2)); 160 | else if (item->type == TYPE_REMAKE) wattroff(win_list, COLOR_PAIR(3)); 161 | else if (item->type == TYPE_A) wattroff(win_list, COLOR_PAIR(3)); 162 | } 163 | 164 | void 165 | write_page() 166 | { 167 | werase(win_list); 168 | 169 | wattron(win_list, WA_BOLD); 170 | mvwprintw(win_list, 0, 0, "Title"); 171 | mvwprintw(win_list, 0, COL_TITLE, "Size"); 172 | mvwprintw(win_list, 0, COL_TITLE + COL_SIZE, "SE"); 173 | mvwprintw(win_list, 0, COL_TITLE + COL_SIZE + COL_SEEDERS, "LE"); 174 | mvwprintw(win_list, 0, COL_TITLE + COL_SIZE + COL_SEEDERS + COL_LEECHERS, "DLs"); 175 | wattroff(win_list, WA_BOLD); 176 | 177 | if(torrent_list.size) { 178 | int i; 179 | for(i = 0; i < LIST_PAGE_SIZE && list_offset+i < torrent_list.size; i++) 180 | { 181 | if(i == choice) 182 | wattron(win_list, WA_STANDOUT); 183 | else 184 | wattroff(win_list, WA_STANDOUT); 185 | 186 | write_item(i); 187 | } 188 | } 189 | wattroff(win_list, WA_STANDOUT); 190 | wrefresh(win_list); 191 | 192 | rewrite_title(); 193 | } 194 | 195 | void 196 | set_status(const char * msg) 197 | { 198 | werase(win_status); 199 | mvwaddstr(win_status, 0, 0, msg); 200 | wrefresh(win_status); 201 | } 202 | 203 | void 204 | ask_categories() 205 | { 206 | werase(win_list); 207 | 208 | wattron(win_list, WA_BOLD); 209 | mvwprintw(win_list, 0, 0, "Categories"); 210 | wattroff(win_list, WA_BOLD); 211 | 212 | int i; 213 | int col = 0; 214 | int y = 1; 215 | 216 | for (i = 0; i < nyaa_categories_size; i++) { 217 | wmove(win_list, y++, col*28); 218 | if (nyaa_categories[i].sukebe) { 219 | wattron(win_list, COLOR_PAIR(5)); 220 | wprintw(win_list, "%d (s) %s\n", i, nyaa_categories[i].name); 221 | wattroff(win_list, COLOR_PAIR(5)); 222 | } else { 223 | wprintw(win_list, "%d %s\n", i, nyaa_categories[i].name); 224 | } 225 | 226 | if (getcury(win_list) >= getmaxy(win_list)-1) { 227 | col++; 228 | y = 1; 229 | } 230 | } 231 | 232 | wrefresh(win_list); 233 | 234 | set_status("Category number: "); 235 | 236 | char input[4]; 237 | int i_input; 238 | int res; 239 | 240 | curs_set(1); echo(); 241 | wgetnstr(win_status, input, sizeof(input)-1); 242 | res = sscanf(input, "%d", &i_input); 243 | curs_set(0); noecho(); 244 | 245 | write_page(); 246 | 247 | if (res != EOF && res && i_input >= 0 && i_input < nyaa_categories_size) { 248 | search.category = i_input; 249 | set_status("Searching in new category."); 250 | } else { 251 | set_status("Invalid category."); 252 | } 253 | rewrite_help(0); 254 | wrefresh(win_status); 255 | } 256 | 257 | void 258 | cycle_sort() 259 | { 260 | if (!search.sort || search.sort >= SORT_ALL-1) 261 | search.sort = 1; 262 | else 263 | search.sort++; 264 | 265 | rewrite_help(0); 266 | } 267 | 268 | void 269 | cycle_order() 270 | { 271 | search.order = 272 | (search.order == ORDER_ASC) 273 | ? ORDER_DESC : ORDER_ASC; 274 | rewrite_help(0); 275 | } 276 | 277 | void 278 | ask_search() 279 | { 280 | set_status("Search: "); 281 | 282 | curs_set(1); echo(); 283 | wgetnstr(win_status, search.term, sizeof(search.term)-1); 284 | curs_set(0); noecho(); 285 | } 286 | 287 | void 288 | perform_search() 289 | { 290 | set_status("Getting torrent list..."); 291 | 292 | torrent_list.size = 0; 293 | write_page(); 294 | 295 | if(parse_nyaa(&torrent_list, &search)) { 296 | list_offset = 0; 297 | choice = 0; 298 | 299 | write_page(); 300 | set_status("Ready."); 301 | } 302 | else 303 | set_status("Error with search."); 304 | } 305 | 306 | void 307 | download_selected() 308 | { 309 | if(!torrent_list.size) return; 310 | 311 | if (config.download) { 312 | set_status("Downloading..."); 313 | download_nyaa(get_selected()); 314 | } 315 | 316 | if (config.cmd != NULL) { 317 | set_status("Running command..."); 318 | endwin(); 319 | run_command(config.cmd, get_selected()); 320 | refresh(); 321 | } 322 | 323 | set_status("Ready."); 324 | } 325 | 326 | void 327 | move_choice(int add) 328 | { 329 | if(!torrent_list.size) return; 330 | 331 | write_item(choice); 332 | 333 | choice += add; 334 | 335 | if(list_offset+choice <= 0) 336 | choice = 0; 337 | else if(list_offset+choice >= torrent_list.size) 338 | choice = torrent_list.size - list_offset - 1; 339 | 340 | if(choice < 0) { 341 | /* Go to previous page */ 342 | list_offset -= LIST_PAGE_SIZE; 343 | if(list_offset < 0) list_offset = 0; 344 | choice = LIST_PAGE_SIZE+choice; 345 | write_page(); 346 | } else if(choice > LIST_PAGE_SIZE-1) { 347 | /* Go to next page */ 348 | list_offset += LIST_PAGE_SIZE; 349 | choice -= LIST_PAGE_SIZE; 350 | write_page(); 351 | } 352 | 353 | wattron(win_list, WA_STANDOUT); 354 | write_item(choice); 355 | wattroff(win_list, WA_STANDOUT); 356 | 357 | wrefresh(win_list); 358 | } 359 | 360 | void 361 | rewrite_title() 362 | { 363 | werase(win_title); 364 | if (torrent_list.size) 365 | mvwprintw(win_title, 0, 0, "ncnyaa v%s - %s (%d/%d)", 366 | _VERSION, 367 | search.term, 368 | list_offset / LIST_PAGE_SIZE + 1, 369 | torrent_list.size / LIST_PAGE_SIZE + 1); 370 | else 371 | mvwprintw(win_title, 0, 0, "ncnyaa v%s", _VERSION); 372 | wrefresh(win_title); 373 | } 374 | 375 | void 376 | rewrite_help(int all) 377 | { 378 | if (all) { 379 | werase(win_help); 380 | wmove(win_help, 0, 0); 381 | waddstr(win_help, "c:Cat s:Search d:DL+Run o/O:Sort q:Quit"); 382 | } 383 | 384 | wmove(win_help, 0, COLS - 12); 385 | 386 | if(search.category < 10) waddch(win_help, ' '); 387 | wprintw(win_help, "%d/", search.category); 388 | switch (search.sort) { 389 | case SORT_SEEDERS: 390 | waddstr(win_help, "SE "); break; 391 | case SORT_LEECHERS: 392 | waddstr(win_help, "LE "); break; 393 | case SORT_DOWNLOADS: 394 | waddstr(win_help, "DLs "); break; 395 | case SORT_SIZE: 396 | waddstr(win_help, "Size"); break; 397 | case SORT_NAME: 398 | waddstr(win_help, "Name"); break; 399 | case SORT_DATE: 400 | default: 401 | waddstr(win_help, "Date"); break; 402 | } 403 | waddch(win_help, '/'); 404 | waddstr(win_help, (search.order == ORDER_ASC) ? "Asc " : "Desc"); 405 | 406 | wrefresh(win_help); 407 | } 408 | 409 | void 410 | refresh_windows() 411 | { 412 | write_page(); 413 | rewrite_help(1); 414 | set_status("Ready."); 415 | } 416 | 417 | void 418 | handle_resize() 419 | { 420 | endwin(); 421 | refresh(); 422 | clear(); 423 | 424 | if (COLS >= MIN_COLS && LINES >= MIN_LINES) { 425 | mvwin(win_help, LINES-2, 0); 426 | mvwin(win_status, LINES-1, 0); 427 | 428 | wresize(win_title, 1, COLS); 429 | wresize(win_list, LINES-3, COLS); 430 | wresize(win_help, 1, COLS); 431 | wresize(win_status, 1, COLS); 432 | 433 | refresh_windows(); 434 | } else { 435 | make_error("Terminal too small."); 436 | } 437 | } 438 | 439 | void 440 | make_error(char * str) 441 | { 442 | mvaddstr(0, 0, str); 443 | refresh(); 444 | } 445 | 446 | int 447 | wcols(const wchar_t * str, int width) 448 | { 449 | size_t i; 450 | int remained_len = width; 451 | 452 | for (i = 0; str[i] != L'\0'; i++) 453 | { 454 | int cols = wcwidth(str[i]); 455 | remained_len -= (cols > 1) ? cols : 1; 456 | 457 | if (remained_len < 0) 458 | return i; 459 | } 460 | return width; 461 | } 462 | 463 | void 464 | wfillx(WINDOW * win, int size) 465 | { 466 | while(getcurx(win) < size) 467 | waddch(win, ' '); 468 | } 469 | --------------------------------------------------------------------------------