Basics of joining.
authorCarl Hetherington <cth@carlh.net>
Sat, 23 Nov 2013 20:24:51 +0000 (20:24 +0000)
committerCarl Hetherington <cth@carlh.net>
Sat, 23 Nov 2013 20:24:51 +0000 (20:24 +0000)
18 files changed:
src/lib/audio_content.cc
src/lib/audio_content.h
src/lib/content.cc
src/lib/content.h
src/lib/exceptions.h
src/lib/ffmpeg_content.cc
src/lib/ffmpeg_content.h
src/lib/ffmpeg_examiner.cc
src/lib/ffmpeg_examiner.h
src/lib/subtitle_content.cc
src/lib/subtitle_content.h
src/lib/util.cc
src/lib/util.h
src/lib/video_content.cc
src/lib/video_content.h
src/wx/content_menu.cc
src/wx/content_menu.h
test/util_test.cc

index e0eaacb919137f74a89756f61714da9d38eaf0f4..04823d1e6f12a0aa653a6c6a0f3a3c9ae57af29c 100644 (file)
 #include "analyse_audio_job.h"
 #include "job_manager.h"
 #include "film.h"
+#include "exceptions.h"
+
+#include "i18n.h"
 
 using std::string;
+using std::vector;
 using boost::shared_ptr;
 using boost::lexical_cast;
 using boost::dynamic_pointer_cast;
@@ -60,6 +64,28 @@ AudioContent::AudioContent (shared_ptr<const Film> f, shared_ptr<const cxml::Nod
        _audio_delay = node->number_child<int> ("AudioDelay");
 }
 
