├── LICENSE.txt ├── README.pod ├── cpanfile ├── dist └── unlinkmkv-win64.7z ├── unlinkmkv └── unlinkmkv.ini.dist /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Garret C. Noling 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.pod: -------------------------------------------------------------------------------- 1 | =encoding utf8 2 | =head1 NAME 3 | 4 | unlinkmkv - automate the tedious process of unlinking segmented MKV files. 5 | 6 | =head2 WHAT? 7 | 8 | A segmented MKV is an MKV that utilizes external additional MKV files to create 9 | a "whole" MKV. A common example is that when an anime series uses the same 10 | introduction and ending in every episode, sometimes the encoder will break the 11 | introduction and ending into their own MKV files, and then "link" to the 12 | segments as chapters in each episode's individual MKV. The problem is that very 13 | few players/filters/splitters support this, so this script automates the 14 | mkvtoolnix tools to "rebuild" each episode into "complete" MKVs. 15 | 16 | =head2 SYNOPSIS 17 | 18 | When using C to unlink segments, the chapters when viewing the video 19 | B retained. The MKV spec requires that the segmented files are in the same 20 | directory; so does this. It doesn't matter if you rename the files as long as 21 | all required files are in the same directory as the one being processed; 22 | segmenting is based on internal IDs. Depending on how the original files were 23 | made, you may need to use the C<--fixaudio> and/or C<--fixvideo> options. 24 | 25 | By default, C will now use the current directory as a default video 26 | directory if a file or directory is not given on the command line. This means 27 | that if C is set up and on your path, the majority of the time you 28 | can simply enter the directory and run C with no options. 29 | 30 | Say we have the files: 31 | 32 | princess-resurrection-ep-1.mkv 33 | princess-resurrection-clean-opening.mkv 34 | princess-resurrection-clean-ending.mkv 35 | 36 | And C links the opening and ending files in the 37 | "appropriate places." 38 | 39 | unlinkmkv princess-resurrection-ep-1.mkv 40 | 41 | Doing so will generate a new file under a newly created subdirectory, C<./UMKV> 42 | by default. The file will be about the total size of the original+external 43 | parts. If C<--fixaudio> or C<--fixvideo> is used, which re-encodes the related 44 | parts, about 10% is added to the bitrate by default. 45 | 46 | Now, we test the file in a video player. It's important to check for audio or 47 | video problems, especially when transitioning between where the segments would 48 | have come in. Often times this is the opening/ending and the main show, so seek 49 | inside both places and make sure sound is playing in both places, and the video 50 | looks fine. 51 | 52 | Let's pretend our sound is totally missing in the main video, and the video is 53 | corrupt: 54 | 55 | unlinkmkv --fixaudio --fixvideo princess-resurrection-ep-1.mkv 56 | 57 | What happens is sometimes encoders use codecs that don't play well with 58 | Matroska, and can't be assembled as they were—either the codec (OGG) or the 59 | settings (thanks, encoder guy) being the cause. The C<--fixaudio> and 60 | C<--fixvideo> options simply re-encode the audio and video into a uniform format 61 | that plays nice, with settings that try not to reduce the quality very much (not 62 | noticeable), nor make the files hugemassive. See the FFMPEG notes! 63 | 64 | You could process them all at once with: 65 | 66 | unlinkmkv --fixaudio --fixvideo "/home/user/videos/Princess Resurrection" 67 | 68 | =head2 INSTALLATION 69 | 70 | Clone this repository on Linux or macOS. You can also simply click "Clone or 71 | download" E "Download ZIP" on the top right. I'm also told there is an 72 | unofficial package for Arch Linux that works well. For Windows, see the next 73 | section. Make sure you meet all the dependencies described in the dependency 74 | section. 75 | 76 | Configuration is done in a YAML file named C that should exist 77 | next to the script. An example file is included named C. 78 | Copy this to C and make any required changes for your system. 79 | 80 | =head2 WINDOWS 81 | 82 | This script has been updated for cross platform compatibility and currently is 83 | tested to work on Arch, Debian, Ubuntu, and Windows so long as all required 84 | dependencies are met. For Windows, an archive exists in the C directory 85 | with a compiled version of the script, the required dependencies, and a pre- 86 | configured INI file to use them—this distribution is essentially a "portable" 87 | Windows version. After extracting the archive, either add the new path to your 88 | environment C variable or simply use an absolute path to it on the command 89 | line. 90 | 91 | =head2 DEPENDENCIES 92 | 93 | This script requires: 94 | 95 | Perl >= 5.8.9 96 | MKVToolnix >= 5.1.0 97 | XML::LibXML >= 2.0001 98 | IPC::Run3 99 | File::Which 100 | JSON 101 | String::CRC32 102 | Log::Log4perl 103 | Math::BigFloat 104 | FFMPEG == *real* version of ffmpeg. Recent versions of Ubuntu and Debian come with a prominent ffmpeg fork called libav, which is *not* fully compatible. 105 | Hard drive space, if your temp directory is small, set it to someplace else -- especially if using fixaudio or fixvideo! 106 | 107 | =head2 USAGE 108 | 109 | unlinkmkv {options} {file|path} 110 | 111 | Options: 112 | --tmpdir Set a custom temporary/working folder, /tmp/umkv is the default 113 | --outdir Output directory, required! 114 | --fixaudio, -fa Encode audio, not currently customizable; encodes to 320k AAC for the time being. 115 | --fixvideo, -fv Encode video, not currently customizable; encodes to 8-bit h264 at whatever the existing average bitrate was up to 110% higher then that. 116 | --fixsubtitles, -fs Defaults to on. Sometimes groups don't use uniform subtitle styles and fonts in all segments, it's a good idea to leave this enabled. 117 | --playresx Occasionally the original encoder uses different subtitle resolutions in the different segments. When combined, it causes problems. This forcibly 118 | sets the X resolution for subtitle rendering. 119 | --playresy Same as above, but for the Y axis (vertical). 120 | --ignoredefaultflag Occasionally the default chapter flag exists, but *all* chapters are disabled which confuses the script. Enable on those rare occasions. 121 | --(no-)chapters The script does its best to adjust the chapters, but it's not quite perfect. This disables including chapters in the final file. 122 | --edition Manually specify which edition to keep. Must combine with ignoredefaultflag if non-default edition. Yes, this is pointless otherwise. 123 | --ffmpeg [path] Specify a path to the ffmpeg binary to use. 124 | --mkvext [path] Specify a path to the mkvextract binary to use. 125 | --mkvinfo [path] Specify a path to the mkvinfo binary to use. 126 | --mkvmerge [path] Specify a path to the mkvmerge binary to use. 127 | --fixvideotemplate FFMPEG encoding settings for use with --fixvideo. See Template for description. 128 | --fixaudiotemplate FFMPEG encoding settings for use with --fixaudio. See Template for description. 129 | 130 | =head2 TEMPLATE 131 | 132 | While you can specify a custom template string on the command line, I highly 133 | recommend you do so via the INI file instead. Variables can be used in templates 134 | with the format C<{var_MYVAR}> and will be replaced with their variable when 135 | processed. A couple magic variables are always provided, and custom variables 136 | can be set for additional maths. 137 | 138 | Special variables: 139 | 140 | var_bitrate The bitrate of the original file 141 | var_size The size of the original file 142 | var_duration The duration of the original file 143 | 144 | Variables themselves are defined by prefixing options with C and they 145 | themselves can contain other variables and simple math. The current-as-of-this- 146 | writing defaults are below: 147 | 148 | fixvideotemplate = -c:v libx264 -b:v {var_minrate}k -minrate {var_minrate}k -maxrate {var_maxrate}k -bufsize 1835k -max_muxing_queue_size 4000 149 | fixaudiotemplate = -map 0 -acodec ac3 -ab 320k 150 | var_minrate = (var_size * 1.1) / var_duration 151 | var_maxrate = var_minrate * 2 152 | 153 | Which takes the original filesize and multiplies it by 1.1, then divides by 154 | duration to get the base and minimum bitrates, then max bitrate is simply the 155 | minimum multiplied by two. Feel free to use whatever your version of ffmpeg 156 | supports, but be mindful that certain codecs don't play well with being joined 157 | (OGG Vorbis in particular). 158 | 159 | =head2 SUPPORT 160 | 161 | Feel free to contact me via GitHub or by email at garret -at- wrjb.cc. 162 | 163 | =head2 COPYRIGHT AND LICENSE 164 | 165 | MIT License 166 | 167 | Copyright (c) 2016-2022 Garret C. Noling 168 | 169 | Permission is hereby granted, free of charge, to any person obtaining a copy 170 | of this software and associated documentation files (the "Software"), to deal 171 | in the Software without restriction, including without limitation the rights 172 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 173 | copies of the Software, and to permit persons to whom the Software is 174 | furnished to do so, subject to the following conditions: 175 | 176 | The above copyright notice and this permission notice shall be included in all 177 | copies or substantial portions of the Software. 178 | 179 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 180 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 181 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 182 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 183 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 184 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 185 | SOFTWARE. 186 | -------------------------------------------------------------------------------- /cpanfile: -------------------------------------------------------------------------------- 1 | requires 'Math::BigFloat'; 2 | requires 'String::CRC32'; 3 | requires 'File::Which'; 4 | requires 'IPC::Run3'; 5 | requires 'Log::Log4perl'; 6 | requires 'XML::LibXML'; 7 | requires 'JSON'; 8 | -------------------------------------------------------------------------------- /dist/unlinkmkv-win64.7z: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gnoling/UnlinkMKV/06e17acc1ad494ddaa5033615d4eb82dcbfabd2b/dist/unlinkmkv-win64.7z -------------------------------------------------------------------------------- /unlinkmkv: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | # UnlinkMKV - Undo segment linking in MKV files 3 | # Garret Noling 2013-2022 4 | 5 | require 5.010; 6 | use strict; 7 | use XML::LibXML; 8 | use File::Glob qw/:globally :nocase/; 9 | use Math::BigFloat; 10 | use Getopt::Long qw/:config passthrough/; 11 | use Log::Log4perl qw/:easy/; 12 | use File::Basename; 13 | use String::CRC32; 14 | use IPC::Run3; 15 | use Cwd qw/cwd realpath abs_path/; 16 | use File::Which qw/which/; 17 | use File::Spec::Functions; 18 | use File::Path qw/mkpath/; 19 | 20 | my $loglevel = 'INFO'; 21 | my $logcolors = 0; 22 | GetOptions( 23 | 'll|loglevel=s' => \$loglevel, 24 | 'colors!' => \$logcolors, 25 | ); 26 | my $logdriver = ($logcolors) ? 'Log::Log4perl::Appender::ScreenColoredLevels' : 'Log::Log4perl::Appender::Screen'; 27 | 28 | $loglevel = uc($loglevel); 29 | my $conf = qq( 30 | log4perl.logger = $loglevel, STDINF 31 | log4perl.appender.STDINF = $logdriver 32 | log4perl.appender.STDINF.stderr = 0 33 | log4perl.appender.STDINF.layout = PatternLayout 34 | log4perl.appender.STDINF.layout.ConversionPattern = %x%m{chomp}%n 35 | ); 36 | Log::Log4perl->init_once(\$conf); 37 | Log::Log4perl::NDC->push(""); 38 | INFO "UnlinkMKV"; 39 | UnlinkMKV::more(); 40 | 41 | my $opt; 42 | my $basedir = canonpath(dirname(abs_path($0))); 43 | my $inifile = catfile($basedir, "unlinkmkv.ini"); 44 | my $cwd = canonpath(cwd()); 45 | $opt->{outdir} = canonpath("$cwd/UMKV"); 46 | $opt->{tmpdir} = canonpath("$cwd/UMKV.tmp"); 47 | $opt->{ffmpeg} = "ffmpeg"; 48 | $opt->{mkvext} = "mkvextract"; 49 | $opt->{mkvinfo} = "mkvinfo"; 50 | $opt->{mkvmerge} = "mkvmerge"; 51 | $opt->{mkvpropedit} = "mkvpropedit"; 52 | $opt->{locale} = "en_US"; 53 | $opt->{fixaudio} = 0; 54 | $opt->{fixvideo} = 0; 55 | $opt->{fixsubtitles} = 1; 56 | $opt->{ignoredefaultflag} = 0; 57 | $opt->{ignoresegmentstart} = 0; 58 | $opt->{chapters} = 1; 59 | $opt->{cleanup} = 1; 60 | $opt->{fixvideotemplate} = '-c:v libx264 -b:v %br%k -minrate %br%k -maxrate %br2%k -bufsize 1835k'; 61 | $opt->{fixaudiotemplate} = '-map 0 -acodec ac3 -ab 320k'; 62 | $opt->{edition} = 1; 63 | $opt->{playresx}; 64 | $opt->{playresy}; 65 | 66 | if(-f $inifile) { 67 | open my $F, $inifile or LOGDIE "failed to open unlinkmkv.ini: $!"; 68 | while (my $line = <$F>) { 69 | chomp($line); 70 | if($line =~ /^[\s\t]*([a-z0-9_]+)[\s\t]*=[\s\t]*["']?([^\s\t].*[^\s\t]?)["']?[\s\t]*$/) { 71 | my $key = $1; 72 | my $val = $2; 73 | $val =~ s/\$basedir/$basedir/g; 74 | DEBUG "[ini] [$key] = [$val]"; 75 | $opt->{$key} = ($val =~ /^\d+$/) ? int($val) : $val; 76 | } 77 | else { 78 | DEBUG "[ini] skipping line [$line]"; 79 | } 80 | } 81 | close $F; 82 | } 83 | 84 | GetOptions( 85 | \%$opt, 86 | 'tmpdir=s', 87 | 'fixaudio|fa!', 88 | 'fixvideo|fv!', 89 | 'fixsubtitles|fs!', 90 | 'outdir=s', 91 | 'playresx=i', 92 | 'playresy=i', 93 | 'ignoredefaultflag!', 94 | 'ignoresegmentstart!', 95 | 'chapters!', 96 | 'ffmpeg=s', 97 | 'mkvext=s', 98 | 'mkvinfo=s', 99 | 'mkvmerge=s', 100 | 'fixvideotemplate=s', 101 | 'fixaudiotemplate=s', 102 | 'cleanup!', 103 | 'edition=i', 104 | ); 105 | 106 | $opt->{outdir} = canonpath($opt->{outdir}); 107 | $opt->{ffmpeg} = canonpath(abs_path((which $opt->{ffmpeg})[0])); 108 | $opt->{mkvext} = canonpath(abs_path((which $opt->{mkvext})[0])); 109 | $opt->{mkvinfo} = canonpath(abs_path((which $opt->{mkvinfo})[0])); 110 | $opt->{mkvmerge} = canonpath(abs_path((which $opt->{mkvmerge})[0])); 111 | $opt->{tmpdir} = canonpath($opt->{tmpdir}); 112 | 113 | INFO "Options"; 114 | UnlinkMKV::more(); 115 | foreach my $key (sort keys %$opt) { 116 | INFO "$key: $opt->{$key}"; 117 | } 118 | UnlinkMKV::less(); 119 | 120 | my $UMKV = UnlinkMKV->new($opt); 121 | 122 | if(scalar(@ARGV) == 0) { 123 | push @ARGV, $cwd; 124 | } 125 | 126 | my @LIST; 127 | foreach my $item (@ARGV) { 128 | if(-d $item) { 129 | opendir my $D, $item; 130 | while (my $F = readdir($D)) { 131 | if(-f catfile($item, $F)&& $F =~ /\.mkv$/i && !-f catfile($opt->{outdir}, $F)) { 132 | push @LIST, canonpath(abs_path("$item/$F")); 133 | } 134 | } 135 | closedir $D; 136 | } 137 | elsif(-f $item) { 138 | push @LIST, canonpath(abs_path($item)); 139 | } 140 | } 141 | do { $UMKV->process($_) } 142 | for sort @LIST; 143 | 144 | exit; 145 | 146 | package UnlinkMKV { 147 | use strict; 148 | use XML::LibXML; 149 | use File::Glob qw/:globally :nocase/; 150 | use Math::BigFloat qw/:constant/; 151 | use Getopt::Long qw/:config passthrough/; 152 | use Log::Log4perl qw/:easy/; 153 | use File::Basename; 154 | use String::CRC32; 155 | use IPC::Run3; 156 | use Cwd qw/cwd realpath abs_path/; 157 | use File::Path qw/make_path rmtree/; 158 | use File::Copy; 159 | use File::Spec::Functions; 160 | use JSON; 161 | use Time::Piece; 162 | use Time::Seconds; 163 | 164 | sub new { 165 | my $type = shift; 166 | my $opt = shift; 167 | my ($self) = {}; 168 | bless($self, $type); 169 | $self->{opt} = $opt; 170 | $self->{xml} = XML::LibXML->new(); 171 | $self->mktmp(); 172 | return $self; 173 | } 174 | 175 | sub DESTROY { 176 | my $self = shift; 177 | if(-d $self->{tmpdir} && $self->{opt}->{cleanup}) { 178 | chdir($self->{opt}->{outdir}); 179 | rmtree($self->{tmpdir}, 0, 1); 180 | DEBUG "removed tmp $self->{tmpdir}"; 181 | chdir($self->{opt}->{outdir}); 182 | DEBUG "removed tmp $self->{roottmp}"; 183 | rmdir($self->{roottmp}); 184 | } 185 | DEBUG "exiting"; 186 | } 187 | 188 | sub mktmp { 189 | my $self = shift; 190 | if(!defined $self->{opt}->{tmpdir}) { 191 | $self->{roottmp} = canonpath(cwd() . "/UnlinkMKV/tmp"); 192 | $self->{tmpdir} = canonpath(cwd() . "/UnlinkMKV/tmp/$$"); 193 | } 194 | else { 195 | $self->{roottmp} = $self->{opt}->{tmpdir}; 196 | $self->{tmpdir} = catfile($self->{opt}->{tmpdir}, $$); 197 | } 198 | if(-d $self->{tmpdir}) { 199 | rmtree($self->{tmpdir}, 0, 1); 200 | DEBUG "removed tmp $self->{tmpdir}"; 201 | } 202 | $self->{attachdir} = catfile($self->{tmpdir}, 'attach'); 203 | $self->{partsdir} = catfile($self->{tmpdir}, 'parts'); 204 | $self->{encodesdir} = catfile($self->{tmpdir}, 'encodes'); 205 | $self->{subtitlesdir} = catfile($self->{tmpdir}, 'subtitles'); 206 | make_path( 207 | $self->{tmpdir}, 208 | $self->{attachdir}, 209 | $self->{partsdir}, 210 | $self->{encodesdir}, 211 | $self->{subtitlesdir}, 212 | { verbose => 0 } 213 | ); 214 | DEBUG "created tmp $self->{tmpdir}"; 215 | } 216 | 217 | sub process { 218 | my $self = shift; 219 | my $item = shift; 220 | my $origpath = canonpath(dirname($item)); 221 | chdir($origpath); 222 | INFO "processing $item"; 223 | more(); 224 | INFO "checking if file is segmented"; 225 | if($self->is_linked($item)) { 226 | INFO "generating chapter file"; 227 | more(); 228 | my (@segments, @splits); 229 | my ($parent, $dir, $suffix)= fileparse($item, qr/\.[mM][kK][vV]/); 230 | INFO "loading chapters"; 231 | more(); 232 | my $xml = $self->{xml}->load_xml(string => $self->sys($self->{opt}->{mkvext}, '--ui-language', $self->{opt}->{locale}, 'chapters', $item)); 233 | open my $out_chapters_orig, '>', catfile($self->{tmpdir}, "$parent-chapters-original.xml"); 234 | print {$out_chapters_orig} $xml->toString; 235 | close $out_chapters_orig; 236 | my $offs_time_end = '00:00:00.000000000'; 237 | my $last_time_end = '00:00:00.000000000'; 238 | my $offset = '00:00:00.000000000'; 239 | my $chaptercount = 1; 240 | my $lastuid; 241 | foreach my $edition ($xml->findnodes('//EditionFlagDefault[.=0]')) { 242 | if(!$self->{opt}->{ignoredefaultflag}) { 243 | $edition->parentNode->unbindNode; 244 | WARN "non-default chapter dropped"; 245 | } 246 | else { 247 | INFO "non-default chapter kept on purpose"; 248 | } 249 | } 250 | foreach my $chapter ($xml->findnodes("//EditionEntry[$self->{opt}->{edition}]/ChapterAtom")) { 251 | my ($ChapterTimeStart) = $chapter->findnodes('./ChapterTimeStart/text()'); 252 | my ($ChapterTimeEnd) = $chapter->findnodes('./ChapterTimeEnd/text()'); 253 | my $ChapterEnabled = ($chapter->findvalue('ChapterFlagEnabled') =~ /^\d+$/)? $chapter->findvalue('ChapterFlagEnabled') : 1; 254 | if($chapter->exists('ChapterSegmentUID') && $ChapterEnabled) { 255 | my ($SegmentUID, $SegmentELE, $SegmentUIDText); 256 | ($SegmentELE) = $chapter->findnodes('./ChapterSegmentUID'); 257 | ($SegmentUID) = $chapter->findnodes('./ChapterSegmentUID/text()'); 258 | $SegmentUIDText = $SegmentUID->textContent(); 259 | if($lastuid eq $SegmentUIDText) { 260 | $chapter->removeChild($chapter->findnodes('./ChapterSegmentUID')); 261 | goto PSegment; 262 | } 263 | if($SegmentELE->getAttribute('format') eq 'hex') { 264 | $SegmentUIDText =~ s/\n//g; 265 | $SegmentUIDText =~ s/\s//g; 266 | #$SegmentUIDText =~ s/([a-zA-Z0-9]{2})/ 0x$1/g; 267 | $SegmentUIDText =~ s/^\s//; 268 | } 269 | elsif($SegmentELE->getAttribute('format') eq 'ascii') { 270 | #$SegmentUIDText =~ s/(.)/sprintf("0x%x ",ord($1))/eg; 271 | $SegmentUIDText =~ s/(.)/sprintf("%xg",ord($1))/eg; 272 | $SegmentUIDText =~ s/\s$//; 273 | } 274 | push @segments, { 275 | start => $ChapterTimeStart->textContent(), 276 | stop => $ChapterTimeEnd->textContent(), 277 | id => $SegmentUIDText, 278 | split_start => $last_time_end 279 | }; 280 | push @splits, $last_time_end unless $last_time_end eq '00:00:00.000000000'; 281 | $offset = $self->add_duration_to_timecode($offset, $ChapterTimeEnd->textContent()); 282 | if($offs_time_end eq '00:00:00.000000000' && $chaptercount > 1) { 283 | $ChapterTimeStart->setData($offset); 284 | $ChapterTimeEnd->setData($self->add_duration_to_timecode($offset, $ChapterTimeEnd->textContent())); 285 | } 286 | else { 287 | $ChapterTimeStart->setData($offs_time_end); 288 | $ChapterTimeEnd->setData($self->add_duration_to_timecode($offs_time_end, $ChapterTimeEnd->textContent())); 289 | } 290 | $offs_time_end = $ChapterTimeEnd->textContent(); 291 | $chapter->removeChild($chapter->findnodes('./ChapterSegmentUID')); 292 | INFO "external"; 293 | $lastuid = $SegmentUIDText; 294 | } 295 | else { 296 | PSegment: 297 | eval { 298 | if(defined $ChapterTimeEnd->textContent() && defined $ChapterTimeStart->textContent()) { } 299 | 1; 300 | } or next; 301 | push @segments, { 302 | file => $self->setpart(basename($item), abs_path($item)), 303 | start => $ChapterTimeStart->textContent(), 304 | stop => $ChapterTimeEnd->textContent(), 305 | split_start => $ChapterTimeStart->textContent(), 306 | split_stop => $ChapterTimeEnd->textContent() }; 307 | $last_time_end = $ChapterTimeEnd->textContent(); 308 | $ChapterTimeStart->setData($self->add_duration_to_timecode($ChapterTimeStart->textContent(), $offset)); 309 | $ChapterTimeEnd->setData($self->add_duration_to_timecode($ChapterTimeEnd->textContent(), $offset)); 310 | $offs_time_end = $ChapterTimeEnd->textContent(); 311 | INFO "internal"; 312 | } 313 | more(); 314 | INFO "chapter start " . $ChapterTimeStart->textContent(); 315 | INFO "chapter end " . $ChapterTimeEnd->textContent(); 316 | INFO "offset start " . $offset; 317 | INFO "offset end " . $offs_time_end; 318 | INFO "chapter enabled " . $ChapterEnabled; 319 | less(); 320 | $chaptercount++; 321 | } 322 | less(); 323 | 324 | foreach my $edition ($xml->findnodes("//EditionEntry[position()!=".$self->{opt}->{edition}."]")) { 325 | $edition->unbindNode; 326 | } 327 | 328 | foreach my $edition ($xml->findnodes('//EditionFlagOrdered')) { 329 | $edition->unbindNode; 330 | } 331 | 332 | INFO "writing chapter temporary file"; 333 | more(); 334 | open my $out_chapters, '>', catfile($self->{tmpdir}, "$parent-chapters.xml"); 335 | print {$out_chapters} $xml->toString; 336 | close $out_chapters; 337 | less(); 338 | 339 | INFO "looking for segment parts"; 340 | more(); 341 | if(!defined $self->{segments}) { 342 | foreach my $mkv (<*.mkv>) { 343 | $mkv = canonpath(abs_path(catfile($dir, $mkv))); 344 | my ($id, $dur)= $self->mkvinfo($mkv); 345 | $self->{segments}->{$id} = { 346 | file => $mkv, 347 | dur => $dur, 348 | }; 349 | } 350 | } 351 | for (@segments) { 352 | next unless defined $_->{id}; 353 | if(defined $self->{segments}->{ $_->{id} } && basename($self->{segments}->{ $_->{id} }->{file})ne basename($item)) { 354 | $_->{file} = $self->setpart(basename($self->{segments}->{ $_->{id} }->{file} ), $self->{segments}->{ $_->{id} }->{file}); 355 | INFO "found part $_->{file}"; 356 | } 357 | } 358 | less(); 359 | 360 | INFO "checking that all required segments were found"; 361 | more(); 362 | my $okay_to_proceed = 1; 363 | for (@segments) { 364 | if(defined $_->{id} && !defined $_->{file}) { 365 | WARN "missing segment: $_->{id}"; 366 | $okay_to_proceed = 0; 367 | } 368 | } 369 | if ($okay_to_proceed) { 370 | INFO "all segments found"; 371 | } 372 | else { 373 | WARN "missing segments"; 374 | less(); 375 | return; 376 | } 377 | less(); 378 | 379 | INFO "flac check"; 380 | more(); 381 | if($self->has_flac($item)) { 382 | INFO basename($item) . " has flac, converting to alac temporarily"; 383 | $self->{flac_items}->{$item} = 1; 384 | my $outfile = catfile($self->{tmpdir}, basename($item)."-alac.mkv"); 385 | $self->sys($self->{opt}->{ffmpeg}, '-i', $item, '-vcodec', 'copy', '-map', '0', '-acodec', 'alac', $outfile); 386 | $item = $outfile; 387 | } 388 | for (my $i = 0; $i <= $#segments; $i++) { 389 | if($self->has_flac($segments[$i]->{file})) { 390 | INFO basename($segments[$i]->{file}) . " has flac, converting to alac temporarily"; 391 | $self->{flac_items}->{$segments[$i]->{file}} = 1; 392 | my $outfile = catfile($self->{tmpdir}, basename($segments[$i]->{file})."-alac.mkv"); 393 | $self->sys($self->{opt}->{ffmpeg}, '-i', $segments[$i]->{file}, '-vcodec', 'copy', '-map', '0', '-acodec', 'alac', $outfile) unless -f $outfile; 394 | $segments[$i]->{file} = $outfile; 395 | } 396 | } 397 | less(); 398 | 399 | my @meta; 400 | my $metaid; 401 | INFO "reading metadata"; 402 | { 403 | my $in = 0; 404 | my ($NAME, $TYPE, $LANG, $ID, $DEF); 405 | foreach my $LINE (split /\n/, $self->sys($self->{opt}->{mkvinfo}, '--ui-language', $self->{opt}->{locale}, $item)) { 406 | chomp $LINE; 407 | if($LINE =~ /^\| ?\+/) { 408 | $in = 0; 409 | if(defined $TYPE) { 410 | my $m; 411 | if(defined $LANG) { 412 | push @$m, '--edit', "track:$TYPE".$metaid->{$TYPE}, '--set', "language=$LANG"; 413 | } 414 | if(defined $NAME) { 415 | push @$m, '--edit', "track:$TYPE".$metaid->{$TYPE}, '--set', "name=\"$NAME\""; 416 | } 417 | if(defined $DEF) { 418 | push @$m, '--edit', "track:$TYPE".$metaid->{$TYPE}, '--set', "flag-default=$DEF"; 419 | } 420 | push @meta, $m; 421 | } 422 | $NAME = undef; 423 | $LANG = undef; 424 | $TYPE = undef; 425 | $ID = undef; 426 | } 427 | if($LINE =~ /^\| \+ Title: (.*)/) { 428 | my $m; 429 | push @$m, '--edit', 'info', '--set', "title=\"$1\""; 430 | push @meta, $m; 431 | } 432 | if($LINE =~ /^\| \+ A track/ || $LINE =~ /^\| \+ Track/) { 433 | $in = 1; 434 | } 435 | elsif($in && $LINE =~ /\| \+ Language: (.*)$/) { 436 | $LANG = $1; 437 | } 438 | elsif($in && $LINE =~ /\| \+ Track type: (.*)/) { 439 | if($1 eq 'audio') { 440 | $TYPE = 'a'; 441 | } 442 | elsif($1 eq 'subtitles') { 443 | $TYPE = 's'; 444 | } 445 | $metaid->{$TYPE}++; 446 | } 447 | elsif($in && $LINE =~ /\| \+ Name: (.*)/) { 448 | $NAME = $1; 449 | } 450 | elsif($in && $LINE =~ /\| \+ Default flag: (.*)/) { 451 | $DEF = $1; 452 | } 453 | } 454 | } 455 | 456 | INFO "searching attachments"; 457 | more(); 458 | foreach my $seg (@segments) { 459 | my $file = "$seg->{file}"; 460 | my $in = 0; 461 | my ($N, $T, $D, $U); 462 | INFO "$file"; 463 | more(); 464 | for (split /\n/, $self->sys($self->{opt}->{mkvinfo}, '--ui-language', $self->{opt}->{locale}, $file)) { 465 | chomp; 466 | if($_ =~ /\|[\s\t]+\+[\s\t]+Attached/i) { 467 | $in = 1; 468 | } 469 | elsif($in && $_ =~ /File name: (.*)/i) { 470 | $N = $1; 471 | } 472 | elsif($in && $_ =~ /Mime type: (.*)/i) { 473 | $T = $1; 474 | } 475 | elsif($in && $_ =~ /File data, size: (.*)/i) { 476 | $D = $1; 477 | } 478 | elsif($in && $_ =~ /File data: size (.*)/i) { 479 | $D = $1; 480 | } 481 | elsif($in && $_ =~ /File UID: (.*)/i) { 482 | $U = $1; 483 | } 484 | if(defined $N && defined $T && defined $D && defined $U) { 485 | if(!-f "$self->{attachdir}/$N") { 486 | push @{ $seg->{attachments} }, { name => $N, type => $T, data => $D, UID => $U }; 487 | INFO "found $N"; 488 | } 489 | else { 490 | INFO "skipping (duplicate) $N"; 491 | } 492 | undef $N; 493 | undef $T; 494 | undef $D; 495 | undef $U; 496 | } 497 | } 498 | if(defined $seg->{attachments} && @{ $seg->{attachments} } > 0) { 499 | INFO "extracting attachments..."; 500 | my $dir = cwd(); 501 | chdir($self->{attachdir}); 502 | $self->sys($self->{opt}->{mkvext}, '--ui-language', $self->{opt}->{locale}, 'attachments', $file, (1 .. $#{ $seg->{attachments} } + 1)); 503 | chdir($dir); 504 | } 505 | less(); 506 | } 507 | less(); 508 | 509 | my @atts; 510 | opendir my $D, $self->{attachdir} or LOGDIE "failed to open attachment directory: $!"; 511 | while (my $item = readdir($D)) { 512 | my $F = catfile($self->{attachdir}, $item); 513 | if(-f $F) { 514 | push @atts, ('--attachment-mime-type', 'application/x-truetype-font', '--attach-file', $F); 515 | } 516 | } 517 | closedir $D; 518 | 519 | if(scalar(@splits) > 0) { 520 | INFO "creating " . scalar(@splits + 1). " splits from $item"; 521 | more(); 522 | $self->sys($self->{opt}->{mkvmerge}, '--ui-language', $self->{opt}->{locale}, '--no-chapters', '-o', catfile($self->{partsdir}, "split-%03d.mkv"), $item, '--split', 'timecodes:' . join(',', @splits)); 523 | less(); 524 | } 525 | 526 | INFO "setting parts"; 527 | more(); 528 | my (@parts, $LAST); 529 | my $count = 1; 530 | foreach my $segment (@segments) { 531 | if(defined $segment->{id} && ($self->{opt}->{ignoresegmentstart} || $segment->{start} =~ /^00:00:00\./) || ($LAST ne $segment->{file} && scalar(@splits) == 0)) { 532 | DEBUG "part $segment->{file}"; 533 | push @parts, $segment->{file}; 534 | } 535 | elsif($LAST ne $segment->{file}) { 536 | my $f = catfile($self->{partsdir}, sprintf("split-%03d.mkv", $count)); 537 | DEBUG "part $f"; 538 | push @parts, $f; 539 | $count++; 540 | } 541 | $LAST = $segment->{file}; 542 | } 543 | less(); 544 | 545 | my $subs; 546 | if($self->{opt}->{fixsubtitles}) { 547 | INFO "extracting subs"; 548 | more(); 549 | foreach my $part (@parts) { 550 | DEBUG "$part"; 551 | my $in = 0; 552 | my $sub = 0; 553 | my ($N, $T, $D, $U); 554 | for (split /\n/, $self->sys($self->{opt}->{mkvinfo}, '--ui-language', $self->{opt}->{locale}, $part)) { 555 | chomp; 556 | if($_ =~ /^\| \+ A track/ || $_ =~ /^\| \+ Track/) { 557 | $in = 1; 558 | undef $N; 559 | undef $T; 560 | undef $D; 561 | undef $U; 562 | $sub = 0; 563 | } 564 | elsif($in && $_ =~ /Track type: subtitles/) { 565 | $sub = 1; 566 | } 567 | elsif($in && $_ =~ /Track number: .*: (\d)\)$/) { 568 | $T = $1; 569 | } 570 | if(defined $in && $sub && $T) { 571 | my $sf = catfile($self->{subtitlesdir}, basename($part) . "-$T.ass"); 572 | $self->sys($self->{opt}->{mkvext}, '--ui-language', $self->{opt}->{locale}, 'tracks', $part, "$T:$sf"); 573 | push @{ $subs->{$part} }, $sf; 574 | undef $T; 575 | $in = 0; 576 | $sub = 0; 577 | } 578 | } 579 | } 580 | less(); 581 | 582 | INFO "making substyles unique"; 583 | more(); 584 | my $styles; 585 | foreach my $f (keys %$subs) { 586 | push @$styles, @{ $self->uniquify_substyles($subs->{$f})}; 587 | } 588 | less(); 589 | 590 | INFO "mashing unique substyles to all parts"; 591 | more(); 592 | foreach my $f (keys %$subs) { 593 | $self->mush_substyles($subs->{$f}, $styles); 594 | } 595 | less(); 596 | 597 | INFO "remuxing subtitles"; 598 | more(); 599 | foreach my $f (keys %$subs) { 600 | DEBUG $f; 601 | my @stracks; 602 | foreach my $T (@{ $subs->{$f} }) { 603 | push @stracks, $T; 604 | } 605 | $self->sys($self->{opt}->{mkvmerge}, '--ui-language', $self->{opt}->{locale}, '-o', "$f-fixsubs.mkv", '--no-chapters', '--no-subs', $f, @stracks, @atts); 606 | $self->replace($f, "$f-fixsubs.mkv"); 607 | } 608 | less(); 609 | } 610 | 611 | if($self->{opt}->{fixvideo} || $self->{opt}->{fixaudio}) { 612 | INFO "encoding parts"; 613 | more(); 614 | foreach my $part (@parts) { 615 | my @vopt = qw/-vcodec copy/; 616 | my @aopt = qw/-map 0 -acodec copy/; 617 | WARN $part; 618 | if($self->{opt}->{fixvideo}) { 619 | @vopt = undef; 620 | my $VV = $self->parseoptvars($self->ffdetails($part)); 621 | $self->{opt}->{fixvideotemplate} =~ s/\t/ /g; 622 | $self->{opt}->{fixvideotemplate} =~ s/ / /g; 623 | do { 624 | $self->{opt}->{fixvideotemplate} =~ s/{$_}/$VV->{$_}/ig; 625 | } 626 | for keys %$VV; 627 | @vopt = split / /, $self->{opt}->{fixvideotemplate}; 628 | } 629 | if($self->{opt}->{fixaudio}) { 630 | @aopt = undef; 631 | $self->{opt}->{fixaudiotemplate} =~ s/\t/ /g; 632 | $self->{opt}->{fixaudiotemplate} =~ s/ / /g; 633 | @aopt = split / /, $self->{opt}->{fixaudiotemplate}; 634 | } 635 | $self->sys($self->{opt}->{ffmpeg}, '-i', $part, @vopt, @aopt, "$part-fixed.mkv"); 636 | $self->replace($part, "$part-fixed.mkv"); 637 | } 638 | less(); 639 | } 640 | 641 | INFO "building file"; 642 | more(); 643 | my @PRTS; 644 | foreach my $part (@parts) { 645 | push @PRTS, $part; 646 | push @PRTS, '+'; 647 | } 648 | pop @PRTS; 649 | if($self->{opt}->{chapters}) { 650 | $self->sys($self->{opt}->{mkvmerge}, '--ui-language', $self->{opt}->{locale}, '--no-chapters', '-M', '--chapters', catfile($self->{tmpdir}, "$parent-chapters.xml" ), '-o', catfile( $self->{encodesdir}, basename($item) ), @PRTS); 651 | } 652 | else { 653 | $self->sys($self->{opt}->{mkvmerge}, '--ui-language', $self->{opt}->{locale}, '--no-chapters', '-M', '-o', catfile( $self->{encodesdir}, basename($item) ), @PRTS); 654 | } 655 | less(); 656 | 657 | INFO "fixing subs, again... (maybe an mkvmerge issue?)"; 658 | more(); 659 | if($self->{opt}->{fixsubtitles}) { 660 | my @FS; 661 | my $in = 0; 662 | my $sub = 0; 663 | my ($N, $T, $D, $U); 664 | for (split /\n/, $self->sys($self->{opt}->{mkvinfo}, '--ui-language', $self->{opt}->{locale}, catfile($self->{encodesdir}, basename($item)))) { 665 | chomp; 666 | if($_ =~ /^\| \+ A track/ || $_ =~ /^\| \+ Track/) { 667 | $in = 1; 668 | undef $N; 669 | undef $T; 670 | undef $D; 671 | undef $U; 672 | $sub = 0; 673 | } 674 | elsif($in && $_ =~ /Track type: subtitles/) { 675 | $sub = 1; 676 | } 677 | elsif($in && $_ =~ /Track number: .*: (\d)\)$/) { 678 | $T = $1; 679 | } 680 | if(defined $in && $sub && $T) { 681 | $self->sys($self->{opt}->{mkvext}, '--ui-language', $self->{opt}->{locale}, 'tracks', catfile($self->{encodesdir}, basename($item)), "$T:" . catfile($self->{encodesdir}, "$T.ass")); 682 | push @FS, catfile($self->{encodesdir}, "$T.ass"); 683 | undef $T; 684 | $in = 0; 685 | $sub = 0; 686 | } 687 | } 688 | $self->sys($self->{opt}->{mkvmerge}, '--ui-language', $self->{opt}->{locale}, '-o', catfile($self->{encodesdir}, "fixed." . basename($item) ), '-S', catfile( $self->{encodesdir}, basename($item) ), @FS); 689 | $self->replace(catfile($self->{encodesdir}, basename($item)), catfile($self->{encodesdir}, "fixed." . basename($item))); 690 | } 691 | less(); 692 | 693 | if(scalar(@meta)>0 && -f catfile($self->{encodesdir}, basename($item))) { 694 | INFO "applying metadata"; 695 | more(); 696 | foreach my $M (@meta) { 697 | $self->sys($self->{opt}->{mkvpropedit}, '--ui-language', $self->{opt}->{locale}, @$M, catfile($self->{encodesdir}, basename($item))); 698 | } 699 | less(); 700 | } 701 | 702 | 703 | if(defined $self->{flac_items} && -f catfile($self->{encodesdir}, basename($item))) { 704 | INFO "encoding back to flac"; 705 | more(); 706 | my $file = catfile($self->{encodesdir}, basename($item)); 707 | (my $final = catfile($self->{encodesdir}, basename($item))) =~ s/-alac\.mkv$//; 708 | $self->sys($self->{opt}->{ffmpeg}, '-i', $file, '-vcodec', 'copy', '-map', '0', '-acodec', 'flac', $final); 709 | $self->replace($file, $final); 710 | less(); 711 | } 712 | 713 | INFO "moving built file to final destination"; 714 | more(); 715 | if(-f catfile($self->{encodesdir}, basename($item))) { 716 | make_path($self->{opt}->{outdir}, { verbose => 0 }); 717 | (my $final = catfile($self->{opt}->{outdir}, basename($item))) =~ s/-alac\.mkv$//; 718 | move(catfile($self->{encodesdir}, basename($item)), $final); 719 | } 720 | else { 721 | WARN "file failed to build"; 722 | } 723 | less(); 724 | } 725 | $self->mktmp() if $self->{opt}->{cleanup}; 726 | less(); 727 | } 728 | 729 | sub softfail { 730 | my $self = shift; 731 | $self->mktmp() if $self->{opt}->{cleanup}; 732 | less(); 733 | } 734 | 735 | sub parseoptvars { 736 | my $self = shift; 737 | my $vars = shift; 738 | my $V; 739 | foreach my $key (keys %$vars) { 740 | $V->{"var_$key"} = "$vars->{$key}"; 741 | } 742 | foreach my $var (grep($_ =~ /^var_/, keys %{ $self->{opt} })) { 743 | $V->{$var} = "$self->{opt}->{$var}"; 744 | do { $V->{$_} = undef unless defined $V->{$_} } 745 | for $V->{$var} =~ /([a-z]\w+(?:'[a-z]\w+)*)/ig; 746 | } 747 | for (0 .. (int(scalar(keys %$V ))* int(scalar( keys %$V)))) { 748 | foreach my $var (keys %$V) { 749 | foreach my $word ($V->{$var} =~ /([a-z]\w+(?:'[a-z]\w+)*)/ig) { 750 | if(defined $V->{$word}) { 751 | $V->{$var} =~ s/$word/$V->{$word}/; 752 | if($V->{$var} !~ /[a-z]/i) { 753 | $V->{$var} = int((eval $V->{$var})+ .5); 754 | } 755 | } 756 | } 757 | } 758 | } 759 | return $V; 760 | } 761 | 762 | sub ffdetails { 763 | my $self = shift; 764 | my $file = shift; 765 | my $duration = 0; 766 | my $size = int((-s $file)/ 1024 + .5); 767 | my $br = 0; 768 | foreach my $line (split /\n/, $self->sys($self->{opt}->{ffmpeg}, '-i', $file)) { 769 | if($line =~ /duration: (\d+):(\d+):(\d+\.\d+),/i) { 770 | $duration = ($1 * 3600 ) + ($2 * 60)+ int($3 + .5); 771 | DEBUG "duration [$1:$2:$3] = $duration seconds"; 772 | } 773 | if($line =~ /duration.*bitrate: (\d+) k/i) { 774 | $br = $1; 775 | DEBUG "bitrate ${br}k"; 776 | } 777 | } 778 | return { 779 | bitrate => $br, 780 | size => $size, 781 | duration => $duration, 782 | }; 783 | } 784 | 785 | sub replace { 786 | my $self = shift; 787 | my $dest = shift; 788 | my $source = shift; 789 | unlink($dest); 790 | move($source, $dest); 791 | } 792 | 793 | sub is_linked { 794 | my $self = shift; 795 | my $item = shift; 796 | my $linked = 0; 797 | more(); 798 | foreach my $line (split /\n/, $self->sys($self->{opt}->{mkvext}, '--ui-language', $self->{opt}->{locale}, 'chapters', $item)) { 799 | if($line =~ /{partsdir}, $link); 818 | DEBUG "copying part $file to $part"; 819 | copy($file, $part); 820 | return $part; 821 | } 822 | 823 | sub uniquify_substyles { 824 | my $self = shift; 825 | my $S = shift; 826 | my @styles; 827 | foreach my $T (@$S) { 828 | DEBUG $T; 829 | my $uniq = crc32($T); 830 | open my $O, '>', "$T.new"; 831 | open my $F, '<', $T; 832 | my $in = 0; 833 | my $di = 0; 834 | my $key; 835 | while (my $line = <$F>) { 836 | 837 | if($line =~ /^\[/ && $line =~ /^\[V4\+ Styles/) { 838 | $in = 1; 839 | $di = 0; 840 | $key = undef; 841 | } 842 | elsif(($in || $di)&& !defined $key && $line =~ /^Format:/i) { 843 | my $test = "$line"; 844 | $test =~ s/ //g; 845 | $test =~ s/^format://i; 846 | my (@parts) = split /,/, $test; 847 | my $c = 0; 848 | foreach my $part (@parts) { 849 | if($in && $part =~ /^name$/i) { 850 | $key = $c; 851 | } 852 | elsif($di && $part =~ /^style$/i) { 853 | $key = $c; 854 | } 855 | $c++; 856 | } 857 | } 858 | elsif($in && defined $key && $line =~ /^style:/i) { 859 | $line =~ s/^style:\s+?//i; 860 | my (@parts) = split /,/, $line; 861 | $parts[$key] = "$parts[$key] u$uniq"; 862 | $line = "Style: " . join(',', @parts); 863 | push @styles, $line; 864 | DEBUG $line; 865 | } 866 | elsif($line =~ /^\[Events/i) { 867 | $in = 0; 868 | $di = 1; 869 | $key = undef; 870 | } 871 | elsif($di && defined $key && $line =~ /^dialogue:/i) { 872 | $line =~ s/^dialogue: //i; 873 | my (@parts) = split /,/, $line; 874 | $parts[$key] = "$parts[$key] u$uniq"; 875 | $line = "Dialogue: " . join(',', @parts); 876 | } 877 | print $O $line; 878 | } 879 | close $F; 880 | close $O; 881 | unlink $T; 882 | move("$T.new", $T); 883 | } 884 | return \@styles; 885 | } 886 | 887 | sub mush_substyles { 888 | my $self = shift; 889 | my $S = shift; 890 | my $styles = shift; 891 | foreach my $T (@$S) { 892 | open my $F, '<', $T; 893 | my @lines = <$F>; 894 | close $F; 895 | open my $F, '>', $T; 896 | my $in = 0; 897 | foreach my $line (@lines) { 898 | if($line =~ /^\[/ && $line =~ /^\[V4\+ Styles/) { 899 | $in = 1; 900 | print $F $line; 901 | } 902 | elsif($in && $line =~ /^format:/i) { 903 | print $F $line; 904 | do { 905 | print $F $_; 906 | } 907 | for @$styles; 908 | } 909 | elsif($in && $line =~ /^style:/i) { 910 | 911 | #do nothing 912 | } 913 | elsif($in && $line =~ /^\[/) { 914 | $in = 0; 915 | print $F $line; 916 | } 917 | elsif(defined $self->{opt}->{playresx} && $line =~ /^PlayResX:/) { 918 | print $F "PlayResX: $self->{opt}->{playresx}\n"; 919 | } 920 | elsif(defined $self->{opt}->{playresy} && $line =~ /^PlayResY:/) { 921 | print $F "PlayResY: $self->{opt}->{playresy}\n"; 922 | } 923 | else { 924 | print $F $line; 925 | } 926 | } 927 | close $F; 928 | } 929 | } 930 | 931 | sub mkvinfo { 932 | my $self = shift; 933 | my $file = shift; 934 | my $info = decode_json($self->sys($self->{opt}->{mkvmerge}, '-F', 'json', '--identify', $file)); 935 | my $val = Time::Seconds->new($info->{container}->{properties}->{duration}/1e9); 936 | return ($info->{container}->{properties}->{segment_uid}, ts_timecode($val)); 937 | } 938 | 939 | sub ts_timecode { 940 | my $s = shift; 941 | my $hours = sprintf("%02d", $s->hours); $s -= ($hours * 3600); 942 | my $mins = sprintf("%02d", $s->minutes); $s -= ($mins * 60); 943 | my $sec = sprintf("%02d", $s->seconds); 944 | my $str = "$hours:$mins:$sec"; 945 | return $str; 946 | } 947 | 948 | sub add_duration_to_timecode { 949 | my $self = shift; 950 | my $time = shift; 951 | my $dur = shift; 952 | my ($th, $tm, $ts)= split /:/, $time; 953 | my ($dh, $dm, $ds)= split /:/, $dur; 954 | $ts = Math::BigFloat->new("$ts"); 955 | $ds = Math::BigFloat->new("$ds"); 956 | my $small = Math::BigFloat->new("0.000000001"); 957 | my $sixty = Math::BigFloat->new("60.000000000"); 958 | my $ds_small = $ds->copy(); 959 | $ds_small->badd($small); 960 | $ts->badd($ds_small); 961 | if($ts->bge($sixty)) { 962 | $ts->bsub($sixty); 963 | $dm++; 964 | } 965 | $tm += $dm; 966 | if($tm >= 60) { 967 | $tm = $tm - 60; 968 | $dh++; 969 | } 970 | $th += $dh; 971 | return sprintf("%02d:%02d:%02.9f", $th, $tm, $ts); 972 | } 973 | 974 | sub has_flac { 975 | my $self = shift; 976 | my $item = shift; 977 | my $flac = 0; 978 | foreach my $LINE (split /\n/, $self->sys($self->{opt}->{mkvinfo}, '--ui-language', $self->{opt}->{locale}, $item)) { 979 | if(defined $LINE && $LINE =~ /Codec ID: A_FLAC/) { 980 | $flac = 1; 981 | } 982 | } 983 | return $flac; 984 | } 985 | 986 | sub sys { 987 | my $self = shift; 988 | my @app = @_; 989 | my $buf; 990 | TRACE "sys > @app"; 991 | run3( 992 | \@app, 993 | undef, 994 | sub { 995 | my $line = shift; 996 | TRACE "sys < $line"; 997 | $buf .= $line; 998 | }, 999 | sub { 1000 | my $line = shift; 1001 | TRACE "sys !! $line"; 1002 | $buf .= $line; 1003 | } ); 1004 | return $buf; 1005 | } 1006 | 1007 | sub more { 1008 | Log::Log4perl::NDC->push(" "); 1009 | } 1010 | 1011 | sub less { 1012 | Log::Log4perl::NDC->pop(); 1013 | print "\n"; 1014 | } 1015 | 1016 | } 1017 | -------------------------------------------------------------------------------- /unlinkmkv.ini.dist: -------------------------------------------------------------------------------- 1 | #outdir = /home/USER/Desktop/UnlinkMKV 2 | #tmpdir = /home/USER/tmp/umkv 3 | ffmpeg = /opt/ffmpeg/bin/ffmpeg 4 | mkvext = /usr/bin/mkvextract 5 | mkvinfo = /usr/bin/mkvinfo 6 | mkvmerge = /usr/bin/mkvmerge 7 | mkvpropedit = /usr/bin/mkvpropedit 8 | fixaudio = 0 9 | fixvideo = 0 10 | fixsubtitles = 1 11 | ignoredefaultflag = 0 12 | chapters = 1 13 | fixvideotemplate = -c:v libx264 -b:v {var_minrate}k -minrate {var_minrate}k -maxrate {var_maxrate}k -bufsize 1835k -max_muxing_queue_size 4000 14 | fixaudiotemplate = -map 0 -acodec ac3 -ab 320k 15 | var_minrate = (var_size * 1.1) / var_duration 16 | var_maxrate = var_minrate * 2 17 | locale = en_US 18 | 19 | --------------------------------------------------------------------------------