/* * update.c: TVM2VDR plugin for the Video Disk Recorder * * See the README file for copyright information and how to reach the author. * */ #include #include #include #include #include "update.h" //*************************************************************************** // TOOLS //*************************************************************************** int isEmpty(const char* str) { return !str || !*str; } int isLink(const char* path) { struct stat sb; if (lstat(path, &sb) == 0) return S_ISLNK(sb.st_mode); tell(0, "TVM2VDR: Error: Detecting state for '%s' failed, error was '%m'", path); return false; } int fileExists(const char* path) { return access(path, F_OK) == 0; } int createLink(const char* link, const char* dest, int force) { if (!fileExists(link) || force) { // may be the link exists and point to a wrong or already deleted destination ... // .. therefore we delete the link at first unlink(link); if (symlink(dest, link) != 0) { tell(0, "TVM2VDR: Failed to create symlink '%s', error was '%m'", link); return fail; } } return success; } //*************************************************************************** // Remove File //*************************************************************************** int removeFile(const char* filename) { if (unlink(filename) != 0) { tell(0, "TVM2VDR: Can't remove file '%s', '%m'", filename); return 1; } tell(2, "TVM2VDR: Removed file '%s'", filename); return 0; } //*************************************************************************** // Create MD5 Hash (unused) //*************************************************************************** const char* createMd5(cDbRow* row, char* md5) { int md5Fields[] = { cEventFields::fiTableId, cEventFields::fiVersion, cEventFields::fiTitle, cEventFields::fiShortText, cEventFields::fiLongDescription, cEventFields::fiStartTime, cEventFields::fiDuration, cEventFields::fiParentalRating, cEventFields::fiVps, 0 }; MD5_CTX c; unsigned char out[MD5_DIGEST_LENGTH]; char tmp[100]; MD5_Init(&c); for (int i = 0; md5Fields[i]; i++) { if (row->getField(md5Fields[i])->format == cDbService::ffAscii) MD5_Update(&c, row->getStrValue(md5Fields[i]), strlen(row->getStrValue(md5Fields[i]))); else { sprintf(tmp, "%ld", row->getIntValue(md5Fields[i])); MD5_Update(&c, tmp, strlen(tmp)); } } MD5_Final(out, &c); for (int n = 0; n < MD5_DIGEST_LENGTH; n++) sprintf(md5+2*n, "%02x", out[n]); md5[sizeMd5String] = 0; return md5; } //*************************************************************************** // //*************************************************************************** cUpdate::cUpdate() { char* dbpath; char* pdir; char* lang; // thread / update control loopActive = no; performUpdate = no; fullupdate = no; fullreload = no; nextAutoUpdateAt = 0; // xmlSubstituteEntitiesDefault(1); xmlLoadExtDtdDefaultValue = 1; pxsltStylesheet = 0; loglevel = 0; xmldir = 0; episodesdir = 0; epgimagedir = 0; withutf8 = no; exsltRegisterAll(); lang = setlocale(LC_CTYPE, 0); if (lang) { tell(0, "TVM2VDR: Set locale to '%s'", lang); if ((strcasestr(lang, "UTF-8") != 0) || (strcasestr(lang, "UTF8") != 0)) { tell(0, "TVM2VDR: detected UTF-8"); withutf8 = yes; } } else { tell(0, "TVM2VDR: Reseting locale for LC_CTYPE failed."); } // channelmap asprintf(&pdir, "%s/tvm2vdr/tvm2vdr_channelmap.conf", cPlugin::ConfigDirectory()); chanmap = new cChannelMap(pdir); free(pdir); #if defined (TVM2VDR_DATA_DIR) asprintf(&episodesdir, "%s/eplists", TVM2VDR_DATA_DIR); asprintf(&xmldir, "%s/epgxml", TVM2VDR_DATA_DIR); asprintf(&epgimagedir, "%s/epgimages", TVM2VDR_DATA_DIR); asprintf(&dbpath, "%s/%s", TVM2VDR_DATA_DIR, "tvm.db"); #else episodesdir = strdup(cPlugin::ConfigDirectory("tvm2vdr/eplists")); xmldir = strdup(cPlugin::ConfigDirectory("tvm2vdr/xml")); epgimagedir = strdup(cPlugin::ConfigDirectory("tvm2vdr/epgimages")); asprintf(&dbpath, "%s/%s", cPlugin::ConfigDirectory("tvm2vdr/"), "tvm.db"); #endif if (!(DirectoryOk(xmldir) || MakeDirs(xmldir, true))) tell(0, "TVM2VDR: could not access or create Directory %s", xmldir); // asprintf(&pdir, "%s/updates", xmldir); // if (!(DirectoryOk(pdir) || MakeDirs(pdir, true))) // tell(0, "TVM2VDR: could not access or create Directory %s", pdir); // free(pdir); if (TVM2VDRConfig.storeSeriesToFs && TVM2VDRConfig.seriesEnabled) if (!(DirectoryOk(episodesdir) || MakeDirs(episodesdir, true))) tell(0, "TVM2VDR: could not access or create Directory %s", episodesdir); asprintf(&pdir, "%s/images", epgimagedir); if (!(DirectoryOk(pdir) || MakeDirs(pdir, true))) tell(0, "TVM2VDR: could not access or create Directory %s", pdir); free(pdir); strcpy(imageExtension, "jpg"); loadXSLT(); // init database ... Table::setFileName(dbpath); Table::setConfPath(cPlugin::ConfigDirectory("tvm2vdr/")); // ISO not supportetd by sqlite ... Table::setEncoding(withutf8 ? "UTF-8": "ISO-8859-1"); asprintf(&pdir, "%s/lv.sqlext", cPlugin::ConfigDirectory("tvm2vdr")); if (fileExists(pdir)) Table::setExtensionFile(pdir); else tell(0, "Fatal: SQL extension '%s' not found!", pdir); free(pdir); // open tables .. fileDb = new Table("tvmfiles", cTvmFields::fields); if (fileDb->open() != success) tell(0, "TVM2VDR: Could not access sqlite database %s for table %s", dbpath, fileDb->TableName()); imageDb = new Table("images", cImageFields::fields, cImageFields::views); if (imageDb->open() != success) tell(0, "TVM2VDR: Could not access sqlite database %s for table %s", dbpath, imageDb->TableName()); episodeDb = new Table("episodes", cEpisodeFields::fields, cEpisodeFields::views); if (episodeDb->open() != success) tell(0, "TVM2VDR: Could not access sqlite database %s for table %s", dbpath, episodeDb->TableName()); epgDb = new Table("events", cEventFields::fields, cEventFields::views); epgDb->setHoldInMem(yes); if (epgDb->open() != success) tell(0, "TVM2VDR: Could not access sqlite database %s for table %s", dbpath, epgDb->TableName()); // hashes if (TVM2VDRConfig.loglevel > 1) tell(0, "TVM2VDR: Start reading hashes from db"); epgDb->clear(); epgDb->setValue(cEventFields::fiSource, "vdr"); for (int f = epgDb->find(cEventFields::viAllBySource); f; f = epgDb->fetch(cEventFields::viAllBySource)) { string evtKey = string(epgDb->getRow()->getIntValueAsStr(cEventFields::fiEventId)) + epgDb->getStrValue(cEventFields::fiChannelId); evtMemList[evtKey].version = epgDb->getIntValue(cEventFields::fiVersion); evtMemList[evtKey].tableid = epgDb->getIntValue(cEventFields::fiTableId); } epgDb->resetFetch(cEventFields::viAllBySource); if (TVM2VDRConfig.loglevel > 1) tell(0, "TVM2VDR: Finished reading hashes from db, got %d hashes", (int)evtMemList.size()); // init epg handler epgHandler = new cTvmEpgHandler(chanmap, &evtMemList); updateTvmMap(dbpath); } //*************************************************************************** // //*************************************************************************** cUpdate::~cUpdate() { if (loopActive) Stop(); free(xmldir); free(epgimagedir); free(episodesdir); if (pxsltStylesheet) xsltFreeStylesheet(pxsltStylesheet); xsltCleanupGlobals(); xmlCleanupParser(); delete epgDb; delete fileDb; delete imageDb; delete episodeDb; } //*************************************************************************** // Get Timer Of Event //*************************************************************************** cTimer* cUpdate::getTimerOf(const cEvent* event) const { for (cTimer* t = Timers.First(); t; t = Timers.Next(t)) if (t->Event() == event) return t; return 0; } //*************************************************************************** // Update Tvm Map Table //*************************************************************************** int cUpdate::updateTvmMap(const char* dbpath) { Table* mapDb; int count = 0; mapDb = new Table("tvmmap", cTvmMapFields::fields); if (mapDb->open() != success) { tell(0, "TVM2VDR: Could not access sqlite database %s for table %s", dbpath, mapDb->TableName()); return fail; } mapDb->truncate(); for (cChanMap::iterator iter = chanmap->chanmap.begin(); iter != chanmap->chanmap.end(); iter++) { int tvmid = iter->first; for (int index = 0; index < chanmap->GetChanCount(tvmid); index++) { mapDb->clear(); mapDb->setValue(cTvmMapFields::fiTvmId, tvmid); mapDb->setValue(cTvmMapFields::fiChannelName, chanmap->GetChanStr(tvmid, index)); mapDb->setValue(cTvmMapFields::fiSource, tvmid ? "tvm" : "vdr"); mapDb->store(); count++; } } tell(2, "TVM2VDR: Update TVM map done with %d entries", count); mapDb->close(); return success; } //*************************************************************************** // //*************************************************************************** void cUpdate::triggerUpdate(int full, int reload) { if (!loopActive) { tell(0, "TVM2VDR: Thread not running, try to recover ..."); Cancel(3); Start(); } fullupdate = full; fullreload = reload; performUpdate = true; waitCondition.Broadcast(); // wakeup the thread } //*************************************************************************** // Schedule Auto Update //*************************************************************************** void cUpdate::scheduleAutoUpdate(int wait) { if (wait) nextAutoUpdateAt = time(0) + wait; else if (TVM2VDRConfig.autoupdate && TVM2VDRConfig.updatetime) nextAutoUpdateAt = time(0) + TVM2VDRConfig.updatetime * 60 * 60; else nextAutoUpdateAt = 0; if (nextAutoUpdateAt && loglevel > 0) { if ((nextAutoUpdateAt-time(0))/60/60 < 1) tell(0, "TVM2VDR: Scheduled next auto update in %ld minute(s)", (nextAutoUpdateAt-time(0))/60); else tell(0, "TVM2VDR: Scheduled next auto update in %ld hour(s)", (nextAutoUpdateAt-time(0))/60/60); } } //*************************************************************************** // Stop Thread //*************************************************************************** void cUpdate::Stop() { loopActive = false; waitCondition.Broadcast(); // wakeup the thread Cancel(3); // wait up to 3 seconds for thread was stopping } //*************************************************************************** // //*************************************************************************** void cUpdate::Action(void) { tell(1, "TVM2VDR: Timer Thread started (pid=%d)", getpid()); mutex.Lock(); // mutex gets ONLY unlocked when sleeping loopActive = true; while (loopActive && Running()) { // calc wait time, we sleep at most 60 seconds or (if shorter) // to the next update time int wait = nextAutoUpdateAt - time(0); wait = wait > 0 && wait < 60 ? wait : 60; waitCondition.TimedWait(mutex, wait*1000); // wait time in ms performUpdate = performUpdate || (nextAutoUpdateAt && nextAutoUpdateAt <= time(0)); if (performUpdate) { update(); // update TVM epg data if (TVM2VDRConfig.seriesEnabled) { downloadEpisodes(); // download and store optionally on local fs evaluateEpisodes(); // try series match refreshEpg(); // update EPG } cSchedules::Cleanup(true); // force store of epg.data to filesystem if (TVM2VDRConfig.getepgimages) getPictures(); // TVM pictures cleanup(); // cleanup xml-files and image-files performUpdate = false; scheduleAutoUpdate(); // schedule next update } } tell(1, "TVM2VDR: Timer Thread ended (pid=%d)", getpid()); } //*************************************************************************** // //*************************************************************************** int cUpdate::reloadMaps() { return chanmap->ReloadChannelMap(); } //*************************************************************************** // //*************************************************************************** void cUpdate::loadXSLT() { char* xsltfile; if (pxsltStylesheet) xsltFreeStylesheet(pxsltStylesheet); asprintf(&xsltfile, "%s/tvm2vdr/tvm2vdr_tvmovie-%s.xsl", cPlugin::ConfigDirectory(), withutf8 ? "utf-8" : "iso-8859-1"); if ((pxsltStylesheet = xsltParseStylesheetFile((const xmlChar*)xsltfile)) == 0) tell(0, "TVM2VDR: can't load xsltfile %s", xsltfile); else tell(0, "TVM2VDR: stylesheet '%s' loaded", xsltfile); free(xsltfile); } //*************************************************************************** // Callbacks //*************************************************************************** static size_t WriteMemoryCallback(void* ptr, size_t size, size_t nmemb, void* data) { size_t realsize = size * nmemb; struct CurlMemoryStruct* mem = (struct CurlMemoryStruct*)data; if (mem->memory) mem->memory = (char*)realloc(mem->memory, mem->size + realsize + 1); else mem->memory = (char*)malloc(mem->size + realsize + 1); if (mem->memory) { memcpy (&(mem->memory[mem->size]), ptr, realsize); mem->size += realsize; mem->memory[mem->size] = 0; } return realsize; } static size_t WriteHeaderCallback(void* ptr, size_t size, size_t nmemb, void* data) { size_t realsize = size * nmemb; struct CurlMemoryStruct* mem = (struct CurlMemoryStruct*)data; char* p; // since TVM updates "ETag" an "Last-Modified:" daily without changing the contents // we have to use "Content-Length:" to check for updates :( const char* attribute = "Content-Length: "; if (mem->ignoreTag && ptr && (p = strcasestr((char*)ptr, attribute))) { // tag attribute found ... sprintf(mem->tag, "%s", p+strlen(attribute)); if ((p = strchr(mem->tag, '\n'))) *p = 0; if ((p = strchr(mem->tag, '\r'))) *p = 0; if ((p = strchr(mem->tag, '"'))) *p = 0; // tell(0, "checking header attribute '%s' => '%s' - '%s'", // attribute, mem->ignoreTag, mem->tag); if (*mem->tag) mem->match = (strcmp(mem->tag, mem->ignoreTag) == 0); // ignore download if tag matches ... if (mem->match) return 0; } return realsize; } //*************************************************************************** // Download File //*************************************************************************** int cUpdate::downloadFile(const char* url, int& size, const char* tag) { CURL* curl_handle; long code; int res = 0; // Initialize 'data' struct data.clear(); strcpy(data.ignoreTag, tag); // init curl if (curl_global_init(CURL_GLOBAL_ALL) != 0) { tell(0, "TVM2VDR: Error, something went wrong with curl_global_init()"); return fail; } curl_handle = curl_easy_init(); if (!curl_handle) { tell(0, "TVM2VDR: Error, unable to get handle from curl_easy_init()"); return fail; } if (TVM2VDRConfig.useproxy) { curl_easy_setopt(curl_handle, CURLOPT_PROXYTYPE, CURLPROXY_HTTP); curl_easy_setopt(curl_handle, CURLOPT_PROXY, TVM2VDRConfig.httpproxy); // Specify HTTP proxy } curl_easy_setopt(curl_handle, CURLOPT_URL, url); // Specify URL to get curl_easy_setopt(curl_handle, CURLOPT_FOLLOWLOCATION, 0); // don't follow redirects curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, WriteMemoryCallback); // Send all data to this function curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, (void*)&data); // Pass our 'data' struct to the callback function curl_easy_setopt(curl_handle, CURLOPT_HEADERFUNCTION, WriteHeaderCallback); // Send header to this function curl_easy_setopt(curl_handle, CURLOPT_WRITEHEADER, (void*)&data); // Pass some header details to this struct curl_easy_setopt(curl_handle, CURLOPT_MAXFILESIZE, 1024*1024); // Set maximum file size to get (bytes) curl_easy_setopt(curl_handle, CURLOPT_NOPROGRESS, 1); // No progress meter curl_easy_setopt(curl_handle, CURLOPT_NOSIGNAL, 1); // No signaling curl_easy_setopt(curl_handle, CURLOPT_TIMEOUT, 30); // Set timeout to 30 seconds curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, TVM2VDR_USERAGENT); // Some servers don't like requests // that are made without a user-agent field // perform http-get if ((res = curl_easy_perform(curl_handle)) != 0) { curl_easy_cleanup(curl_handle); // Cleanup curl stuff int match = data.match; data.clear(); if (res == CURLE_WRITE_ERROR && match) return ignore; tell(1, "TVM2VDR: Error, download of '%s' failed, result was %d", url, res); return fail; } curl_easy_getinfo(curl_handle, CURLINFO_HTTP_CODE, &code); if (code == 404) data.clear(); curl_easy_cleanup(curl_handle); // Cleanup curl stuff size = data.size; return success; } //*************************************************************************** // Un - Scramble //*************************************************************************** void cUpdate::unscramble() { unsigned int hunkSize = 1000; char buffer[hunkSize]; int pos = 0; struct Hunktab { int offset; int len; }; static Hunktab hunktab[] = { {0x006a, 6}, {0x0064, 6}, {0x0070, 88}, {0x0320, 17}, {0x0011, 83}, {0x012c,100}, {0x0000,17}, {0x0331,183}, {0x0258,200}, {0x0190,200}, {0x00c8,100} }; if (data.size < hunkSize) return ; for (unsigned int i = 0; i < sizeof(hunktab)/sizeof(hunktab[0]); i++) { memcpy(&buffer[pos], &data.memory[hunktab[i].offset], hunktab[i].len); pos += hunktab[i].len; } memcpy(data.memory, buffer, sizeof(buffer)); } //*************************************************************************** // Update // // - Datei von TVM holen (sofern aktualisiert, noch nicht geholt oder fullupdate) // Erkennung der Aktualisierung erfolgt über die 'tvmfiles' Tabelle // - Datei lokal ablegen // - XSLT Konvertierung durchführen // - Resultat je zugeordnetem Kanal in der 'events' // Tabelle speichern/ aktualisieren incl. der 'fileref' // - via 'eventsview' View die neuen/aktualisierten Events in // das EPG des VDR aktualisieren/einfügen/löschen // - Neue 'fileref' in die 'tvmfiles' Tabelle übernehmen // -> damit ist die Verarbeitung quittiert //*************************************************************************** int cUpdate::update(void) { int bytes = 0; int files = 0; int rejected = 0; int nonUpdates = 0; time_t clock = time(0); int res; tell(0, "TVM2VDR: EPG-DATA %s started", fullupdate ? "Full-Update" : fullreload ? "Reload" : "Update"); if (fullupdate || fullreload) { // delete all events from tvm channels .. dropVdrsEpg(); } // loop from today over configured range .. for (int day = 0; day < TVM2VDRConfig.days; day++) { struct tm tm; localtime_r(&clock, &tm); tell(1, "TVM2VDR: Updating day today+%d now", day); // loop over all channels of channel map for (cChanMap::iterator iter = chanmap->chanmap.begin(); iter != chanmap->chanmap.end(); iter++) { int isupdatefile = 0; if (iter->first <= 0) continue; // dont use update files since they are always identical :o ?!? // for (isupdatefile = 0; isupdatefile <= 1; isupdatefile++) // { FILE* fout; int fileSize = 0; char* url; char* filename; // the file to be downloaded char* outfilename; // the file with path to be stored localy asprintf(&filename, "%4d%02d%02d_%03d.xml", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, iter->first); asprintf(&outfilename, "%s/%s%s.gz", xmldir, isupdatefile ? "updates/" : "", filename); // lookup file information fileDb->setValue(cTvmFields::fiName, filename); int found = fileDb->find(); // URL - Aufbau // http://wwwa.tvmovie.de/static/tvghost/html/onlinedata/xml[aend-]-gz5/YYYYMMDD_CHA.xml.tvm asprintf(&url, "http://wwwa.tvmovie.de/static/tvghost/html/onlinedata/xml-%sgz5/%s.tvm", isupdatefile ? "aend-" : "", filename); if (fullreload && found) { char* fileRef; asprintf(&fileRef, "%s-%s", fileDb->getStrValue(cTvmFields::fiName), fileDb->getStrValue(cTvmFields::fiTag)); res = updateVdrEpg(iter->first, fileRef); // update VDRs EPG events (add/update/delete) free(fileRef); } else { if (day >= TVM2VDRConfig.upddays && found) { // don't check for update of existing files more than 'upddays' in the future tell(3, "TVM2VDR: Skipping update check of file '%s' for day %d", outfilename, day); fileSize = 0; res = success; } else res = downloadFile(url, fileSize, fullupdate ? "" : fileDb->getStrValue(cTvmFields::fiTag)); // force download on fullupdate! if (res == ignore) // non update { nonUpdates++; tell(2, "TVM2VDR: skipping download of '%s' due to non-update", url); } else if (res != success) // error { tell(1, "TVM2VDR: Error, download of '%s' failed", url); } else if (fileSize > 0) { bytes += fileSize; files++; tell(2, "TVM2VDR: Downloaded file '%s' with (%d) Bytes", url, fileSize); unscramble(); if ((fout = fopen(outfilename, "w+"))) { char* fileRef; fwrite(data.memory, sizeof(data.memory[0]), data.size, fout); fclose(fout); asprintf(&fileRef, "%s-%s", fileDb->getStrValue(cTvmFields::fiName), data.tag); if ((res = processTvmFile(iter->first, outfilename, fileRef)) == success) res = updateVdrEpg(iter->first, fileRef); // update VDRs EPG events (add/update/delete) else rejected++; if (res == success) { if (found) { // delete old entrys in events table char* where; const char* oldFileRef = fileDb->getStrValue(cTvmFields::fiFileRef); asprintf(&where, "source = 'tvm' and fileref = '%s'", oldFileRef); epgDb->deleteWhere(where); free(where); } // Confirm processing of TVM file fileDb->setValue(cTvmFields::fiTag, data.tag); fileDb->setValue(cTvmFields::fiFileRef, fileRef); fileDb->store(); } free(fileRef); } else { tell(1, "TVM2VDR: Can't open file '%s' for writing, error was '%s'", outfilename, strerror(errno)); } } } free(url); free(filename); free(outfilename); // } // Clean up data.clear(); } tm.tm_mday++; clock = mktime(&tm); } fullupdate = false; double mb = (double)bytes / 1024.0 / 1024.0; tell(0, "TVM2VDR: EPG-DATA Update finished, loaded %d files (%.3f %cB), %d non-updates " "skipped, %d rejected due to format error.", files, mb > 2 ? mb : (double)bytes/1024.0, mb > 2 ? 'M' : 'K', nonUpdates, rejected); return true; } //*************************************************************************** // //*************************************************************************** xmlDocPtr cUpdate::transformXml(const char* xmlfile, xsltStylesheetPtr pxsltStylesheet) { xmlDocPtr doc, transformedDoc = 0; if ((doc = xmlReadFile(xmlfile, "ISO-8859-1", 0))) { if ((transformedDoc = xsltApplyStylesheet(pxsltStylesheet, doc, 0)) == 0) { tell(1, "TVM2VDR: Error applying XSLT stylesheet"); } xmlFreeDoc(doc); } else { char* rejectName; tell(2, "TVM2VDR: Error parsing XML File '%s'", xmlfile); asprintf(&rejectName, "%s.rejected", xmlfile); rename(xmlfile, rejectName); tell(2, "TVM2VDR: Invalid file '%s' rejectetd to '%s'", xmlfile, rejectName); free(rejectName); } return transformedDoc; } //*************************************************************************** // //*************************************************************************** int cUpdate::processTvmFile(const int tvmid, const char* xmlfile, const char* fileRef) { xmlDocPtr transformedDoc; xmlNodePtr xmlRoot; if ((transformedDoc = transformXml(xmlfile, pxsltStylesheet)) == 0) { tell(0, "TVM2VDR: XSLT transformation for '%s' failed, ignoring", xmlfile); return fail; } if (!(xmlRoot = xmlDocGetRootElement(transformedDoc))) { tell(0, "TVM2VDR: Invalid xml document returned from xslt for '%s', ignoring", xmlfile); return fail; } // process 'all' events for 'all' configured channles of this tvmid for (int index = 0; index < chanmap->GetChanCount(tvmid); index++) { tChannelID channelId = chanmap->GetChanID(tvmid, index); if (!channelId.Valid()) { tell(0, "TVM2VDR: Invalid channel ID '%s' in channelmap.conf, ignoring", chanmap->GetChanStr(tvmid, index)); continue; } for (xmlNodePtr node = xmlRoot->xmlChildrenNode; node; node = node->next) { cDbRow* evt = 0; char* prop = 0; tEventID id; string comp; // skip all unexpected elements if (node->type != XML_ELEMENT_NODE || strcmp((char*)node->name, "event") != 0) continue; // get/check id if (!(prop = (char*)xmlGetProp(node, (xmlChar*)"id")) || !*prop || !(id = atoi(prop))) { xmlFree(prop); tell(0, "TVM2VDR: Missing event id, ignoring!\n"); continue; } xmlFree(prop); // create event .. evt = new cDbRow(cEventFields::fields); evt->setValue(cEventFields::fiEventId, id); evt->setValue(cEventFields::fiChannelId, channelId.ToString()); epgDb->find(evt); evt->setValue(cEventFields::fiUpdFlg, "I"); evt->setValue(cEventFields::fiSource, "tvm"); evt->setValue(cEventFields::fiFileRef, fileRef); evt->setValue(cEventFields::fiTableId, 0); evt->setValue(cEventFields::fiVersion, 0xFF); parseEvent(evt, node); // compressed .. comp = evt->getStrValue(cEventFields::fiTitle); prepareCompressed(comp); evt->setValue(cEventFields::fiCompTitle, comp.c_str()); comp = evt->getStrValue(cEventFields::fiShortText); prepareCompressed(comp); evt->setValue(cEventFields::fiCompShortText, comp.c_str()); // store .. epgDb->store(evt); delete evt; } } xmlFreeDoc(transformedDoc); tell(3, "TVM2VDR: XML File '%s' processed", xmlfile); return success; } //*************************************************************************** // Parse XML Event //*************************************************************************** int cUpdate::parseEvent(cDbRow* event, xmlNode* node) { const char* name; char* content; char* images = 0; char* imagetype = 0; for (xmlNodePtr n = node->xmlChildrenNode; n; n = n->next) { if (n->type != XML_ELEMENT_NODE) continue; name = (const char*)n->name; content = (char*)xmlNodeGetContent(n); if (strcmp(name, "images") == 0) images = strdup(content); else if (strcmp(name, "imagetype") == 0) imagetype = strdup(content); else if (cDbService::FieldDef* f = cEventFields::toField(name)) { if (f->format == cDbService::ffAscii) event->setValue(f->index, content); else event->setValue(f->index, atoi(content)); } else { tell(2, "TVM2VDR: Ignoring unexpected element <%s>\n", name); } xmlFree(content); } if (images && imagetype) storeImages(event->getIntValue(cEventFields::fiEventId), images, imagetype); free(images); free(imagetype); return success; } //*************************************************************************** // To/From Row //*************************************************************************** cEvent* cUpdate::createEventFromRow(const cDbRow* row) { cEvent* e = new cEvent(row->getIntValue(cEventFields::fiEventId)); e->SetTableID(row->getIntValue(cEventFields::fiTableId)); e->SetVersion(row->getIntValue(cEventFields::fiVersion)); e->SetTitle(row->getStrValue(cEventFields::fiTitle)); e->SetShortText(row->getStrValue(cEventFields::fiShortText)); e->SetStartTime(row->getIntValue(cEventFields::fiStartTime)); e->SetDuration(row->getIntValue(cEventFields::fiDuration)); e->SetParentalRating(row->getIntValue(cEventFields::fiParentalRating)); e->SetVps(row->getIntValue(cEventFields::fiVps)); e->SetDescription(row->getStrValue(cEventFields::fiDescription)); return e; } //*************************************************************************** // Update VDRs EPG //*************************************************************************** int cUpdate::updateVdrEpg(const int tvmid, const char* fileRef) { const cEvent* event; cSchedule* s; cSchedulesLock schedulesLock(true); // , 1000); cSchedules* ss = (cSchedules*)cSchedules::Schedules(schedulesLock); if (!ss) { tell(0, "TVM2VDR: Error, can't get lock on schedules"); return fail; } if (isEmpty(fileRef) || !strchr(fileRef, '-')) return fail; int nameLen = strchr(fileRef, '-') - fileRef; // iterate over all channels of this tvmid for (int index = 0; index < chanmap->GetChanCount(tvmid); index++) { int count = 0; int updated = 0; int deleted = 0; tChannelID channelId = chanmap->GetChanID(tvmid, index); if (!channelId.Valid()) { tell(0, "TVM2VDR: Invalid channel ID '%s' in channelmap.conf, ignoring", chanmap->GetChanStr(tvmid, index)); continue; } if (!(s = (cSchedule*)ss->GetSchedule(channelId))) s = ss->AddSchedule(channelId); epgDb->clear(); epgDb->setValue(cEventFields::fiChannelId, channelId.ToString()); epgDb->setValue(cEventFields::fiSource, "tvm"); // iterate over all events of this channel for (int found = epgDb->find(cEventFields::viByChannel); found; found = epgDb->fetch(cEventFields::viByChannel)) { tEventID id = epgDb->getIntValue(cEventFields::fiEventId); if (!epgDb->find(cEventFields::viVdr)) // perform select to load view fields { tell(0, "TVM2VDR: Fatal can't lookup event %d", id); continue; } if (strncmp(epgDb->getStrValue(cEventFields::fiFileRef), fileRef, nameLen) != 0) { epgDb->resetFetch(cEventFields::viVdr); continue; } int refMatch = strcmp(epgDb->getStrValue(cEventFields::fiFileRef), fileRef) == 0; cTimer* timer = 0; if (event = s->GetEvent(id)) { if (refMatch) updated++; else deleted++; if (timer = getTimerOf(event)) timer->SetEvent(0); s->DelEvent((cEvent*)event); } if (refMatch) { count++; event = s->AddEvent(createEventFromRow(epgDb->getRow())); if (timer) timer->SetEvent(event); } epgDb->resetFetch(cEventFields::viVdr); } epgDb->resetFetch(cEventFields::viByChannel); s->Sort(); tell(2, "TVM2VDR: Channel '%s' - %d events inserted, %d updated, %d events removed ", (const char*)channelId.ToString(), count-updated, updated, deleted); } ss->SetModified(s); return success; } //*************************************************************************** // //*************************************************************************** int cUpdate::dropVdrsEpg() { cSchedulesLock schedulesLock(true); // , 1000); cSchedules* ss = (cSchedules*)cSchedules::Schedules(schedulesLock); for (cChanMap::iterator iter = chanmap->chanmap.begin(); iter != chanmap->chanmap.end(); iter++) { if (iter->first <= 0) continue; for (int index = 0; index < chanmap->GetChanCount(iter->first); index++) { cSchedule* s; tChannelID channelId = chanmap->GetChanID(iter->first, index); if (channelId.Valid() && (s = (cSchedule*)ss->GetSchedule(channelId))) { cTimer* timer; while (cEvent* evt = s->Events()->First()) { if (timer = getTimerOf(evt)) timer->SetEvent(0); s->DelEvent(evt); } ss->SetModified(s); } } } return done; } //*************************************************************************** // //*************************************************************************** int cUpdate::refreshEpg() { const cEvent* event; int count = 0; cSchedule* s; cSchedulesLock schedulesLock(true); cSchedules* ss = (cSchedules*)cSchedules::Schedules(schedulesLock); if (!ss) { tell(0, "TVM2VDR: Error, can't get lock on schedules"); return fail; } // refresh all EPG events which marked with 'U' epgDb->clear(); epgDb->setValue(cEventFields::fiSource, "tvm"); epgDb->setValue(cEventFields::fiUpdFlg, "U"); for (int found = epgDb->find(cEventFields::viByUpdFlag); found; found = epgDb->fetch(cEventFields::viByUpdFlag)) { if (!epgDb->find(cEventFields::viVdr)) // perform select to load view fields { tell(0, "TVM2VDR: Fatal lookup of event '%ld/%s' failed", epgDb->getIntValue(cEventFields::fiEventId), epgDb->getStrValue(cEventFields::fiChannelId)); continue; } tChannelID channelId = tChannelID::FromString(epgDb->getStrValue(cEventFields::fiChannelId)); cTimer* timer = 0; if (!(s = (cSchedule*)ss->GetSchedule(channelId))) { epgDb->resetFetch(cEventFields::viVdr); tell(0, "TVM2VDR: Fatal lookup of channel '%s' failed", epgDb->getStrValue(cEventFields::fiChannelId)); continue; } if (event = s->GetEvent(epgDb->getIntValue(cEventFields::fiEventId))) { timer = getTimerOf(event); s->DelEvent((cEvent*)event); } event = s->AddEvent(createEventFromRow(epgDb->getRow())); if (timer) timer->SetEvent(event); s->Sort(); ss->SetModified(s); epgDb->resetFetch(cEventFields::viVdr); // set update flag epgDb->find(); epgDb->setValue(cEventFields::fiUpdFlg, "I"); epgDb->store(); count++; } epgDb->resetFetch(cEventFields::viByUpdFlag); tell(1, "TVM2VDR: Updated %d epg events due to episodes match", count); return success; } //*************************************************************************** // Store Images //*************************************************************************** int cUpdate::storeImages(int evtId, const char* images, const char* ext) { char* next; char* image; int lfn = 0; char* imagesCsv = strdup(images); for (char* p = imagesCsv; p && *p; p = next, lfn++) { if (next = strchr(p, ',')) { *next = 0; // terminate next++; } asprintf(&image, "%s.%s", p, ext); imageDb->clear(); imageDb->setValue(cImageFields::fiEventId, evtId); imageDb->setValue(cImageFields::fiLfn, lfn); imageDb->setValue(cImageFields::fiName, image); imageDb->store(); free(image); } free(imagesCsv); return success; } //*************************************************************************** // Get Image Url //*************************************************************************** int cUpdate::getImageUrl(int imgSize, char*& url, const char* image) { switch (imgSize) { case 0: asprintf(&url, "http://wwwa.tvmovie.de/imageTransfer/XL%s", image); break; case 1: asprintf(&url, "http://wwwa.tvmovie.de/imageTransfer/XXL%s", image); break; case 2: asprintf(&url, "http://img.tvmovie.de/imageTransfer/F1_%s", image); break; } return 0; } //*************************************************************************** // Get Pictures //*************************************************************************** int cUpdate::getPictures() { int count = 0; int total = 0; unsigned int bytes = 0; int downloaded; // fetch all images view tell(0, "TVM2VDR: Start update of images"); imageDb->clear(); imageDb->setValue(cImageFields::fiLfn, TVM2VDRConfig.maximagesperevent); // limit to config for (int res = imageDb->find(cImageFields::viAllLessLfn); res; res = imageDb->fetch(cImageFields::viAllLessLfn)) { int eventid = imageDb->getIntValue(cImageFields::fiEventId); const char* image = imageDb->getStrValue(cImageFields::fiName); int lfn = imageDb->getIntValue(cImageFields::fiLfn); if (lfn != 0) tell(0, "TVM2VDR: Got lfn %d from table, max is %d", lfn, TVM2VDRConfig.maximagesperevent); char* destfile; char* linkdest; char* url = 0; asprintf(&destfile, "%s/images/%s", epgimagedir, image); asprintf(&linkdest, "./images/%s", image); downloaded = no; total++; if (!(total % 500)) tell(0, "TVM2VDR: Still updating images, now %d checked and %d loaded (%d KB)", total, count, bytes/1024); // get image if missing if (!fileExists(destfile)) { int fileSize = 0; for (int trySize = TVM2VDRConfig.epgImageSize; trySize+1; trySize--) { free(url); url = 0; getImageUrl(trySize, url, image); tell(2, "TVM2VDR: try loading image from '%s'", url); // if download failed technically OR download succeeded (size > 0) -> abort loop if (downloadFile(url, fileSize) != success || fileSize > 0) break; } if (fileSize > 0) { bytes += fileSize; downloaded = yes; count++; tell(2, "TVM2VDR: Downloaded image '%s' with (%d) bytes", destfile, fileSize); if (FILE* fh1 = fopen(destfile, "w")) { fwrite(data.memory, 1, data.size, fh1); fclose(fh1); } else { tell(1, "TVM2VDR: can't write image to '%s', error was '%m'", destfile); } data.clear(); } } // create links ... char* newpath; if (!lfn) { asprintf(&newpath, "%s/%d.%s", epgimagedir, eventid, imageExtension); createLink(newpath, linkdest, downloaded); free(newpath); } // always create link with index even for '0' asprintf(&newpath, "%s/%d_%d.%s", epgimagedir, eventid, lfn, imageExtension); createLink(newpath, linkdest, downloaded); free(newpath); // .... free(destfile); free(linkdest); free(url); } // while imageDb->resetFetch(cImageFields::viAllLessLfn); double mb = (double)bytes / 1024.0 / 1024.0; tell(0, "TVM2VDR: Loaded %d images (%.3f %cB), checked %d", count, mb > 2 ? mb : (double)bytes/1024.0, mb > 2 ? 'M' : 'K', total); return count; } //*************************************************************************** // Cleanup //*************************************************************************** void cUpdate::cleanup() { // remove obsolete files from xml directory cleanupEvents(xmldir); // remove obsolete files from update directory // char* upddir; // asprintf(&upddir, "%s/updates", xmldir); // removeObsoleteFiles(upddir); // free(upddir); // alt last cleanup pictures if (TVM2VDRConfig.getepgimages) cleanupPictures(); } //*************************************************************************** // Remove Old Files //*************************************************************************** void cUpdate::cleanupEvents(const char* path) { const char* ext = ".xml.gz"; char* where; DIR* dir; struct dirent* dirent; struct tm tm; time_t clock = time(0); int count = 0; if (!(dir = opendir(path))) { tell(1, "TVM2VDR: Can't open directory '%s', '%m'", path); return ; } tell(1, "TVM2VDR: Starting cleanup of '%s'", path); // detete all row in tvmfiles for files of 'yesterday' localtime_r(&clock, &tm); tm.tm_mday--; clock = mktime(&tm); // rebuild 'tm' to adjust tm's date with decremented day! asprintf(&where, "substr(name,1,8) <= '%4d%02d%02d'", tm.tm_year + 1900, tm.tm_mon+1, tm.tm_mday); fileDb->deleteWhere(where); free(where); // delete all TVM events without a reference in tvmfiles // ... VDRs events where cleand by VDR itself epgDb->deleteWhere("source = 'tvm' and fileref not in (select fileref from tvmfiles)"); // delete VDR events older than 6 hours asprintf(&where, "source = 'vdr' and starttime+duration < %ld", time(0)-6*60*60); epgDb->deleteWhere(where); free(where); // cleanup directory, remove all files witout a entry in tvmfiles while (dirent = readdir(dir)) { char* filename; // check extension if (strncmp(dirent->d_name + strlen(dirent->d_name) - strlen(ext), ext, strlen(ext)) != 0) continue; // check asprintf(&filename, "%.*s", (int)strlen(dirent->d_name)-3, dirent->d_name); // filename withgout ".gz" fileDb->setValue(cTvmFields::fiName, filename); free(filename); if (!fileDb->find()) { asprintf(&filename, "%s/%s", path, dirent->d_name); if (!removeFile(filename)) count++; free(filename); } } closedir(dir); tell(1, "TVM2VDR: Cleanup finished, removed (%d) files", count); } //*************************************************************************** // Remove Pictures //*************************************************************************** void cUpdate::cleanupPictures() { const char* ext = ".jpg"; struct dirent* dirent; DIR* dir; char* pdir; int iCount = 0; int lCount = 0; // ----------------------- // remove unused images asprintf(&pdir, "%s/images", epgimagedir); if (!(dir = opendir(pdir))) { tell(1, "TVM2VDR: Can't open directory '%s', '%m'", pdir); free(pdir); return ; } free(pdir); tell(1, "TVM2VDR: Starting cleanup of images in '%s'", epgimagedir); // cleanup image table -> remove unsused images imageDb->deleteWhere("eventid not in (select eventid from events)"); // ----------------------- // cleanup 'images' directory while ((dirent = readdir(dir))) { // check extension if (strncmp(dirent->d_name + strlen(dirent->d_name) - strlen(ext), ext, strlen(ext)) != 0) continue; imageDb->clear(); imageDb->setValue(cImageFields::fiLfn, TVM2VDRConfig.maximagesperevent); // limit to config imageDb->setValue(cImageFields::fiName, dirent->d_name); if (!imageDb->find(cImageFields::viAllByName)) { asprintf(&pdir, "%s/images/%s", epgimagedir, dirent->d_name); if (!removeFile(pdir)) iCount++; free(pdir); } imageDb->resetFetch(cImageFields::viAllByName); } // ----------------------- // remove wasted symlinks if (!(dir = opendir(epgimagedir))) { tell(1, "TVM2VDR: Can't open directory '%s', '%m'", epgimagedir); return ; } while ((dirent = readdir(dir))) { // check extension if (strncmp(dirent->d_name + strlen(dirent->d_name) - strlen(ext), ext, strlen(ext)) != 0) continue; asprintf(&pdir, "%s/%s", epgimagedir, dirent->d_name); if (isLink(pdir) && (!fileExists(pdir) || !pictureLinkNeeded(dirent->d_name))) { if (!removeFile(pdir)) lCount++; } free(pdir); } tell(1, "TVM2VDR: Cleanup finished, removed (%d) images and (%d) symlinks", iCount, lCount); } //*************************************************************************** // Link Needed //*************************************************************************** int cUpdate::pictureLinkNeeded(const char* linkName) { // we don't need to patch the linkname "123456_0.jpg" // since atoi() stops at the first non numerical character ... imageDb->clear(); imageDb->setValue(cImageFields::fiLfn, 0); imageDb->setValue(cImageFields::fiEventId, atoi(linkName)); return imageDb->find(); }