#if 0 set -x g++ -W -Wall -ggdb -Os -o `basename $0 .cc` $0 $* exit $? #endif /* A simple utility to cut a recording in place. This is most usable when the recordings are in very small files. Smallest VDR allows is 100 MB, and this is quite OK. Cutting in place means most of the recording probably does not have to be copied on disk, only parts of files around the cutting mark need to be copied, and therefore the process is very fast. Additionally, the files that need to be touched are recorded onto themselves, including the index file, thus no additional disk space is needed. This is ideal when you need to free some space by cutting but have no space for temporary cutting results. Copyright © 2010 Jaakko Hyvätti Version 0.5 This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. The author may be contacted at: Email: Jaakko.Hyvatti@iki.fi URL: http://www.iki.fi/hyvatti/ Phone: +358 40 5011222 Please send any suggestions and bug reports to the Email address above. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; static bool debug = false; // This has to be the same as in VDR recording.h, otherwise the marks // will not be in correct places! #define FRAMESPERSEC 25 static const char *IndexToHMSF(int Index) { static char buffer[16]; int f = (Index % FRAMESPERSEC) + 1; int s = (Index / FRAMESPERSEC); int m = s / 60 % 60; int h = s / 3600; s %= 60; snprintf(buffer, sizeof(buffer), "%d:%02d:%02d.%02d", h, m, s, f); return buffer; } static unsigned int HMSFToIndex(const char *HMSF) { int h, m, s, f = 0; if (3 <= sscanf(HMSF, "%d:%d:%d.%d", &h, &m, &s, &f)) return (h * 3600 + m * 60 + s) * FRAMESPERSEC + f - 1; return 0; } static void remove_if_empty (string d) { // Find the first / after the first non-/ char. Used later when // checking for top dir which is never removed. unsigned int limit = 0; while (limit < d.length() && '/' == d [limit]) ++limit; while (limit < d.length() && '/' != d [limit]) ++limit; // Remove any trailing slashes. There should be none though. unsigned int er = d.length(); while (er > 0 && '/' == d [er-1]) --er; if (er != d.length ()) d.erase (er); // Now the loop over this and parent directories bool first = true; while (limit < er) { if (debug) { cerr << "try rmdir " << d << endl; } else { if (0 != rmdir (d.c_str())) { if (first) { // We know that this dir should have been empty cerr << d << ": warning: unable to remove dir even if it should have been empty: " << strerror(errno) << endl; } return; } cout << d << ": empty directory, removed" << endl; } first = false; // remove last component while (er > 0 && '/' != d [er-1]) --er; // remove any trailing slashes while (er > 0 && '/' == d [er-1]) --er; if (er != d.length ()) d.erase (er); } } struct tIndex { uint32_t offset; unsigned char type; unsigned char number; int16_t reserved; }; struct fileops { enum mode { nop, del, preserve, copy }; // If operation==copy, parts that need copying are listed here. vector offsets; // Parts of index file needed are listed here. vector indexes; // If operation==preserve or ==copy, // this is the target file number. int number; // What to do with this file. mode operation; fileops () : operation(nop) {}; }; // Do not use the entry files[0], use file number as such as index. static fileops files[256]; map linkdirs; // Follow the links on this file name // Delete the actual file too (if in delete mode) // Mark the directory to be deleted if empty (if in delete mode) static void checklink (string d, int oldnum, int newnum) { char oldnostr [4]; sprintf (oldnostr, "%03d", oldnum); string fn = d + "/" + oldnostr + ".vdr"; struct stat st; if (0 > lstat (fn.c_str(), &st)) return; if (S_ISLNK (st.st_mode)) { char linkdest [1024]; int chars = readlink (fn.c_str(), linkdest, sizeof (linkdest)-1); if (chars < 0) { cerr << fn << ": warning: unable to read symbolic link: " << strerror(errno) << endl; return; } linkdest [chars] = '\0'; int er = strlen (linkdest); while (er > 0 && '/' == linkdest [er-1]) --er; while (er > 0 && '/' != linkdest [er-1]) --er; while (er > 0 && '/' == linkdest [er-1]) --er; string linkdir (linkdest, 0, er); if (0 == newnum && er) { // refuse to remove files in root dir :-) if (debug) { cerr << "rm " << linkdest << endl; } else if (0 != unlink (linkdest)) { cerr << fn << " => " << linkdest << ": warning: unable to delete linked file: " << strerror(errno) << endl; return; } // Mark the dir to be deleted if empty. if (linkdirs.end() == linkdirs.find(linkdir)) linkdirs [linkdir] = true; } else { // Mark that this dir should not be tried to be deleted, as it // is not empty. linkdirs [linkdir] = false; if (newnum && oldnum != newnum) { // Rename the destination. char newnostr [4]; sprintf (newnostr, "%03d", newnum); string newdest = linkdir + "/" + newnostr + ".vdr"; if (debug) { cerr << "mv " << linkdest << " " << newdest << endl; } else if (0 != rename (linkdest, newdest.c_str())) { cerr << fn << " => " << linkdest << " to " << newdest << ": error: unable to rename linked file: " << strerror(errno) << endl; return; } string newlink = d + "/" + newnostr + ".vdr"; if (debug) { cerr << "ln -s " << newdest << " " << newlink << endl; } else if (0 != symlink (newdest.c_str(), newlink.c_str())) { cerr << fn << " => " << newdest << " to " << newlink << ": error: unable to create symlink: " << strerror(errno) << endl; return; } } } } else if (newnum && oldnum != newnum) { char newnostr [4]; sprintf (newnostr, "%03d", newnum); string newdest = d + "/" + newnostr + ".vdr"; if (debug) { cerr << "mv " << fn << " " << newdest << endl; } else if (0 != rename (fn.c_str(), newdest.c_str())) { cerr << fn << " to " << newdest << ": error: unable to rename file: " << strerror(errno) << endl; return; } } if (0 == newnum || (oldnum != newnum && S_ISLNK (st.st_mode))) { if (debug) { cerr << "rm " << fn << endl; } else if (0 != unlink (fn.c_str())) { cerr << fn << ": warning: unable to delete: " << strerror(errno) << endl; return; } } } static void copy_index (fstream &f, int num, unsigned int startoffs, unsigned int dst, unsigned int src, unsigned int len) { unsigned int diff = 0; bool first = true; unsigned int buflen = 256*1024; if (len < buflen) buflen = len; tIndex *buf = new tIndex [buflen]; assert (buf); while (len) { unsigned int readlen = buflen; if (len < readlen) readlen = len; f.clear(); // clear the EOF condition. f.seekg (src * sizeof(tIndex)); f.read ((char *)buf, readlen*sizeof(tIndex)); unsigned int n = f.gcount(); n /= sizeof (tIndex); src += n; if (debug) cerr << "Read " << n << " records, pos now " << src << "==" << f.tellg() << endl; if (0 == n && f.eof()) { // This is unexpected cerr << "Warning: trying to read " << readlen << " records of " << len << " to be copied, but found EOF" << endl; exit (1); break; } if (0 == n) { cerr << "Read error trying to read " << readlen << " records of " << len << " to be copied: " << strerror(errno) << endl; break; } // Convert file numbers tIndex *bufp = buf; unsigned int i = n; if (first) { first = false; diff = startoffs - bufp->offset; if (debug) { cerr << "Copy index: file: " << num << ", old file: " << int(bufp->number) << "First offset: new: " << startoffs << ", old: " << bufp->offset << ", index difference " << diff << endl; } } while (i--) { bufp->offset += diff; (bufp++)->number = num; } if (!debug) { f.clear(); f.seekp (dst * sizeof(tIndex)); if (!f.write ((char *)buf, n * sizeof(tIndex))) { cerr << "write error: " << strerror(errno) << endl; exit (1); } dst += n; } len -= n; } delete [] buf; } static void copy_in_file (fstream &f, unsigned int dst, unsigned int src, unsigned int len) { unsigned int buflen = 4*1024*1024; if (len < buflen) buflen = len; char *buf = new char [buflen]; assert (buf); while (len) { unsigned int readlen = buflen; if (len < readlen) readlen = len; f.clear(); f.seekg (src); f.read (buf, readlen); unsigned int n = f.gcount(); src += n; if (debug) cerr << "Read " << n << " bytes, pos now " << src << "==" << f.tellg() << endl; if (0 == n && f.eof()) { // This is unexpected cerr << "Warning: trying to read " << readlen << " bytes of " << len << " to be copied, but found EOF" << endl; break; } if (0 == n) { cerr << "Read error trying to read " << readlen << " bytes of " << len << " to be copied: " << strerror(errno) << endl; break; } if (!debug) { f.clear(); f.seekp (dst); if (!f.write (buf, n)) { cerr << "write error: " << strerror(errno) << endl; exit (1); } dst += n; } len -= n; } delete [] buf; } static void edit (const string dir, int newnum, int &indexnum, vector &offsets, fstream &indexf, vector &indexes) { char fnewno[4]; sprintf (fnewno, "%03d", newnum); string fn = dir + "/" + fnewno + ".vdr"; assert (offsets.size() > 0); assert (indexes.size() > 0); // First of all, free some disk space by truncating the file // at the last editing mark. if ((offsets.size() & 1) == 0) { if (debug) { cerr << "truncate " << fn << " " << offsets.back() << endl; } else if (0 != truncate (fn.c_str (), offsets.back())) { cerr << fn << ": warning: unable to truncate to " << offsets.back() << " bytes: " << strerror(errno) << endl; } // What if all you need to do is truncate? if (offsets.size() == 2 && 0 == offsets[0]) { assert (indexes.size () == 2); unsigned int i1 = indexes[0]; unsigned int i2 = indexes[1]; copy_index (indexf, newnum, 0, indexnum, i1, i2-i1); indexnum += i2-i1; return; } } else { // Push the size of the file as last editing mark struct stat st; if (0 != stat (fn.c_str(), &st)) { cerr << fn << ": unable to stat, aborting: " << strerror(errno) << endl; exit (1); } offsets.push_back (st.st_size); } assert ((offsets.size () & 1) == 0); assert (indexes.size () == offsets.size ()); // Now offsets is always of even size. fstream f (fn.c_str(), ios::in | ios::out); assert (f); unsigned int reslen = 0; vector::const_iterator i = indexes.begin(); for (vector::const_iterator o = offsets.begin(); o != offsets.end(); ++o) { unsigned int p1 = *o; unsigned int p2 = *++o; unsigned int i1 = *i++; unsigned int i2 = *i++; copy_index (indexf, newnum, reslen, indexnum, i1, i2-i1); indexnum += i2-i1; if (reslen == p1) { // Skip, no need to copy as this region is not moved. reslen = p2; } else { if (debug) { cerr << "copy in " << fn << " " << p2-p1 << " bytes " << p1 << ".." << p2 << " to " << f.tellp() << ".." << (unsigned int)(f.tellp())+p2-p1 << endl; } copy_in_file (f, reslen, p1, p2-p1); reslen += p2-p1; } } f.close(); if (debug) { cerr << "truncate " << fn << " " << reslen << endl; } else if (0 != truncate (fn.c_str(), reslen)) { cerr << fn << ": warning: unable to truncate to " << reslen << " bytes: " << strerror(errno) << endl; } } int main (int argc, char **argv) { int c; while (EOF != (c = getopt (argc, argv, "d"))) switch (c) { case 0: break; case 'd': debug = true; break; default: cerr << "For help, run without options." << endl; exit (1); } argc -= optind; argv += optind; if (argc != 1) { cerr << "Usage: cut_in_place recording/..rec" << endl; exit (1); } string dir (argv[0]); unsigned int er = dir.length(); while (er > 0 && '/' == dir [er-1]) --er; if (er != dir.length ()) dir.erase (er); string indexname = dir + "/index.vdr"; fstream indexf (indexname.c_str(), ios::in | ios::out); assert (indexf); string markname = dir + "/marks.vdr"; fstream markf (markname.c_str(), ios::in | ios::out); assert (markf); uint32_t resume = 0; string resumename = dir + "/resume.vdr"; fstream resumef (resumename.c_str(), ios::in | ios::out); if (resumef) { resumef.read ((char *)&resume, sizeof (resume)); } string m; vector markframe, newmarks; while (markf.good()) { std::getline(markf,m); // cout << "mark: " << HMSFToIndex(m.c_str()) << endl; if(!m.empty()) markframe.push_back (HMSFToIndex(m.c_str())); } tIndex irec; unsigned int frame = 0, newframe = 0; vector::const_iterator currmark = markframe.begin(); bool within_marks = false; for (;; ++frame) { indexf.read ((char *)&irec, sizeof (irec)); if (0 == indexf.gcount() && indexf.eof()) break; if (sizeof (irec) != indexf.gcount()) { if (!indexf.eof()) { cerr << indexname << ": read error, read " << indexf.gcount() << " bytes: " << strerror(errno) << endl; exit (1); } cerr << indexname << ": warning: last record was " << indexf.gcount() << " bytes, should be " << sizeof (irec) << endl; break; } if (currmark != markframe.end()) { if (within_marks) { if (*currmark < frame) { within_marks = false; newmarks.push_back (newframe-1); ++currmark; } } else { if (*currmark <= frame) { within_marks = true; newmarks.push_back (newframe); ++currmark; } } } if (resume == frame) resume = newframe; if (within_marks) { switch (files [irec.number].operation) { case fileops::nop: files [irec.number].operation = fileops::preserve; files [irec.number].indexes.push_back (frame); files [irec.number].indexes.push_back (frame+1); break; case fileops::del: files [irec.number].operation = fileops::copy; files [irec.number].offsets.push_back (irec.offset); files [irec.number].indexes.push_back (frame); files [irec.number].indexes.push_back (frame+1); break; case fileops::preserve: files [irec.number].indexes.back() = frame+1; break; case fileops::copy: if ((files [irec.number].offsets.size() & 1) == 0) { files [irec.number].offsets.push_back (irec.offset); files [irec.number].indexes.push_back (frame); files [irec.number].indexes.push_back (frame+1); } else { files [irec.number].indexes.back() = frame+1; } break; } ++newframe; } else { // !within_marks switch (files [irec.number].operation) { case fileops::nop: files [irec.number].operation = fileops::del; break; case fileops::del: // No need to mark anything break; case fileops::preserve: files [irec.number].operation = fileops::copy; files [irec.number].offsets.push_back (0); files [irec.number].offsets.push_back (irec.offset); break; case fileops::copy: if ((files [irec.number].offsets.size() & 1) == 1) files [irec.number].offsets.push_back (irec.offset); break; } } } // Go through the file records and count which file is renamed to which one. int newnum = 0; int indexnum = 0; for (int i = 1; i <= 255; ++i) { switch (files [i].operation) { case fileops::nop: break; case fileops::del: checklink (dir, i, 0); cout << setw(3) << setfill('0') << i << ": deleted" << endl; break; case fileops::preserve: ++newnum; files [i].number = newnum; checklink (dir, i, newnum); assert (files[i].indexes.size () == 2); copy_index (indexf, newnum, 0, indexnum, files[i].indexes[0], files[i].indexes[1] - files[i].indexes[0]); indexnum += files[i].indexes[1] - files[i].indexes[0]; cout << setw(3) << setfill('0') << i << ": preserved as " << setw(3) << setfill('0') << newnum << endl; break; case fileops::copy: ++newnum; files [i].number = newnum; if (debug) { for (vector::const_iterator o = files[i].indexes.begin(); o != files[i].indexes.end(); ++o) { cerr << "Index start: " << *o; if (files[i].indexes.end() != o+1) { cerr << ", end: " << *++o << endl; } } } checklink (dir, i, newnum); edit (dir, newnum, indexnum, files[i].offsets, indexf, files[i].indexes); cout << setw(3) << setfill('0') << i << ": copied to " << setw(3) << setfill('0') << newnum; for (vector::const_iterator o = files[i].offsets.begin(); o != files[i].offsets.end(); ++o) { cout << ", start: " << *o; if (files[i].offsets.end() != o+1) { cout << ", end: " << *++o; } } cout << endl; break; } } // Truncate the index file indexf.close(); if (debug) { cerr << "truncate " << indexname << " " << indexnum*sizeof(tIndex) << endl; } else if (0 != truncate (indexname.c_str(), indexnum*sizeof(tIndex))) { cerr << indexname << ": unable to truncate to " << indexnum*sizeof(tIndex) << " bytes: " << strerror(errno) << endl; } // Remove empty directories left over when deleting files. for (map::const_iterator s = linkdirs.begin(); s != linkdirs.end(); ++s) { if (s->second) remove_if_empty (s->first); } markf.clear(); markf.seekp (0); for (vector::const_iterator m = newmarks.begin(); m != newmarks.end(); ++m) { if (debug) { cerr << "Index mark: " << IndexToHMSF(*m) << endl; } else { markf << IndexToHMSF(*m) << endl; } } streampos p = markf.tellp(); if (p <= 0) { cerr << markname << ": offset is " << p << endl; } if (debug) { cerr << "truncate " << markname << " " << p << " =0, no not really!" << endl; } else { if (0 != truncate (markname.c_str (), p)) { cerr << markname << ": unable to truncate to " << p << " bytes: " << strerror(errno) << endl; } } resumef.clear(); resumef.seekp (0); resumef.write ((char *)&resume, sizeof (resume)); return 0; }