/** \brief parse option from configuration file. * * \param[in,out] as Pointer to session handle * \param[in] opt name of option to set (left of =) * \param[in] param name of value for option (right of =) * \param type type for param, currently unused * \return 0 if parsing was successful, -1 if an error occured. currently * always returns 0 */ PRIVATE int set_option(auto_handle *as, const char *opt, const char *param, option_type type) { int32_t numval; dbg_printf(P_INFO2, "%s=%s (type: %d)", opt, param, type); assert(as != NULL); if(!strcmp(opt, "url")) { getFeeds(&as->feeds, param); } else if(!strcmp(opt, "feed")) { parseFeed(&as->feeds, param); } else if(!strcmp(opt, "download-folder")) { set_path(param, &as->download_folder); } else if(!strcmp(opt, "statefile")) { set_path(param, &as->statefile); } else if(!strcmp(opt, "interval")) { numval = parseUInt(param); if(numval > 0) { as->check_interval = numval; } else if(numval != -1) { dbg_printf(P_ERROR, "Interval must be 1 minute or more, reverting to default (%dmin)\n\t%s=%s", AM_DEFAULT_INTERVAL, opt, param); } else { dbg_printf(P_ERROR, "Unknown parameter: %s=%s", opt, param); } } else if(!strcmp(opt, "patterns")) { addPatterns_old(&as->filters, param); } else if(!strcmp(opt, "filter")) { parseFilter(&as->filters, param); } else if(!strcmp(opt, "prowl-apikey")) { as->prowl_key = am_strdup(param); } else if(!strcmp(opt, "download-done-script")) { as->download_done_script = am_strdup(param); } else { dbg_printf(P_ERROR, "Unknown option: %s", opt); } return 0; }
auto_handle* session_init(void) { char path[MAXPATHLEN]; char *home; auto_handle *ses = am_malloc(sizeof(auto_handle)); /* numbers */ ses->max_bucket_items = AM_DEFAULT_MAXBUCKET; ses->bucket_changed = 0; ses->check_interval = AM_DEFAULT_INTERVAL; /* strings */ ses->download_folder = get_temp_folder(); home = get_home_folder(); sprintf(path, "%s/%s", home, AM_DEFAULT_STATEFILE); am_free(home); ses->statefile = am_strdup(path); ses->prowl_key = NULL; ses->prowl_key_valid = 0; ses->download_done_script = NULL; ses->match_only = 0; /* lists */ ses->filters = NULL; ses->feeds = NULL; ses->downloads = NULL; return ses; }
/** \brief add new item to bucket list * * \param[in] guid Unique identifier for a bucket item (e.g. a URL) * \param[in,out] head pointer to the bucket list * \param[in] maxBucketItems number of maximum items in bucket list * \return always returns 0 * * The size of the provided bucket list is kept to maxBucketItems. * If it gets larger than the specified value, the oldest element is removed from the list. */ int addToBucket(const char* guid, NODE **head, const int maxBucketItems) { addToHead(am_strdup(guid), head); if(maxBucketItems > 0 && listCount(*head) > (uint32_t)maxBucketItems) { dbg_printf(P_INFO2, "[add_to_bucket] bucket gets too large, deleting last item...\n"); removeLast(*head, NULL); } return 0; }
PRIVATE void callDownloadDoneScript(const char* scriptname, const char* filename) { if (scriptname && *scriptname && filename && *filename) { int i; char * cmd[] = { am_strdup(scriptname), am_strdup(filename), NULL }; signal(SIGCHLD, onSigCHLD); if (!fork ()) { if (execvp (scriptname, cmd) == -1) dbg_printf(P_ERROR, "error executing script \"%s\": %d", cmd[0], errno); _exit (0); } for (i=0; cmd[i]; ++i) am_free (cmd[i]); } }
PRIVATE char* shorten(const char *str) { int tmp_pos; char c; char *retStr; char *tmp = (char*)am_malloc(MAX_PARAM_LEN+1); uint32_t line_pos = 0, i; uint32_t len = strlen(str); if(!tmp) { dbg_printf(P_ERROR, "[shorten] calloc(MAX_PARAM_LEN) failed!"); return NULL; } memset(tmp, 0, MAX_PARAM_LEN+1); while (isspace(str[line_pos])) { ++line_pos; } tmp_pos = 0; while(line_pos < len) { /* case 1: quoted strings */ if(tmp_pos != 0) { for(i = 0; i < strlen(AM_DELIMITER); ++i) tmp[tmp_pos++] = AM_DELIMITER[i]; } if (str[line_pos] == '"' || str[line_pos] == '\'') { c = str[line_pos]; ++line_pos; /* skip quote */ while(str[line_pos] != c && line_pos < len && str[line_pos] != '\n' && str[line_pos] != '\0') { tmp[tmp_pos++] = str[line_pos++]; } if(str[line_pos] == c) { line_pos++; /* skip the closing quote */ } } else { while(line_pos < len && str[line_pos] != '\n' && str[line_pos] != '\0') { tmp[tmp_pos++] = str[line_pos++]; } } while (isspace(str[line_pos])) { ++line_pos; } } tmp[tmp_pos] = '\0'; assert(strlen(tmp) < MAX_PARAM_LEN); retStr = am_strdup(tmp); am_free(tmp); return retStr; }
PRIVATE void set_path(const char *src, char **dst) { char *tmp; if(src && strlen(src) < MAXPATHLEN) { tmp = resolve_path(src); if(tmp) { if ( *dst != NULL ) { am_free(*dst); } *dst = am_strdup(tmp); am_free(tmp); } } }
auto_handle* session_init(void) { char path[MAXPATHLEN]; char *home; am_session_t *ses = am_malloc(sizeof(am_session_t)); /* numbers */ ses->rpc_version = AM_DEFAULT_RPC_VERSION; ses->max_bucket_items = AM_DEFAULT_MAXBUCKET; ses->bucket_changed = 0; ses->check_interval = AM_DEFAULT_INTERVAL; ses->use_transmission = AM_DEFAULT_USETRANSMISSION; ses->start_torrent = AM_DEFAULT_STARTTORRENTS; ses->transmission_version = AM_TRANSMISSION_1_3; ses->rpc_port = AM_DEFAULT_RPCPORT; /* strings */ ses->transmission_path = get_tr_folder(); ses->torrent_folder = get_temp_folder(); ses->host = NULL; ses->auth = NULL; home = get_home_folder(); sprintf(path, "%s/%s", home, AM_DEFAULT_STATEFILE); am_free(home); ses->statefile = am_strdup(path); ses->prowl_key = NULL; ses->toasty_key = NULL; ses->pushalot_key = NULL; ses->pushover_key = NULL; ses->prowl_key_valid = 0; ses->match_only = 0; ses->transmission_external = NULL; /* lists */ ses->filters = NULL; ses->feeds = NULL; ses->downloads = NULL; ses->upspeed = -1; return ses; }
/** \brief Create a new WebData object * * \param[in] url URL for a WebData object * * The parameter \a url is optional. You may provide \c NULL if no URL is required or not known yet. */ PRIVATE struct WebData* WebData_new(const char *url) { WebData *data = NULL; data = am_malloc(sizeof(WebData)); if(!data) return NULL; data->url = NULL; data->content_filename = NULL; data->content_length = -1; data->response = NULL; if(url) { data->url = am_strdup((char*)url); } data->response = HTTPData_new(); if(!data->response) { WebData_free(data); return NULL; } return data; }
PRIVATE int parseSubOption(char* line, char **option, char **param) { const char *subopt_delim = "=>"; uint32_t i = 0; *option = NULL; *param = NULL; assert(line && *line); while(line[i] != '\0') { if(line[i] == subopt_delim[0] && line[i+1] == subopt_delim[1]) { *option = am_strndup(line, i-1); *param = am_strdup(line + i + strlen(subopt_delim)); break; } i++; } if(*option && *param) return 0; else return -1; }
int main(int argc, char **argv) { auto_handle *session = NULL; char *config_file = NULL; char *logfile = NULL; char *xmlfile = NULL; char erbuf[100]; NODE *current = NULL; uint32_t count = 0; uint8_t first_run = 1; uint8_t once = 0; uint8_t verbose = AM_DEFAULT_VERBOSE; uint8_t append_log = 0; uint8_t match_only = 0; /* this sets the log level to the default before anything else is done. ** This way, if any outputting happens in readargs(), it'll be printed ** to stderr. */ log_init(NULL, verbose, 0); readargs(argc, argv, &config_file, &logfile, &xmlfile, &nofork, &verbose, &once, &append_log, &match_only); /* reinitialize the logging with the values from the command line */ log_init(logfile, verbose, append_log); if(!config_file) { config_file = am_strdup(AM_DEFAULT_CONFIGFILE); } strncpy(AutoConfigFile, config_file, strlen(config_file)); session = session_init(); session->match_only = match_only; if(parse_config_file(session, AutoConfigFile) != 0) { if(errno == ENOENT) { snprintf(erbuf, sizeof(erbuf), "Cannot find file '%s'", config_file); } else { snprintf(erbuf, sizeof(erbuf), "Unknown error"); } fprintf(stderr, "Error parsing config file: %s\n", erbuf); shutdown_daemon(session); } setup_signals(); if(!nofork) { /* start daemon */ if(daemonize() != 0) { dbg_printf(P_ERROR, "Error: Daemonize failed. Aborting..."); shutdown_daemon(session); } dbg_printft( P_MSG, "Daemon started"); } filter_printList(session->filters); dbg_printf(P_MSG, "Trailermatic version: %s", LONG_VERSION_STRING); dbg_printf(P_INFO, "verbose level: %d", verbose); dbg_printf(P_INFO, "foreground mode: %s", nofork == 1 ? "yes" : "no"); dbg_printf(P_INFO, "config file: %s", AutoConfigFile); dbg_printf(P_INFO, "check interval: %d min", session->check_interval); dbg_printf(P_INFO, "download folder: %s", session->download_folder); dbg_printf(P_INFO, "state file: %s", session->statefile); dbg_printf(P_MSG, "%d feed URLs", listCount(session->feeds)); dbg_printf(P_MSG, "Read %d filters from config file", listCount(session->filters)); if(session->prowl_key) { dbg_printf(P_INFO, "Prowl API key: %s", session->prowl_key); } if(listCount(session->feeds) == 0) { dbg_printf(P_ERROR, "No feed URL specified in trailermatic.conf!\n"); shutdown_daemon(session); } if(listCount(session->filters) == 0) { dbg_printf(P_ERROR, "No filters specified in trailermatic.conf!\n"); shutdown_daemon(session); } /* check if Prowl API key is given, and if it is valid */ if(session->prowl_key && verifyProwlAPIKey(session->prowl_key) ) { session->prowl_key_valid = 1; } load_state(session->statefile, &session->downloads); while(!closing) { dbg_printft( P_INFO, "------ Checking for new trailers ------"); if(xmlfile && *xmlfile) { processFile(session, xmlfile); once = 1; } else { current = session->feeds; count = 0; while(current && current->data) { ++count; dbg_printf(P_INFO2, "Checking feed %d ...", count); processFeed(session, current->data, first_run); current = current->next; } if(first_run) { dbg_printf(P_INFO2, "New bucket size: %d", session->max_bucket_items); } first_run = 0; } /* leave loop when program is only supposed to run once */ if(once) { break; } sleep(session->check_interval * 60); } shutdown_daemon(session); return 0; }
/** \brief parse option from configuration file. * * \param[in,out] as Pointer to session handle * \param[in] opt name of option to set (left of =) * \param[in] param name of value for option (right of =) * \param type type for param, currently unused * \return 0 if parsing was successful, -1 if an error occured. currently * always returns 0 */ PRIVATE int set_option(auto_handle *as, const char *opt, const char *param, option_type type) { int32_t numval; int32_t result = SUCCESS; dbg_printf(P_INFO2, "[config] %s=%s (type: %d)", opt, param, type); assert(as != NULL); if(!strcmp(opt, "url")) { dbg_printf(P_ERROR, "the 'url' option is not supported any more, please use the 'feed' option instead!"); result = FAILURE; } else if(!strcmp(opt, "feed")) { result = parseFeed(&as->feeds, param); } else if(!strcmp(opt, "transmission-home")) { set_path(param, &as->transmission_path); } else if(!strcmp(opt, "prowl-apikey")) { as->prowl_key = am_strdup(param); } else if(!strcmp(opt, "toasty-deviceid")) { as->toasty_key = am_strdup(param); } else if(!strcmp(opt, "pushalot-token")) { as->pushalot_key = am_strdup(param); } else if(!strcmp(opt, "transmission-version")) { if (!strcmp(param, "external")) { /* we should probably only set this when transmission-external is set */ as->transmission_version = AM_TRANSMISSION_EXTERNAL; } else if(param[0] == '1' && param[1] == '.' && param[2] == '2') { as->transmission_version = AM_TRANSMISSION_1_2; } else if(param[0] == '1' && param[1] == '.' && param[2] == '3') { as->transmission_version = AM_TRANSMISSION_1_3; } else { dbg_printf(P_ERROR, "Unknown parameter: %s=%s", opt, param); } } else if (!strcmp(opt, "transmission-external")) { set_path(param, &as->transmission_external); as->transmission_version = AM_TRANSMISSION_EXTERNAL; } else if(!strcmp(opt, "torrent-folder")) { set_path(param, &as->torrent_folder); } else if(!strcmp(opt, "statefile")) { set_path(param, &as->statefile); } else if(!strcmp(opt, "rpc-host")) { as->host = am_strdup(param); } else if(!strcmp(opt, "rpc-auth")) { as->auth = am_strdup(param); } else if(!strcmp(opt, "upload-limit")) { numval = parseUInt(param); if(numval > 0) { as->upspeed = (uint16_t)numval; } else { dbg_printf(P_ERROR, "Unknown parameter: %s=%s", opt, param); } } else if(!strcmp(opt, "rpc-port")) { numval = parseUInt(param); if (numval > 1024 && numval < 65535) { as->rpc_port = numval; } else if(numval != -1) { dbg_printf(P_ERROR, "RPC port must be an integer between 1025 and 65535, reverting to default (%d)\n\t%s=%s", AM_DEFAULT_RPCPORT, opt, param); } else { dbg_printf(P_ERROR, "Unknown parameter: %s=%s", opt, param); } } else if(!strcmp(opt, "interval")) { numval = parseUInt(param); if(numval > 0) { as->check_interval = numval; } else if(numval != -1) { dbg_printf(P_ERROR, "Interval must be 1 minute or more, reverting to default (%dmin)\n\t%s=%s", AM_DEFAULT_INTERVAL, opt, param); } else { dbg_printf(P_ERROR, "Unknown parameter: %s=%s", opt, param); } } else if(!strcmp(opt, "use-transmission")) { if(!strncmp(param, "0", 1) || !strncmp(param, "no", 2)) { as->use_transmission = 0; } else if(!strncmp(param, "1", 1) || !strncmp(param, "yes", 3)) { as->use_transmission = 1; } else { dbg_printf(P_ERROR, "Unknown parameter: %s=%s", opt, param); } } else if(!strcmp(opt, "start-torrents")) { if(!strncmp(param, "0", 1) || !strncmp(param, "no", 2)) { as->start_torrent = 0; } else if(!strncmp(param, "1", 1) || !strncmp(param, "yes", 3)) { as->start_torrent = 1; } else { dbg_printf(P_ERROR, "Unknown parameter for option '%s': '%s'", opt, param); } } else if(!strcmp(opt, "patterns")) { dbg_printf(P_ERROR, "the 'patterns' option is not supported any more, please use the 'filter' option instead!"); result = FAILURE; } else if(!strcmp(opt, "filter")) { result = parseFilter(&as->filters, param); } else { dbg_printf(P_ERROR, "Unknown option: %s", opt); } return result; }
int main(int argc, char **argv) { auto_handle * ses = NULL; char *config_file = NULL; char *logfile = NULL; char *pidfile = NULL; char *xmlfile = NULL; char erbuf[100]; NODE *current = NULL; uint32_t count = 0; uint8_t first_run = 1; uint8_t once = 0; uint8_t verbose = AM_DEFAULT_VERBOSE; uint8_t append_log = 0; uint8_t match_only = 0; /* this sets the log level to the default before anything else is done. ** This way, if any outputting happens in readargs(), it'll be printed ** to stderr. */ log_init(NULL, verbose, 0); readargs(argc, argv, &config_file, &logfile, &pidfile, &xmlfile, &nofork, &verbose, &once, &append_log, &match_only); /* reinitialize the logging with the values from the command line */ log_init(logfile, verbose, append_log); dbg_printf(P_MSG, "Automatic version: %s", LONG_VERSION_STRING); if(!config_file) { config_file = am_strdup(AM_DEFAULT_CONFIGFILE); } strncpy(AutoConfigFile, config_file, strlen(config_file)); ses = session_init(); ses->match_only = match_only; if(parse_config_file(ses, AutoConfigFile) != 0) { if(errno == ENOENT) { snprintf(erbuf, sizeof(erbuf), "Cannot find file '%s'", config_file); } else { snprintf(erbuf, sizeof(erbuf), "Unknown error"); } fprintf(stderr, "Error parsing config file: %s\n", erbuf); shutdown_daemon(ses); } if(!setupSession(ses)) { shutdown_daemon(ses); } setup_signals(); if(!nofork) { /* start daemon */ if(daemonize(pidfile) != 0) { dbg_printf(P_ERROR, "Error: Daemonize failed. Aborting..."); shutdown_daemon(mySession); } dbg_printft( P_MSG, "Daemon started"); } dbg_printf(P_INFO, "verbose level: %d", verbose); dbg_printf(P_INFO, "foreground mode: %s", nofork == true ? "yes" : "no"); while(!closing) { isRunning = true; dbg_printft( P_INFO, "------ Checking for new episodes ------"); if(xmlfile && *xmlfile) { processFile(mySession, xmlfile); once = 1; } else { current = mySession->feeds; count = 0; while(current && current->data) { ++count; processFeed(mySession, current->data, first_run); current = current->next; } if(first_run) { dbg_printf(P_INFO2, "New bucket size: %d", mySession->max_bucket_items); } first_run = 0; } /* leave loop when program is only supposed to run once */ if(once) { break; } isRunning = false; if(seenHUP) { signal_handler(SIGHUP); } sleep(mySession->check_interval * 60); } shutdown_daemon(mySession); return 0; }
/** \brief Upload data to a specified URL. * * \param url Path to where data shall be uploaded * \param auth (Optional) authentication information in the form of "user:password" * \param data Data that shall be uploaded * \param data_size size of the data * \return Web server response */ PUBLIC HTTPResponse* sendHTTPData(const char *url, const char* auth, const void *data, uint32_t data_size) { CURL *curl_handle = NULL; CURLcode res; long rc, tries = 2, len; WebData* response_data = NULL; HTTPResponse* resp = NULL; char sessionKey[MAXLEN]; struct curl_slist * headers = NULL; if( !url || !data ) { return NULL; } response_data = WebData_new(url); do { --tries; WebData_clear(response_data); if( curl_handle == NULL) { if(gbGlobalInitDone == FALSE) { curl_global_init(CURL_GLOBAL_ALL); gbGlobalInitDone = TRUE; } if( ( curl_handle = am_curl_init(auth, TRUE) ) ) { curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, response_data); curl_easy_setopt(curl_handle, CURLOPT_WRITEHEADER, response_data); //Transmission-specific options for HTTP POST if(strstr(response_data->url, "transmission") != NULL) { curl_easy_setopt(curl_handle, CURLOPT_HEADERFUNCTION, parse_Transmission_response ); headers = curl_slist_append(headers, "Content-Type: application/json"); if( gSessionID ) { if((len = snprintf(sessionKey, MAXLEN, "X-Transmission-Session-Id: %s", gSessionID)) > 0) { sessionKey[len] = '\0'; } headers = curl_slist_append(headers, sessionKey); } curl_easy_setopt( curl_handle, CURLOPT_HTTPHEADER, headers ); } curl_easy_setopt(curl_handle, CURLOPT_URL, response_data->url); } else { dbg_printf(P_ERROR, "am_curl_init() failed"); break; } } curl_easy_setopt(curl_handle, CURLOPT_POSTFIELDS, data); curl_easy_setopt(curl_handle, CURLOPT_POSTFIELDSIZE, data_size); if( ( res = curl_easy_perform(curl_handle) ) ) { dbg_printf(P_ERROR, "Upload to '%s' failed: %s", url, curl_easy_strerror(res)); break; } else { curl_easy_getinfo(curl_handle, CURLINFO_RESPONSE_CODE, &rc); dbg_printf(P_INFO2, "response code: %ld", rc); if(rc == 409) { if(gSessionID) { dbg_printf(P_DBG, "Error code 409, session ID: %s", gSessionID); } else { dbg_printf(P_ERROR, "Error code 409, no session ID"); } closeCURLSession( curl_handle ); curl_slist_free_all( headers ); headers = NULL; curl_handle = NULL; } else { resp = HTTPResponse_new(); curl_easy_getinfo(curl_handle, CURLINFO_RESPONSE_CODE, &resp->responseCode); //copy data if present if(response_data->response->data) { resp->size = response_data->response->buffer_pos; resp->data = am_strndup(response_data->response->data, resp->size); } //copy filename if present if(response_data->content_filename) { resp->content_filename = am_strdup(response_data->content_filename); } break; } } } while(tries > 0); /* cleanup */ closeCURLSession(curl_handle); if(headers) { curl_slist_free_all(headers); } WebData_free(response_data); return resp; }
PUBLIC HTTPResponse* getHTTPData(const char *url, const char *cookies, CURL ** curl_session) { CURLcode res; CURL *curl_handle = NULL; CURL *session = *curl_session; char *escaped_url = NULL; WebData *data = NULL; HTTPResponse *resp = NULL; long responseCode = -1; if(!url) { return NULL; } data = WebData_new(url); if(!data) { return NULL; } dbg_printf(P_INFO2, "[getHTTPData] url=%s, curl_session=%p", url, (void*)session); if(session == NULL) { if(gbGlobalInitDone == FALSE) { curl_global_init(CURL_GLOBAL_ALL); gbGlobalInitDone = TRUE; } session = am_curl_init(NULL, FALSE); *curl_session = session; } curl_handle = session; if(curl_handle) { escaped_url = url_encode_whitespace(url); assert(escaped_url); curl_easy_setopt(curl_handle, CURLOPT_URL, escaped_url); curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, data); curl_easy_setopt(curl_handle, CURLOPT_WRITEHEADER, data); if(cookies && *cookies) { /* if there's an explicit cookie string, use it */ curl_easy_setopt(curl_handle, CURLOPT_COOKIE, cookies); } else { /* otherwise, enable cookie-handling since there might be cookies defined within the URL */ curl_easy_setopt(curl_handle, CURLOPT_COOKIEFILE, ""); } res = curl_easy_perform(curl_handle); /* curl_easy_cleanup(curl_handle); */ curl_easy_getinfo(curl_handle, CURLINFO_RESPONSE_CODE, &responseCode); dbg_printf(P_INFO2, "[getHTTPData] response code: %d", responseCode); if(res != 0) { dbg_printf(P_ERROR, "[getHTTPData] '%s': %s (retval: %d)", url, curl_easy_strerror(res), res); } else { /* Only the very first connection attempt (where curl_session == NULL) should store the session, ** and only the last one should close the session. */ resp = HTTPResponse_new(); resp->responseCode = responseCode; //copy data if present if(data->response->data) { resp->size = data->response->buffer_pos; resp->data = am_strndup(data->response->data, resp->size); } //copy filename if present if(data->content_filename) { resp->content_filename = am_strdup(data->content_filename); } } am_free(escaped_url); } else { dbg_printf(P_ERROR, "curl_handle is uninitialized!"); resp = NULL; } WebData_free(data); return resp; }