+AudioContent::AudioContent (shared_ptr<const Film> f, vector<shared_ptr<Content> > c)
+       : Content (f, c)
+{
+       shared_ptr<AudioContent> ref = dynamic_pointer_cast<AudioContent> (c[0]);
+       assert (ref);
+       
+       for (size_t i = 0; i < c.size(); ++i) {
+               shared_ptr<AudioContent> ac = dynamic_pointer_cast<AudioContent> (c[i]);
+
+               if (ac->audio_gain() != ref->audio_gain()) {
+                       throw JoinError (_("Content to be joined must have the same audio gain."));
+               }
+
+               if (ac->audio_delay() != ref->audio_delay()) {
+                       throw JoinError (_("Content to be joined must have the same audio delay."));
+               }
+       }
+
+       _audio_gain = ref->audio_gain ();
+       _audio_delay = ref->audio_delay ();
+}
+
 void
 AudioContent::as_xml (xmlpp::Node* node) const
 {
index 73919105d8a339cab8e8f93df6a61f77bf146534..b100d7abad18f4f83e5f724a92b8ca322a490d60 100644 (file)
@@ -46,6 +46,7 @@ public:
        AudioContent (boost::shared_ptr<const Film>, Time);
        AudioContent (boost::shared_ptr<const Film>, boost::filesystem::path);
        AudioContent (boost::shared_ptr<const Film>, boost::shared_ptr<const cxml::Node>);
+       AudioContent (boost::shared_ptr<const Film>, std::vector<boost::shared_ptr<Content> >);
 
        void as_xml (xmlpp::Node *) const;
        std::string technical_summary () const;
index 16069b7a7967b52a1200c40d241e2a6e91ff20eb..eaa55790ba063eb31bbfc36231f756f2b6ba3054 100644 (file)
 #include "util.h"
 #include "content_factory.h"
 #include "ui_signaller.h"
+#include "exceptions.h"
+
+#include "i18n.h"
 
 using std::string;
 using std::stringstream;
 using std::set;
+using std::list;
 using std::cout;
+using std::vector;
 using boost::shared_ptr;
 using boost::lexical_cast;
 
@@ -72,19 +77,42 @@ Content::Content (shared_ptr<const Film> f, shared_ptr<const cxml::Node> node)
        : _film (f)
        , _change_signals_frequent (false)
 {
-       _paths.push_back (node->string_child ("Path"));
+       list<cxml::NodePtr> path_children = node->node_children ("Path");
+       for (list<cxml::NodePtr>::const_iterator i = path_children.begin(); i != path_children.end(); ++i) {
+               _paths.push_back ((*i)->content ());
+       }
        _digest = node->string_child ("Digest");
        _position = node->number_child<Time> ("Position");
        _trim_start = node->number_child<Time> ("TrimStart");
        _trim_end = node->number_child<Time> ("TrimEnd");
 }
 
+Content::Content (shared_ptr<const Film> f, vector<shared_ptr<Content> > c)
+       : _film (f)
+       , _position (c.front()->position ())
+       , _trim_start (c.front()->trim_start ())
+       , _trim_end (c.back()->trim_end ())
+       , _change_signals_frequent (false)
+{
+       for (size_t i = 0; i < c.size(); ++i) {
+               if (i > 0 && c[i]->trim_start ()) {
+                       throw JoinError (_("Only the first piece of content to be joined can have a start trim."));
+               }
+
+               if (i < (c.size() - 1) && c[i]->trim_end ()) {
+                       throw JoinError (_("Only the last piece of content to be joined can have an end trim."));
+               }
+       }
+}
+
 void
 Content::as_xml (xmlpp::Node* node) const
 {
        boost::mutex::scoped_lock lm (_mutex);
-       
-       node->add_child("Path")->add_child_text (_paths.front().string());
+
+       for (vector<boost::filesystem::path>::const_iterator i = _paths.begin(); i != _paths.end(); ++i) {
+               node->add_child("Path")->add_child_text (i->string ());
+       }
        node->add_child("Digest")->add_child_text (_digest);
        node->add_child("Position")->add_child_text (lexical_cast<string> (_position));
        node->add_child("TrimStart")->add_child_text (lexical_cast<string> (_trim_start));
@@ -95,15 +123,10 @@ void
 Content::examine (shared_ptr<Job> job)
 {
        boost::mutex::scoped_lock lm (_mutex);
-       boost::filesystem::path p = _paths.front ();
+       vector<boost::filesystem::path> p = _paths;
        lm.unlock ();
        
-       string d;
-       if (boost::filesystem::is_regular_file (p)) {
-               d = md5_digest (p);
-       } else {
-               d = md5_digest_directory (p, job);
-       }
+       string const d = md5_digest (p, job);
 
        lm.lock ();
        _digest = d;
@@ -206,7 +229,13 @@ Content::identifier () const
 bool
 Content::path_valid () const
 {
-       return boost::filesystem::exists (_paths.front ());
+       for (vector<boost::filesystem::path>::const_iterator i = _paths.begin(); i != _paths.end(); ++i) {
+               if (!boost::filesystem::exists (*i)) {
+                       return false;
+               }
+       }
+
+       return true;
 }
 
 void
index b4d90b22f4353a28a800424652ba9cd43a3a16f8..9cf6d866ab04906b3288188792c66f9403b8592e 100644 (file)
@@ -52,6 +52,7 @@ public:
        Content (boost::shared_ptr<const Film>, Time);
        Content (boost::shared_ptr<const Film>, boost::filesystem::path);
        Content (boost::shared_ptr<const Film>, boost::shared_ptr<const cxml::Node>);
+       Content (boost::shared_ptr<const Film>, std::vector<boost::shared_ptr<Content> >);
        virtual ~Content () {}
        
        virtual void examine (boost::shared_ptr<Job>);
index b04d973dc7a62197571ce7d6697917f2023b23e0..f4631c09b67310bcf75492daca3b2b58dfd5e835 100644 (file)
@@ -104,7 +104,14 @@ private:
        /** name of the file that this exception concerns */
        boost::filesystem::path _file;
 };
-       
+
+class JoinError : public StringError
+{
+public:
+       JoinError (std::string s)
+               : StringError (s)
+       {}
+};
 
 /** @class OpenFileError.
  *  @brief Indicates that some error occurred when trying to open a file.
index 4ca2ddc2933ff34e7a323c0a9b038c2a22d5e52f..e843e1e168c3fbb71e561279b9babe2debf6b376 100644 (file)
@@ -26,6 +26,7 @@
 #include "filter.h"
 #include "film.h"
 #include "log.h"
+#include "exceptions.h"
 
 #include "i18n.h"
 
@@ -37,6 +38,7 @@ using std::cout;
 using std::pair;
 using boost::shared_ptr;
 using boost::lexical_cast;
+using boost::dynamic_pointer_cast;
 
 int const FFmpegContentProperty::SUBTITLE_STREAMS = 100;
 int const FFmpegContentProperty::SUBTITLE_STREAM = 101;
@@ -83,6 +85,33 @@ FFmpegContent::FFmpegContent (shared_ptr<const Film> f, shared_ptr<const cxml::N
        _first_video = node->optional_number_child<double> ("FirstVideo");
 }
 
+FFmpegContent::FFmpegContent (shared_ptr<const Film> f, vector<boost::shared_ptr<Content> > c)
+       : Content (f, c)
+       , VideoContent (f, c)
+       , AudioContent (f, c)
+       , SubtitleContent (f, c)
+{
+       shared_ptr<FFmpegContent> ref = dynamic_pointer_cast<FFmpegContent> (c[0]);
+       assert (ref);
+       
+       for (size_t i = 0; i < c.size(); ++i) {
+               shared_ptr<FFmpegContent> fc = dynamic_pointer_cast<FFmpegContent> (c[i]);
+               if (*(fc->_subtitle_stream.get()) != *(ref->_subtitle_stream.get())) {
+                       throw JoinError (_("Content to be joined must use the same subtitle stream."));
+               }
+
+               if (*(fc->_audio_stream.get()) != *(ref->_audio_stream.get())) {
+                       throw JoinError (_("Content to be joined must use the same audio stream."));
+               }
+       }
+
+       _subtitle_streams = ref->subtitle_streams ();
+       _subtitle_stream = ref->subtitle_stream ();
+       _audio_streams = ref->audio_streams ();
+       _audio_stream = ref->audio_stream ();
+       _first_video = ref->_first_video;
+}
+
 void
 FFmpegContent::as_xml (xmlpp::Node* node) const
 {
@@ -299,12 +328,24 @@ operator== (FFmpegSubtitleStream const & a, FFmpegSubtitleStream const & b)
        return a.id == b.id;
 }
 
+bool
+operator!= (FFmpegSubtitleStream const & a, FFmpegSubtitleStream const & b)
+{
+       return a.id != b.id;
+}
+
 bool
 operator== (FFmpegAudioStream const & a, FFmpegAudioStream const & b)
 {
        return a.id == b.id;
 }
 
+bool
+operator!= (FFmpegAudioStream const & a, FFmpegAudioStream const & b)
+{
+       return a.id != b.id;
+}
+
 FFmpegAudioStream::FFmpegAudioStream (shared_ptr<const cxml::Node> node)
        : mapping (node->node_child ("Mapping"))
 {
index 775cb92205c9f2b9a1660a3713d08c14ab5871f9..4576aaf45281f59f50a0743e2983363336acbb4c 100644 (file)
@@ -63,6 +63,7 @@ private:
 };
 
 extern bool operator== (FFmpegAudioStream const & a, FFmpegAudioStream const & b);
+extern bool operator!= (FFmpegAudioStream const & a, FFmpegAudioStream const & b);
 
 class FFmpegSubtitleStream
 {
@@ -81,6 +82,7 @@ public:
 };
 
 extern bool operator== (FFmpegSubtitleStream const & a, FFmpegSubtitleStream const & b);
+extern bool operator!= (FFmpegSubtitleStream const & a, FFmpegSubtitleStream const & b);
 
 class FFmpegContentProperty : public VideoContentProperty
 {
@@ -97,6 +99,7 @@ class FFmpegContent : public VideoContent, public AudioContent, public SubtitleC
 public:
        FFmpegContent (boost::shared_ptr<const Film>, boost::filesystem::path);
        FFmpegContent (boost::shared_ptr<const Film>, boost::shared_ptr<const cxml::Node>);
+       FFmpegContent (boost::shared_ptr<const Film>, std::vector<boost::shared_ptr<Content> >);
 
        boost::shared_ptr<FFmpegContent> shared_from_this () {
                return boost::dynamic_pointer_cast<FFmpegContent> (Content::shared_from_this ());
@@ -153,7 +156,7 @@ public:
                boost::mutex::scoped_lock lm (_mutex);
                return _first_video;
        }
-       
+
 private:
        friend class ffmpeg_pts_offset_test;
        
index 4d10aabbb85daf819f127a6946888446e56121ec..8e4f24720bc920204aafcb19a2364ce53034c3d7 100644 (file)
@@ -50,12 +50,12 @@ FFmpegExaminer::FFmpegExaminer (shared_ptr<const FFmpegContent> c)
                        
                        _audio_streams.push_back (
                                shared_ptr<FFmpegAudioStream> (
-                                       new FFmpegAudioStream (stream_name (s), i, s->codec->sample_rate, s->codec->channels)
+                                       new FFmpegAudioStream (audio_stream_name (s), i, s->codec->sample_rate, s->codec->channels)
                                        )
                                );
 
                } else if (s->codec->codec_type == AVMEDIA_TYPE_SUBTITLE) {
-                       _subtitle_streams.push_back (shared_ptr<FFmpegSubtitleStream> (new FFmpegSubtitleStream (stream_name (s), i)));
+                       _subtitle_streams.push_back (shared_ptr<FFmpegSubtitleStream> (new FFmpegSubtitleStream (subtitle_stream_name (s), i)));
                }
        }
 
@@ -140,6 +140,36 @@ FFmpegExaminer::video_length () const
        return max (1, length);
 }
 
+string
+FFmpegExaminer::audio_stream_name (AVStream* s) const
+{
+       stringstream n;
+
+       n << stream_name (s);
+
+       if (!n.str().empty()) {
+               n << "; ";
+       }
+
+       n << s->codec->channels << " channels";
+
+       return n.str ();
+}
+
+string
+FFmpegExaminer::subtitle_stream_name (AVStream* s) const
+{
+       stringstream n;
+
+       n << stream_name (s);
+
+       if (n.str().empty()) {
+               n << _("unknown");
+       }
+
+       return n.str ();
+}
+
 string
 FFmpegExaminer::stream_name (AVStream* s) const
 {
@@ -160,11 +190,5 @@ FFmpegExaminer::stream_name (AVStream* s) const
                }
        }
 
-       if (!n.str().empty()) {
-               n << "; ";
-       }
-
-       n << s->codec->channels << " channels";
-
        return n.str ();
 }
index 4912d899a85f7a70015fa83c72fe61743cb6aab2..4de475d2a3f78d753985f6b165e56bf3b74c7290 100644 (file)
@@ -47,6 +47,8 @@ public:
        
 private:
        std::string stream_name (AVStream* s) const;
+       std::string audio_stream_name (AVStream* s) const;
+       std::string subtitle_stream_name (AVStream* s) const;
        boost::optional<double> frame_time (int) const;
        
        std::vector<boost::shared_ptr<FFmpegSubtitleStream> > _subtitle_streams;
index 1194e2dc950b99531185544b162ea5b2eae9b907..6c0d3af86a6687571d74e3bb576add9494c67567 100644 (file)
 #include <libcxml/cxml.h>
 #include "subtitle_content.h"
 #include "util.h"
+#include "exceptions.h"
+
+#include "i18n.h"
 
 using std::string;
+using std::vector;
 using boost::shared_ptr;
 using boost::lexical_cast;
+using boost::dynamic_pointer_cast;
 
 int const SubtitleContentProperty::SUBTITLE_OFFSET = 500;
 int const SubtitleContentProperty::SUBTITLE_SCALE = 501;
@@ -47,6 +52,28 @@ SubtitleContent::SubtitleContent (shared_ptr<const Film> f, shared_ptr<const cxm
        _subtitle_scale = node->number_child<float> ("SubtitleScale");
 }
 
+SubtitleContent::SubtitleContent (shared_ptr<const Film> f, vector<shared_ptr<Content> > c)
+       : Content (f, c)
+{
+       shared_ptr<SubtitleContent> ref = dynamic_pointer_cast<SubtitleContent> (c[0]);
+       assert (ref);
+       
+       for (size_t i = 0; i < c.size(); ++i) {
+               shared_ptr<SubtitleContent> sc = dynamic_pointer_cast<SubtitleContent> (c[i]);
+
+               if (sc->subtitle_offset() != ref->subtitle_offset()) {
+                       throw JoinError (_("Content to be joined must have the same subtitle offset."));
+               }
+
+               if (sc->subtitle_scale() != ref->subtitle_scale()) {
+                       throw JoinError (_("Content to be joined must have the same subtitle scale."));
+               }
+       }
+
+       _subtitle_offset = ref->subtitle_offset ();
+       _subtitle_scale = ref->subtitle_scale ();
+}
+
 void
 SubtitleContent::as_xml (xmlpp::Node* root) const
 {
index c29485feeb6796bf1bbe942c0949da96d1fe4501..854647d181dfc065f11ed191b051d5c939965683 100644 (file)
@@ -34,6 +34,7 @@ class SubtitleContent : public virtual Content
 public:
        SubtitleContent (boost::shared_ptr<const Film>, boost::filesystem::path);
        SubtitleContent (boost::shared_ptr<const Film>, boost::shared_ptr<const cxml::Node>);
+       SubtitleContent (boost::shared_ptr<const Film>, std::vector<boost::shared_ptr<Content> >);
        
        void as_xml (xmlpp::Node *) const;
 
index 484c4fb9ba012edb895b0c466f484af2028bd2d8..7ae4f8b165c34b8195559b55fdc4d7a259c9d047 100644 (file)
@@ -386,47 +386,9 @@ md5_digest (void const * data, int size)
        return s.str ();
 }
 
-/** @param file File name.
- *  @return MD5 digest of file's contents.
- */
-string
-md5_digest (boost::filesystem::path file)
-{
-       ifstream f (file.string().c_str(), std::ios::binary);
-       if (!f.good ()) {
-               throw OpenFileError (file.string());
-       }
-       
-       f.seekg (0, std::ios::end);
-       int bytes = f.tellg ();
-       f.seekg (0, std::ios::beg);
-
-       int const buffer_size = 64 * 1024;
-       char buffer[buffer_size];
-
-       MD5_CTX md5_context;
-       MD5_Init (&md5_context);
-       while (bytes > 0) {
-               int const t = min (bytes, buffer_size);
-               f.read (buffer, t);
-               MD5_Update (&md5_context, buffer, t);
-               bytes -= t;
-       }
-
-       unsigned char digest[MD5_DIGEST_LENGTH];
-       MD5_Final (digest, &md5_context);
-
-       stringstream s;
-       for (int i = 0; i < MD5_DIGEST_LENGTH; ++i) {
-               s << std::hex << std::setfill('0') << std::setw(2) << ((int) digest[i]);
-       }
-
-       return s.str ();
-}
-
 /** @param job Optional job for which to report progress */
 string
-md5_digest_directory (boost::filesystem::path directory, shared_ptr<Job> job)
+md5_digest (vector<boost::filesystem::path> files, shared_ptr<Job> job)
 {
        int const buffer_size = 64 * 1024;
        char buffer[buffer_size];
@@ -434,18 +396,10 @@ md5_digest_directory (boost::filesystem::path directory, shared_ptr<Job> job)
        MD5_CTX md5_context;
        MD5_Init (&md5_context);
 
-       int files = 0;
-       if (job) {
-               for (boost::filesystem::directory_iterator i(directory); i != boost::filesystem::directory_iterator(); ++i) {
-                       ++files;
-               }
-       }
-
-       int j = 0;
-       for (boost::filesystem::directory_iterator i(directory); i != boost::filesystem::directory_iterator(); ++i) {
-               ifstream f (i->path().string().c_str(), std::ios::binary);
+       for (size_t i = 0; i < files.size(); ++i) {
+               ifstream f (files[i].string().c_str(), std::ios::binary);
                if (!f.good ()) {
-                       throw OpenFileError (i->path().string());
+                       throw OpenFileError (files[i].string());
                }
        
                f.seekg (0, std::ios::end);
@@ -460,8 +414,7 @@ md5_digest_directory (boost::filesystem::path directory, shared_ptr<Job> job)
                }
 
                if (job) {
-                       job->set_progress (float (j) / files);
-                       ++j;
+                       job->set_progress (float (i) / files.size ());
                }
        }
 
index 5e568cc27934cb84828cea9a0a25743520dea1b9..7dcd920b7cac3d72974332eda264466faf0cc42a 100644 (file)
@@ -67,8 +67,7 @@ extern double seconds (struct timeval);
 extern void dcpomatic_setup ();
 extern void dcpomatic_setup_gettext_i18n (std::string);
 extern std::vector<std::string> split_at_spaces_considering_quotes (std::string);
-extern std::string md5_digest (boost::filesystem::path);
-extern std::string md5_digest_directory (boost::filesystem::path, boost::shared_ptr<Job>);
+extern std::string md5_digest (std::vector<boost::filesystem::path>, boost::shared_ptr<Job>);
 extern std::string md5_digest (void const *, int);
 extern void ensure_ui_thread ();
 extern std::string audio_channel_name (int);
index 743e6eb18661f3ed88b0c4939bf36e725ff064fe..ca4ed8a9f019bcd9525997f779e76b5c33822af8 100644 (file)
@@ -28,6 +28,7 @@
 #include "colour_conversion.h"
 #include "util.h"
 #include "film.h"
+#include "exceptions.h"
 
 #include "i18n.h"
 
@@ -42,9 +43,11 @@ using std::string;
 using std::stringstream;
 using std::setprecision;
 using std::cout;
+using std::vector;
 using boost::shared_ptr;
 using boost::lexical_cast;
 using boost::optional;
+using boost::dynamic_pointer_cast;
 
 VideoContent::VideoContent (shared_ptr<const Film> f)
        : Content (f)
@@ -96,6 +99,48 @@ VideoContent::VideoContent (shared_ptr<const Film> f, shared_ptr<const cxml::Nod
        _colour_conversion = ColourConversion (node->node_child ("ColourConversion"));
 }
 
+VideoContent::VideoContent (shared_ptr<const Film> f, vector<shared_ptr<Content> > c)
+       : Content (f, c)
+{
+       shared_ptr<VideoContent> ref = dynamic_pointer_cast<VideoContent> (c[0]);
+       assert (ref);
+       
+       for (size_t i = 0; i < c.size(); ++i) {
+               shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> (c[i]);
+
+               if (vc->video_size() != ref->video_size()) {
+                       throw JoinError (_("Content to be joined must have the same picture size."));
+               }
+
+               if (vc->video_frame_rate() != ref->video_frame_rate()) {
+                       throw JoinError (_("Content to be joined must have the same video frame rate."));
+               }
+
+               if (vc->video_frame_type() != ref->video_frame_type()) {
+                       throw JoinError (_("Content to be joined must have the same video frame type."));
+               }
+
+               if (vc->crop() != ref->crop()) {
+                       throw JoinError (_("Content to be joined must have the same crop."));
+               }
+
+               if (vc->ratio() != ref->ratio()) {
+                       throw JoinError (_("Content to be joined must have the same ratio."));
+               }
+
+               if (vc->colour_conversion() != ref->colour_conversion()) {
+                       throw JoinError (_("Content to be joined must have the same colour conversion."));
+               }
+       }
+
+       _video_size = ref->video_size ();
+       _video_frame_rate = ref->video_frame_rate ();
+       _video_frame_type = ref->video_frame_type ();
+       _crop = ref->crop ();
+       _ratio = ref->ratio ();
+       _colour_conversion = ref->colour_conversion ();
+}
+
 void
 VideoContent::as_xml (xmlpp::Node* node) const
 {
index 0fc6d4e7aa6645ba8b0b03a4f5c1a5542321d46b..effca5c61c314c50c4328d02adb5db7ce92dd48e 100644 (file)
@@ -46,6 +46,7 @@ public:
        VideoContent (boost::shared_ptr<const Film>, Time, VideoContent::Frame);
        VideoContent (boost::shared_ptr<const Film>, boost::filesystem::path);
        VideoContent (boost::shared_ptr<const Film>, boost::shared_ptr<const cxml::Node>);
+       VideoContent (boost::shared_ptr<const Film>, std::vector<boost::shared_ptr<Content> >);
 
        void as_xml (xmlpp::Node *) const;
        std::string technical_summary () const;
index a6e2339731b0a5bbb2e657bcf833a040e236e236..254109eb3c882083c52193abf816d8b04d8c871b 100644 (file)
 #include "lib/content_factory.h"
 #include "lib/examine_content_job.h"
 #include "lib/job_manager.h"
+#include "lib/exceptions.h"
 #include "content_menu.h"
 #include "repeat_dialog.h"
 #include "wx_util.h"
 
 using std::cout;
+using std::vector;
 using boost::shared_ptr;
 using boost::weak_ptr;
 using boost::dynamic_pointer_cast;
 
 enum {
        ID_repeat = 1,
+       ID_join,
        ID_find_missing,
        ID_remove
 };
@@ -46,11 +49,13 @@ ContentMenu::ContentMenu (shared_ptr<Film> f, wxWindow* p)
        , _parent (p)
 {
        _repeat = _menu->Append (ID_repeat, _("Repeat..."));
+       _join = _menu->Append (ID_join, _("Join"));
        _find_missing = _menu->Append (ID_find_missing, _("Find missing..."));
        _menu->AppendSeparator ();
        _remove = _menu->Append (ID_remove, _("Remove"));
 
        _parent->Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&ContentMenu::repeat, this), ID_repeat);
+       _parent->Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&ContentMenu::join, this), ID_join);
        _parent->Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&ContentMenu::find_missing, this), ID_find_missing);
        _parent->Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&ContentMenu::remove, this), ID_remove);
 }
@@ -65,6 +70,16 @@ ContentMenu::popup (ContentList c, wxPoint p)
 {
        _content = c;
        _repeat->Enable (!_content.empty ());
+
+       int n = 0;
+       for (ContentList::const_iterator i = _content.begin(); i != _content.end(); ++i) {
+               if (dynamic_pointer_cast<FFmpegContent> (*i)) {
+                       ++n;
+               }
+       }
+       
+       _join->Enable (n > 1);
+       
        _find_missing->Enable (_content.size() == 1 && !_content.front()->path_valid ());
        _remove->Enable (!_content.empty ());
        _parent->PopupMenu (_menu, p);
@@ -94,6 +109,35 @@ ContentMenu::repeat ()
        _content.clear ();
 }
 
+void
+ContentMenu::join ()
+{
+       vector<shared_ptr<Content> > fc;
+       for (ContentList::const_iterator i = _content.begin(); i != _content.end(); ++i) {
+               shared_ptr<FFmpegContent> f = dynamic_pointer_cast<FFmpegContent> (*i);
+               if (f) {
+                       fc.push_back (f);
+               }
+       }
+
+       assert (fc.size() > 1);
+
+       shared_ptr<Film> film = _film.lock ();
+       if (!film) {
+               return;
+       }
+
+       try {
+               shared_ptr<FFmpegContent> joined (new FFmpegContent (film, fc));
+               for (ContentList::const_iterator i = _content.begin(); i != _content.end(); ++i) {
+                       film->remove_content (*i);
+               }
+               film->add_content (joined);
+       } catch (JoinError& e) {
+               error_dialog (_parent, std_to_wx (e.what ()));
+       }
+}
+
 void
 ContentMenu::remove ()
 {
index 2a672532001ca0ddaebcec1f41c04d4d03a599b8..9f4bd4cc4378059d116fc8d071660826ca6e0ac2 100644 (file)
@@ -37,6 +37,7 @@ public:
 
 private:
        void repeat ();
+       void join ();
        void find_missing ();
        void remove ();
        void maybe_found_missing (boost::weak_ptr<Job>, boost::weak_ptr<Content>, boost::weak_ptr<Content>);
@@ -46,6 +47,7 @@ private:
        wxWindow* _parent;
        ContentList _content;
        wxMenuItem* _repeat;
+       wxMenuItem* _join;
        wxMenuItem* _find_missing;
        wxMenuItem* _remove;
 };
index a5e671f238d9e4e9ba856bd9c015215b9620c9db..864abab295640bf36d9d6be32cf9f34cfbd99304 100644 (file)
@@ -23,6 +23,7 @@
 
 using std::string;
 using std::vector;
+using boost::shared_ptr;
 
 BOOST_AUTO_TEST_CASE (util_test)
 {
@@ -43,8 +44,12 @@ BOOST_AUTO_TEST_CASE (util_test)
 
 BOOST_AUTO_TEST_CASE (md5_digest_test)
 {
-       string const t = md5_digest ("test/data/md5.test");
+       vector<boost::filesystem::path> p;
+       p.push_back ("test/data/md5.test");
+       string const t = md5_digest (p, shared_ptr<Job> ());
        BOOST_CHECK_EQUAL (t, "15058685ba99decdc4398c7634796eb0");
 
-       BOOST_CHECK_THROW (md5_digest ("foobar"), OpenFileError);
+       p.clear ();
+       p.push_back ("foobar");
+       BOOST_CHECK_THROW (md5_digest (p, shared_ptr<Job> ()), OpenFileError);
 }